From 0de768e924df2e42a0ef4534fc45703e5489c8a1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 15 May 2026 13:02:58 +0000 Subject: [PATCH] docs: add ConnectionProviderService guidelines and enable detailed activity logging - Added comprehensive documentation for `ConnectionProviderService`, outlining architectural design, coding standards, and operational flows. - Enhanced `ConnectionProvider` and `ConnectedApp` models with Spatie activity logging configuration for detailed CRUD activity tracking. - Updated delete methods in service classes to include audit logs and cascade behavior refinement. - Revised activity log migration schema for stricter typing and method structure. --- app/Models/ConnectedApp.php | 18 +++- app/Models/ConnectionProvider.php | 18 +++- app/Services/ConnectedAppService.php | 14 +-- app/Services/ConnectionProviderService.php | 29 +++++- app/Services/README.md | 98 +++++++++++++++++++ config/activitylog.php | 5 +- ...05_15_115455_create_activity_log_table.php | 6 +- 7 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 app/Services/README.md diff --git a/app/Models/ConnectedApp.php b/app/Models/ConnectedApp.php index c06bc5f..63cd36a 100644 --- a/app/Models/ConnectedApp.php +++ b/app/Models/ConnectedApp.php @@ -4,9 +4,11 @@ namespace App\Models; -use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, SoftDeletes}; use Illuminate\Database\Eloquent\Attributes\Fillable; +use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, SoftDeletes}; use Illuminate\Database\Eloquent\Relations\HasOne; +use Spatie\Activitylog\Models\Concerns\LogsActivity; +use Spatie\Activitylog\Support\LogOptions; #[Fillable([ 'name', @@ -17,7 +19,7 @@ ])] class ConnectedApp extends Model { - use SoftDeletes; + use LogsActivity, SoftDeletes; public function protocol(): HasOne { @@ -33,4 +35,16 @@ public function status(): HasOne { return $this->hasOne(ConnectionStatus::class, 'id', 'connection_status_id'); } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() // Log all fillable attributes + ->logOnlyDirty() // Log only attributes that actually changed + ->dontLogIfAttributesChangedOnly(['updated_at']) // Skip if only updated_at changed + ->dontLogEmptyChanges() // Prevent saving logs with no changed attributes + ->setDescriptionForEvent(fn ( + string $eventName + ) => ":causer.name {$eventName} connected app ':subject.name'"); + } } diff --git a/app/Models/ConnectionProvider.php b/app/Models/ConnectionProvider.php index a2516cb..f342b78 100644 --- a/app/Models/ConnectionProvider.php +++ b/app/Models/ConnectionProvider.php @@ -4,16 +4,30 @@ namespace App\Models; -use Illuminate\Database\Eloquent\{Model, Relations\HasMany, SoftDeletes}; use Illuminate\Database\Eloquent\Attributes\Fillable; +use Illuminate\Database\Eloquent\{Model, Relations\HasMany, SoftDeletes}; +use Spatie\Activitylog\Models\Concerns\LogsActivity; +use Spatie\Activitylog\Support\LogOptions; #[Fillable(['name', 'slug'])] final class ConnectionProvider extends Model { - use SoftDeletes; + use LogsActivity, SoftDeletes; public function connectedApps(): HasMany { return $this->hasMany(ConnectedApp::class); } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() // Log all fillable attributes + ->logOnlyDirty() // Log only attributes that actually changed + ->dontLogIfAttributesChangedOnly(['updated_at']) // Skip if only updated_at changed + ->dontLogEmptyChanges() // Prevent saving logs with no changed attributes + ->setDescriptionForEvent(fn ( + string $eventName + ) => ":causer.name {$eventName} connection provider ':subject.name'"); + } } diff --git a/app/Services/ConnectedAppService.php b/app/Services/ConnectedAppService.php index faca2c8..805bc0e 100644 --- a/app/Services/ConnectedAppService.php +++ b/app/Services/ConnectedAppService.php @@ -39,9 +39,10 @@ public function update(int $id, ConnectAppRequest $data): void throw new InvalidArgumentException('Invalid id'); } DB::transaction( - fn () => ConnectedApp::query() - ->where('id', $id) - ->update($data->toArray()) + function () use ($id, $data): void { + $app = ConnectedApp::query()->findOrFail($id); + $app->update($data->toArray()); + } ); } @@ -56,9 +57,10 @@ public function delete(int $id): void } DB::transaction( - fn () => ConnectedApp::query() - ->where('id', $id) - ->delete() + function () use ($id): void { + $app = ConnectedApp::query()->findOrFail($id); + $app->delete(); + } ); } diff --git a/app/Services/ConnectionProviderService.php b/app/Services/ConnectionProviderService.php index 71d4bef..b643fc9 100644 --- a/app/Services/ConnectionProviderService.php +++ b/app/Services/ConnectionProviderService.php @@ -58,16 +58,20 @@ public function getAll(): PaginatedDataCollection public function update(string $slug, StoreConnectionProviderRequestData $data): void { DB::transaction( - fn() => ConnectionProvider::query() - ->where('slug', $slug) - ->update($data->toArray()) + function () use ($slug, $data): void { + $app = ConnectionProvider::query() + ->where('slug', $slug); + + $app->update($data->toArray()); + } ); } /** * Delete the connection provider and associated connected apps. + * * @throws ModelNotFoundException when the connection provider is not found. - * @throws Throwable when an error occurs during the deletion. + * @throws Throwable when an error occurs during the deletion. */ public function delete(string $slug): void { @@ -76,12 +80,27 @@ function () use ($slug): void { $provider = ConnectionProvider::query() ->where('slug', $slug) ->firstOrFail(); + $deletedApps = $provider->connectedApps() + ->select(['id', 'name']) + ->get() + ->toArray(); // Note: This performs a mass soft-delete query. // Model events for ConnectedApp will NOT fire. $provider->connectedApps()->delete(); + // Stop the default logging + $provider->disableLogging()->delete(); - $provider->delete(); + activity() + ->performedOn($provider) + ->event('provider_cascade_deleted') + ->withProperties([ + 'slug' => $slug, + 'name' => $provider->name, + 'deleted_apps_count' => count($deletedApps), + 'deleted_apps' => $deletedApps, + ]) + ->log(":causer.name Deleted connection provider '{$provider->name}' alongside ".count($deletedApps).' connected apps.'); } ); } diff --git a/app/Services/README.md b/app/Services/README.md new file mode 100644 index 0000000..d831e2e --- /dev/null +++ b/app/Services/README.md @@ -0,0 +1,98 @@ +# Service Class Guidelines & Flow: ConnectionProviderService + +This document outlines the architectural flow, design patterns, and coding guidelines implemented within the +ConnectionProviderService class. It serves as a reference for maintaining this service and acts as a blueprint for +creating similar service classes within the application. + +## 1. General Coding Guidelines + +The service class adheres to several strict Laravel and general PHP best practices to ensure robustness, predictability, +and maintainability. + +1. Strict Typing: The file declares strict_types=1 at the top. All method arguments and return values must have explicit + type hints. + +2. Data Transfer Objects (DTOs): Instead of passing raw arrays or generic Laravel Request objects, the service utilizes + strongly typed DTOs (via Spatie\LaravelData). + + Input: StoreConnectionProviderRequestData + + Output: ConnectionProviderData, PaginatedDataCollection + +3. Database Transactions: All data mutation methods (Create, Update, Delete) are wrapped within DB::transaction() + closures to guarantee database integrity. If any part of the operation fails, the entire transaction rolls back. + +4. The #[NoDiscard] Attribute: Read-only methods (search, getAll, getProvider) utilize the #[NoDiscard] attribute. This + strictly enforces that the calling code must capture and use the returned data, preventing logic leaks where a + developer calls a getter but ignores the result. + +5. Explicit Exception Documentation: Every method that can throw an exception documents it clearly in the PHPDoc block ( + e.g., @throws ModelNotFoundException, @throws Throwable). + +## 2. Architectural Flow by Operation + +### A. Read Operations + +Read operations are optimized for memory and performance by explicitly selecting only the necessary columns. + +* search(string $value) + + Flow: Initiates an Eloquent query targeting the slug or name columns using a LIKE operator (%value%). + + Returns: An Eloquent Collection of ConnectionProvider models. + +* getAll() + + Flow: Queries the database selecting only id, name, and slug. It then paginates the results. + + Returns: The paginated result is transformed into a PaginatedDataCollection containing ConnectionProviderData objects. + +* getProvider(string $slug) + + Flow: Looks up a specific provider by its slug, selecting only id, name, and slug. + + Returns: Transforms the found model into a single ConnectionProviderData object. + +### B. Write Operations (Create / Update) + +Write operations rely on DTOs to ensure the data being inserted or updated is pre-validated and formatted correctly. + +* create(StoreConnectionProviderRequestData $data) + + Flow: Opens a DB transaction. Converts the validated DTO to an array and passes it to the Eloquent create method. + +* update(string $slug, StoreConnectionProviderRequestData $data) + + Flow: Opens a DB transaction. Locates the model by its slug and updates it using the array representation of the provided DTO. + +### C. Delete Operation (Cascade & Logging) + +The delete method contains the most complex flow, handling cascading soft-deletes and custom audit logging. + + delete(string $slug) + + Step 1 (Find): Locates the ConnectionProvider by slug or throws a ModelNotFoundException. + + Step 2 (Fetch Relations): Retrieves the id and name of all associated connectedApps to preserve their state for the audit log. + + Step 3 (Cascade Delete): Performs a mass query deletion ($provider->connectedApps()->delete()). + + Note: Because this is a mass delete, individual model events for ConnectedApp will not fire. + + Step 4 (Provider Delete): Disables default logging on the provider to prevent redundant logs, then deletes the provider. + + Step 5 (Audit Log): Manually triggers a comprehensive Spatie Activity log (provider_cascade_deleted). It records the specific user (:causer.name), the provider details, and an array of the deleted child apps. + +## 3. Performance & Security Best Practices + +When extending this service or building new ones, adhere strictly to the following patterns demonstrated in the code: + +1. Selective Querying: Always use ```$model->select(['id', 'column_name'])``` when returning data to the frontend or + DTOs to avoid memory bloat from fetching unused columns (e.g., timestamps, hidden credentials). + +2. Route/Key Binding vs. Service Lookups: Notice that update, delete, and getProvider accept a primitive string + ```$slug / $id``` rather than an already-resolved Model. This keeps the service decoupled from HTTP routing logic and + allows it to be called safely from CLI commands or background jobs. + +3. Auditable Actions: For destructive actions (like cascades), always disable automatic noise and implement a custom, + highly detailed activity log so system administrators can trace exactly what data was removed and by whom. diff --git a/config/activitylog.php b/config/activitylog.php index 27cd6aa..86316e7 100644 --- a/config/activitylog.php +++ b/config/activitylog.php @@ -1,7 +1,8 @@ id(); $table->string('log_name')->nullable()->index(); $table->text('description');