# 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 '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 @foreach($activities as $activity)
@if($activity->description === 'created') + @elseif($activity->description === 'deleted') × @else @endif

{{ $activity->causer->name ?? 'System' }} {{ $activity->description }} {{ class_basename($activity->subject_type) }} @if($activity->subject) {{ $activity->subject->title }} @endif

@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 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 '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)