679 lines
14 KiB
Markdown
679 lines
14 KiB
Markdown
|
|
# Activity Logging
|
|||
|
|
|
|||
|
|
Core PHP Framework provides comprehensive activity logging to track changes to your models and user actions. Built on Spatie's `laravel-activitylog`, it adds workspace-scoped logging and automatic cleanup.
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
Activity logging helps you:
|
|||
|
|
|
|||
|
|
- Track who changed what and when
|
|||
|
|
- Maintain audit trails for compliance
|
|||
|
|
- Debug issues by reviewing historical changes
|
|||
|
|
- Display activity feeds to users
|
|||
|
|
- Revert changes when needed
|
|||
|
|
|
|||
|
|
## Setup
|
|||
|
|
|
|||
|
|
### Installation
|
|||
|
|
|
|||
|
|
The activity log package is included in Core PHP:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
composer require spatie/laravel-activitylog
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Migration
|
|||
|
|
|
|||
|
|
Run migrations to create the `activity_log` table:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
php artisan migrate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Configuration
|
|||
|
|
|
|||
|
|
Publish and customize the configuration:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
php artisan vendor:publish --tag=activitylog
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Core PHP extends the default configuration:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// config/core.php
|
|||
|
|
'activity' => [
|
|||
|
|
'enabled' => env('ACTIVITY_LOG_ENABLED', true),
|
|||
|
|
'retention_days' => env('ACTIVITY_RETENTION_DAYS', 90),
|
|||
|
|
'cleanup_enabled' => true,
|
|||
|
|
'log_ip_address' => false, // GDPR compliance
|
|||
|
|
],
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Basic Usage
|
|||
|
|
|
|||
|
|
### Adding Logging to Models
|
|||
|
|
|
|||
|
|
Use the `LogsActivity` trait:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace Mod\Blog\Models;
|
|||
|
|
|
|||
|
|
use Illuminate\Database\Eloquent\Model;
|
|||
|
|
use Core\Activity\Concerns\LogsActivity;
|
|||
|
|
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
protected $fillable = ['title', 'content', 'published_at'];
|
|||
|
|
|
|||
|
|
// Specify which attributes to log
|
|||
|
|
protected array $activityLogAttributes = ['title', 'content', 'published_at'];
|
|||
|
|
|
|||
|
|
// Optionally, log all fillable attributes
|
|||
|
|
// protected static $logFillable = true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Automatic Logging
|
|||
|
|
|
|||
|
|
Changes are logged automatically:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$post = Post::create([
|
|||
|
|
'title' => 'My First Post',
|
|||
|
|
'content' => 'Hello world!',
|
|||
|
|
]);
|
|||
|
|
// Activity logged: "created" event
|
|||
|
|
|
|||
|
|
$post->update(['title' => 'Updated Title']);
|
|||
|
|
// Activity logged: "updated" event with changes
|
|||
|
|
|
|||
|
|
$post->delete();
|
|||
|
|
// Activity logged: "deleted" event
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Manual Logging
|
|||
|
|
|
|||
|
|
Log custom activities:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
activity()
|
|||
|
|
->performedOn($post)
|
|||
|
|
->causedBy(auth()->user())
|
|||
|
|
->withProperties(['custom' => 'data'])
|
|||
|
|
->log('published');
|
|||
|
|
|
|||
|
|
// Or use the helper on the model
|
|||
|
|
$post->logActivity('published', ['published_at' => now()]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Configuration Options
|
|||
|
|
|
|||
|
|
### Log Attributes
|
|||
|
|
|
|||
|
|
Specify which attributes to track:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
// Log specific attributes
|
|||
|
|
protected array $activityLogAttributes = ['title', 'content', 'status'];
|
|||
|
|
|
|||
|
|
// Log all fillable attributes
|
|||
|
|
protected static $logFillable = true;
|
|||
|
|
|
|||
|
|
// Log all attributes
|
|||
|
|
protected static $logAttributes = ['*'];
|
|||
|
|
|
|||
|
|
// Log only dirty (changed) attributes
|
|||
|
|
protected static $logOnlyDirty = true;
|
|||
|
|
|
|||
|
|
// Don't log these attributes
|
|||
|
|
protected static $logAttributesToIgnore = ['updated_at', 'view_count'];
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Log Events
|
|||
|
|
|
|||
|
|
Control which events trigger logging:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
// Log only these events (default: all)
|
|||
|
|
protected static $recordEvents = ['created', 'updated', 'deleted'];
|
|||
|
|
|
|||
|
|
// Don't log these events
|
|||
|
|
protected static $ignoreEvents = ['retrieved'];
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Custom Log Names
|
|||
|
|
|
|||
|
|
Organize activities by type:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
public function getActivitylogOptions(): LogOptions
|
|||
|
|
{
|
|||
|
|
return LogOptions::defaults()
|
|||
|
|
->logOnly(['title', 'content'])
|
|||
|
|
->logOnlyDirty()
|
|||
|
|
->setDescriptionForEvent(fn(string $eventName) => "Post {$eventName}")
|
|||
|
|
->useLogName('blog');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Retrieving Activity
|
|||
|
|
|
|||
|
|
### Get All Activity
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// All activity in the system
|
|||
|
|
$activities = Activity::all();
|
|||
|
|
|
|||
|
|
// Recent activity
|
|||
|
|
$recent = Activity::latest()->limit(10)->get();
|
|||
|
|
|
|||
|
|
// Activity for specific model
|
|||
|
|
$postActivity = Activity::forSubject($post)->get();
|
|||
|
|
|
|||
|
|
// Activity by specific user
|
|||
|
|
$userActivity = Activity::causedBy($user)->get();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Filtering Activity
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// By log name
|
|||
|
|
$blogActivity = Activity::inLog('blog')->get();
|
|||
|
|
|
|||
|
|
// By description
|
|||
|
|
$publishedPosts = Activity::where('description', 'published')->get();
|
|||
|
|
|
|||
|
|
// By date range
|
|||
|
|
$recentActivity = Activity::whereBetween('created_at', [
|
|||
|
|
now()->subDays(7),
|
|||
|
|
now(),
|
|||
|
|
])->get();
|
|||
|
|
|
|||
|
|
// By properties
|
|||
|
|
$activity = Activity::whereJsonContains('properties->status', 'published')->get();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Activity Scopes
|
|||
|
|
|
|||
|
|
Core PHP adds workspace scoping:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
use Core\Activity\Scopes\ActivityScopes;
|
|||
|
|
|
|||
|
|
// Activity for current workspace
|
|||
|
|
$workspaceActivity = Activity::forCurrentWorkspace()->get();
|
|||
|
|
|
|||
|
|
// Activity for specific workspace
|
|||
|
|
$activity = Activity::forWorkspace($workspace)->get();
|
|||
|
|
|
|||
|
|
// Activity for specific subject type
|
|||
|
|
$postActivity = Activity::forSubjectType(Post::class)->get();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Activity Properties
|
|||
|
|
|
|||
|
|
### Storing Extra Data
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
activity()
|
|||
|
|
->performedOn($post)
|
|||
|
|
->withProperties([
|
|||
|
|
'old_status' => 'draft',
|
|||
|
|
'new_status' => 'published',
|
|||
|
|
'scheduled_at' => $post->published_at,
|
|||
|
|
'notified_subscribers' => true,
|
|||
|
|
])
|
|||
|
|
->log('published');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Retrieving Properties
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$activity = Activity::latest()->first();
|
|||
|
|
|
|||
|
|
$properties = $activity->properties;
|
|||
|
|
$oldStatus = $activity->properties['old_status'] ?? null;
|
|||
|
|
|
|||
|
|
// Access as object
|
|||
|
|
$newStatus = $activity->properties->new_status;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Changes Tracking
|
|||
|
|
|
|||
|
|
View before/after values:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$post->update(['title' => 'New Title']);
|
|||
|
|
|
|||
|
|
$activity = Activity::forSubject($post)->latest()->first();
|
|||
|
|
|
|||
|
|
$changes = $activity->changes();
|
|||
|
|
// [
|
|||
|
|
// 'attributes' => ['title' => 'New Title'],
|
|||
|
|
// 'old' => ['title' => 'Old Title']
|
|||
|
|
// ]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Activity Presentation
|
|||
|
|
|
|||
|
|
### Display Activity Feed
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// Controller
|
|||
|
|
public function activityFeed()
|
|||
|
|
{
|
|||
|
|
$activities = Activity::with(['causer', 'subject'])
|
|||
|
|
->forCurrentWorkspace()
|
|||
|
|
->latest()
|
|||
|
|
->paginate(20);
|
|||
|
|
|
|||
|
|
return view('activity-feed', compact('activities'));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```blade
|
|||
|
|
<!-- View -->
|
|||
|
|
@foreach($activities as $activity)
|
|||
|
|
<div class="activity-item">
|
|||
|
|
<div class="activity-icon">
|
|||
|
|
@if($activity->description === 'created')
|
|||
|
|
<span class="text-green-500">+</span>
|
|||
|
|
@elseif($activity->description === 'deleted')
|
|||
|
|
<span class="text-red-500">×</span>
|
|||
|
|
@else
|
|||
|
|
<span class="text-blue-500">•</span>
|
|||
|
|
@endif
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="activity-content">
|
|||
|
|
<p>
|
|||
|
|
<strong>{{ $activity->causer->name ?? 'System' }}</strong>
|
|||
|
|
{{ $activity->description }}
|
|||
|
|
<em>{{ class_basename($activity->subject_type) }}</em>
|
|||
|
|
@if($activity->subject)
|
|||
|
|
<a href="{{ route('posts.show', $activity->subject) }}">
|
|||
|
|
{{ $activity->subject->title }}
|
|||
|
|
</a>
|
|||
|
|
@endif
|
|||
|
|
</p>
|
|||
|
|
<time>{{ $activity->created_at->diffForHumans() }}</time>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
@endforeach
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Custom Descriptions
|
|||
|
|
|
|||
|
|
Make descriptions more readable:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
public function getActivitylogOptions(): LogOptions
|
|||
|
|
{
|
|||
|
|
return LogOptions::defaults()
|
|||
|
|
->setDescriptionForEvent(function(string $eventName) {
|
|||
|
|
return match($eventName) {
|
|||
|
|
'created' => 'created post "' . $this->title . '"',
|
|||
|
|
'updated' => 'updated post "' . $this->title . '"',
|
|||
|
|
'deleted' => 'deleted post "' . $this->title . '"',
|
|||
|
|
'published' => 'published post "' . $this->title . '"',
|
|||
|
|
default => $eventName . ' post',
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Workspace Isolation
|
|||
|
|
|
|||
|
|
### Automatic Scoping
|
|||
|
|
|
|||
|
|
Activity is automatically scoped to workspaces:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// Only returns activity for current workspace
|
|||
|
|
$activity = Activity::forCurrentWorkspace()->get();
|
|||
|
|
|
|||
|
|
// Explicitly query another workspace (admin only)
|
|||
|
|
if (auth()->user()->isSuperAdmin()) {
|
|||
|
|
$activity = Activity::forWorkspace($otherWorkspace)->get();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Cross-Workspace Activity
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// Admin reports across all workspaces
|
|||
|
|
$systemActivity = Activity::withoutGlobalScopes()->get();
|
|||
|
|
|
|||
|
|
// Activity counts by workspace
|
|||
|
|
$stats = Activity::withoutGlobalScopes()
|
|||
|
|
->select('workspace_id', DB::raw('count(*) as count'))
|
|||
|
|
->groupBy('workspace_id')
|
|||
|
|
->get();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Activity Cleanup
|
|||
|
|
|
|||
|
|
### Automatic Pruning
|
|||
|
|
|
|||
|
|
Configure automatic cleanup of old activity:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// config/core.php
|
|||
|
|
'activity' => [
|
|||
|
|
'retention_days' => 90,
|
|||
|
|
'cleanup_enabled' => true,
|
|||
|
|
],
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Schedule the cleanup command:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// app/Console/Kernel.php
|
|||
|
|
protected function schedule(Schedule $schedule)
|
|||
|
|
{
|
|||
|
|
$schedule->command('activity:prune')
|
|||
|
|
->daily()
|
|||
|
|
->at('02:00');
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Manual Pruning
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Delete activity older than configured retention period
|
|||
|
|
php artisan activity:prune
|
|||
|
|
|
|||
|
|
# Delete activity older than specific number of days
|
|||
|
|
php artisan activity:prune --days=30
|
|||
|
|
|
|||
|
|
# Dry run (see what would be deleted)
|
|||
|
|
php artisan activity:prune --dry-run
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Selective Deletion
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// Delete activity for specific model
|
|||
|
|
Activity::forSubject($post)->delete();
|
|||
|
|
|
|||
|
|
// Delete activity by log name
|
|||
|
|
Activity::inLog('temporary')->delete();
|
|||
|
|
|
|||
|
|
// Delete activity older than date
|
|||
|
|
Activity::where('created_at', '<', now()->subMonths(6))->delete();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Advanced Usage
|
|||
|
|
|
|||
|
|
### Batch Logging
|
|||
|
|
|
|||
|
|
Log multiple changes as a single activity:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
activity()->enableLogging();
|
|||
|
|
|
|||
|
|
// Disable automatic logging temporarily
|
|||
|
|
activity()->disableLogging();
|
|||
|
|
|
|||
|
|
Post::create([/*...*/]); // Not logged
|
|||
|
|
Post::create([/*...*/]); // Not logged
|
|||
|
|
Post::create([/*...*/]); // Not logged
|
|||
|
|
|
|||
|
|
// Re-enable and log batch operation
|
|||
|
|
activity()->enableLogging();
|
|||
|
|
|
|||
|
|
activity()
|
|||
|
|
->performedOn($workspace)
|
|||
|
|
->log('imported 100 posts');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Custom Activity Models
|
|||
|
|
|
|||
|
|
Extend the activity model:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Models;
|
|||
|
|
|
|||
|
|
use Spatie\Activitylog\Models\Activity as BaseActivity;
|
|||
|
|
|
|||
|
|
class Activity extends BaseActivity
|
|||
|
|
{
|
|||
|
|
public function scopePublic($query)
|
|||
|
|
{
|
|||
|
|
return $query->where('properties->public', true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function wasSuccessful(): bool
|
|||
|
|
{
|
|||
|
|
return $this->properties['success'] ?? true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Update config:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// config/activitylog.php
|
|||
|
|
'activity_model' => App\Models\Activity::class,
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Queued Logging
|
|||
|
|
|
|||
|
|
Log activity in the background for performance:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// In a job or listener
|
|||
|
|
dispatch(function () use ($post, $user) {
|
|||
|
|
activity()
|
|||
|
|
->performedOn($post)
|
|||
|
|
->causedBy($user)
|
|||
|
|
->log('processed');
|
|||
|
|
})->afterResponse();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## GDPR Compliance
|
|||
|
|
|
|||
|
|
### Anonymize User Data
|
|||
|
|
|
|||
|
|
Don't log personally identifiable information:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// config/core.php
|
|||
|
|
'activity' => [
|
|||
|
|
'log_ip_address' => false,
|
|||
|
|
'anonymize_after_days' => 30,
|
|||
|
|
],
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Anonymization
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class AnonymizeOldActivity
|
|||
|
|
{
|
|||
|
|
public function handle(): void
|
|||
|
|
{
|
|||
|
|
Activity::where('created_at', '<', now()->subDays(30))
|
|||
|
|
->whereNotNull('causer_id')
|
|||
|
|
->update([
|
|||
|
|
'causer_id' => null,
|
|||
|
|
'causer_type' => null,
|
|||
|
|
'properties->ip_address' => null,
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### User Data Deletion
|
|||
|
|
|
|||
|
|
Delete user's activity when account is deleted:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class User extends Model
|
|||
|
|
{
|
|||
|
|
protected static function booted()
|
|||
|
|
{
|
|||
|
|
static::deleting(function ($user) {
|
|||
|
|
// Delete or anonymize activity
|
|||
|
|
Activity::causedBy($user)->delete();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Performance Optimization
|
|||
|
|
|
|||
|
|
### Eager Loading
|
|||
|
|
|
|||
|
|
Prevent N+1 queries:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$activities = Activity::with(['causer', 'subject'])
|
|||
|
|
->latest()
|
|||
|
|
->paginate(20);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Selective Logging
|
|||
|
|
|
|||
|
|
Only log important changes:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class Post extends Model
|
|||
|
|
{
|
|||
|
|
use LogsActivity;
|
|||
|
|
|
|||
|
|
// Only log changes to these critical fields
|
|||
|
|
protected array $activityLogAttributes = ['title', 'published_at', 'status'];
|
|||
|
|
|
|||
|
|
// Only log when attributes actually change
|
|||
|
|
protected static $logOnlyDirty = true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Disable Logging Temporarily
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// Disable for bulk operations
|
|||
|
|
activity()->disableLogging();
|
|||
|
|
|
|||
|
|
Post::query()->update(['migrated' => true]);
|
|||
|
|
|
|||
|
|
activity()->enableLogging();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Testing
|
|||
|
|
|
|||
|
|
### Testing Activity Logging
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace Tests\Feature;
|
|||
|
|
|
|||
|
|
use Tests\TestCase;
|
|||
|
|
use Mod\Blog\Models\Post;
|
|||
|
|
use Spatie\Activitylog\Models\Activity;
|
|||
|
|
|
|||
|
|
class PostActivityTest extends TestCase
|
|||
|
|
{
|
|||
|
|
public function test_logs_post_creation(): void
|
|||
|
|
{
|
|||
|
|
$post = Post::create([
|
|||
|
|
'title' => 'Test Post',
|
|||
|
|
'content' => 'Test content',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$activity = Activity::forSubject($post)->first();
|
|||
|
|
|
|||
|
|
$this->assertEquals('created', $activity->description);
|
|||
|
|
$this->assertEquals(auth()->id(), $activity->causer_id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function test_logs_attribute_changes(): void
|
|||
|
|
{
|
|||
|
|
$post = Post::factory()->create(['title' => 'Original']);
|
|||
|
|
|
|||
|
|
$post->update(['title' => 'Updated']);
|
|||
|
|
|
|||
|
|
$activity = Activity::forSubject($post)->latest()->first();
|
|||
|
|
|
|||
|
|
$this->assertEquals('updated', $activity->description);
|
|||
|
|
$this->assertEquals('Original', $activity->changes()['old']['title']);
|
|||
|
|
$this->assertEquals('Updated', $activity->changes()['attributes']['title']);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Best Practices
|
|||
|
|
|
|||
|
|
### 1. Log Business Events
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// ✅ Good - meaningful business events
|
|||
|
|
$post->logActivity('published', ['published_at' => now()]);
|
|||
|
|
$post->logActivity('featured', ['featured_until' => $date]);
|
|||
|
|
|
|||
|
|
// ❌ Bad - technical implementation details
|
|||
|
|
$post->logActivity('database_updated');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Include Context
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// ✅ Good - rich context
|
|||
|
|
activity()
|
|||
|
|
->performedOn($post)
|
|||
|
|
->withProperties([
|
|||
|
|
'published_at' => $post->published_at,
|
|||
|
|
'notification_sent' => true,
|
|||
|
|
'subscribers_count' => $subscribersCount,
|
|||
|
|
])
|
|||
|
|
->log('published');
|
|||
|
|
|
|||
|
|
// ❌ Bad - minimal context
|
|||
|
|
activity()->performedOn($post)->log('published');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Use Descriptive Log Names
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// ✅ Good - organized by domain
|
|||
|
|
activity()->useLog('blog')->log('post published');
|
|||
|
|
activity()->useLog('commerce')->log('order placed');
|
|||
|
|
|
|||
|
|
// ❌ Bad - generic log name
|
|||
|
|
activity()->useLog('default')->log('thing happened');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Learn More
|
|||
|
|
|
|||
|
|
- [Activity Feed UI](/packages/admin#activity-feed)
|
|||
|
|
- [GDPR Compliance](/security/gdpr)
|
|||
|
|
- [Testing Activity](/testing/activity-logging)
|