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.
This commit is contained in:
parent
0a2786165e
commit
0de768e924
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, SoftDeletes};
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, SoftDeletes};
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'name',
|
'name',
|
||||||
@ -17,7 +19,7 @@
|
|||||||
])]
|
])]
|
||||||
class ConnectedApp extends Model
|
class ConnectedApp extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use LogsActivity, SoftDeletes;
|
||||||
|
|
||||||
public function protocol(): HasOne
|
public function protocol(): HasOne
|
||||||
{
|
{
|
||||||
@ -33,4 +35,16 @@ public function status(): HasOne
|
|||||||
{
|
{
|
||||||
return $this->hasOne(ConnectionStatus::class, 'id', 'connection_status_id');
|
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'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,30 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\{Model, Relations\HasMany, SoftDeletes};
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
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'])]
|
#[Fillable(['name', 'slug'])]
|
||||||
final class ConnectionProvider extends Model
|
final class ConnectionProvider extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use LogsActivity, SoftDeletes;
|
||||||
|
|
||||||
public function connectedApps(): HasMany
|
public function connectedApps(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ConnectedApp::class);
|
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'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,9 +39,10 @@ public function update(int $id, ConnectAppRequest $data): void
|
|||||||
throw new InvalidArgumentException('Invalid id');
|
throw new InvalidArgumentException('Invalid id');
|
||||||
}
|
}
|
||||||
DB::transaction(
|
DB::transaction(
|
||||||
fn () => ConnectedApp::query()
|
function () use ($id, $data): void {
|
||||||
->where('id', $id)
|
$app = ConnectedApp::query()->findOrFail($id);
|
||||||
->update($data->toArray())
|
$app->update($data->toArray());
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,9 +57,10 @@ public function delete(int $id): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(
|
DB::transaction(
|
||||||
fn () => ConnectedApp::query()
|
function () use ($id): void {
|
||||||
->where('id', $id)
|
$app = ConnectedApp::query()->findOrFail($id);
|
||||||
->delete()
|
$app->delete();
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,14 +58,18 @@ public function getAll(): PaginatedDataCollection
|
|||||||
public function update(string $slug, StoreConnectionProviderRequestData $data): void
|
public function update(string $slug, StoreConnectionProviderRequestData $data): void
|
||||||
{
|
{
|
||||||
DB::transaction(
|
DB::transaction(
|
||||||
fn() => ConnectionProvider::query()
|
function () use ($slug, $data): void {
|
||||||
->where('slug', $slug)
|
$app = ConnectionProvider::query()
|
||||||
->update($data->toArray())
|
->where('slug', $slug);
|
||||||
|
|
||||||
|
$app->update($data->toArray());
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the connection provider and associated connected apps.
|
* Delete the connection provider and associated connected apps.
|
||||||
|
*
|
||||||
* @throws ModelNotFoundException when the connection provider is not found.
|
* @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.
|
||||||
*/
|
*/
|
||||||
@ -76,12 +80,27 @@ function () use ($slug): void {
|
|||||||
$provider = ConnectionProvider::query()
|
$provider = ConnectionProvider::query()
|
||||||
->where('slug', $slug)
|
->where('slug', $slug)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
$deletedApps = $provider->connectedApps()
|
||||||
|
->select(['id', 'name'])
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
// Note: This performs a mass soft-delete query.
|
// Note: This performs a mass soft-delete query.
|
||||||
// Model events for ConnectedApp will NOT fire.
|
// Model events for ConnectedApp will NOT fire.
|
||||||
$provider->connectedApps()->delete();
|
$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.');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
98
app/Services/README.md
Normal file
98
app/Services/README.md
Normal file
@ -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.
|
||||||
@ -1,7 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Spatie\Activitylog\Actions\CleanActivityLogAction;
|
declare(strict_types=1);
|
||||||
use Spatie\Activitylog\Actions\LogActivityAction;
|
|
||||||
|
use Spatie\Activitylog\Actions\{CleanActivityLogAction, LogActivityAction};
|
||||||
use Spatie\Activitylog\Models\Activity;
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class() extends Migration
|
||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('activity_log', function (Blueprint $table) {
|
Schema::create('activity_log', function (Blueprint $table): void {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('log_name')->nullable()->index();
|
$table->string('log_name')->nullable()->index();
|
||||||
$table->text('description');
|
$table->text('description');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user