dibakar/activity-scope

A robust and flexible activity logging system for Laravel Eloquent models with fluent building, auto-actor resolution, and privacy-first features.

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/dibakar/activity-scope

1.0.0 2025-12-25 06:42 UTC

This package is auto-updated.

Last update: 2025-12-25 08:35:42 UTC


README

Latest Version on Packagist Total Downloads License PHP Version Laravel Version

πŸ“‹ Table of Contents

πŸ“– About {#about}

Activity Scope is a high-performance audit trail and activity logging system for Laravel. It allows you to effortlessly track user actions, monitor Eloquent model changes, and maintain a historical record of your application's stateβ€”all while prioritizing privacy and high performance.

πŸ“š Check out the API.md for a full list of available builder methods and scopes.

✨ Features {#features}

  • πŸ”§ Fluent Activity Builder: Expressive, chainable API for logging any event with ease
  • πŸ€– Smarter Auto-Actor: Automatically resolves the current authenticated user as the actor
  • πŸ”’ Privacy First: Built-in IP anonymization and recursive sensitive metadata scrubbing
  • 🧩 Independent Modular Traits:
    • HasActivities: Core relationships and log retrieval
    • LogsActivity: Zero-config auto-logging for Eloquent events
  • πŸ”— Relationship Support: Log activity across model relationships or on multiple models at once
  • πŸ“ Human-Readable Logs: Integrated MessageBuilder to transform raw logs into meaningful alerts
  • πŸ›‘οΈ Security Ready: Dedicated security event logging for audits
  • 🎯 Type Safety: Built with PHP 8.1+ features and strict typing

πŸ“‹ Requirements {#requirements}

  • PHP: >= 8.1
  • Laravel: ^10.0|^11.0|^12.0
  • Illuminate/Database: ^10.0|^11.0|^12.0
  • Illuminate/Support: ^10.0|^11.0|^12.0

πŸš€ Installation {#installation}

You can install the package via composer:

composer require dibakar/activity-scope

Quick Setup

Publish the config and migrations with our handy install command:

php artisan activityscope:install
php artisan migrate

Manual Setup

Alternatively, you can publish them manually via vendor:publish:

# Publish config file
php artisan vendor:publish --tag=activityscope-config

# Publish migration files
php artisan vendor:publish --tag=activityscope-migrations

# Run migrations
php artisan migrate

βš™οΈ Configuration {#configuration}

After publishing the config file, you can customize the behavior in config/activityscope.php:

return [
    // Enable/disable activity logging
    'enabled' => env('ACTIVITY_LOGGER_ENABLED', true),
    
    // Enable automatic model event logging
    'auto_log' => env('ACTIVITY_LOGGER_AUTO_LOG', false),
    'auto_log_events' => ['created', 'updated', 'deleted'],
    
    // Privacy & Security settings
    'privacy' => [
        'sanitize_data' => env('ACTIVITY_SANITIZE_DATA', true),
        'sensitive_fields' => [
            'password', 'password_confirmation', 'secret', 'token', 
            'key', 'card', 'cvv', 'api_key', 'credit_card', 'ssn'
        ],
        'track_ip_address' => env('ACTIVITY_TRACK_IP', true),
        'anonymize_ip' => env('ACTIVITY_ANONYMIZE_IP', false),
    ],
];

πŸ›  Usage {#usage}

Basic Usage

Using the Global Helper

The most common way to log activity is using the activity() helper:

use App\Models\Post;
use App\Models\User;

$user = auth()->user();
$post = Post::find(1);

// Simple activity logging
activity()
    ->on($post)
    ->did('published')
    ->log();

// With additional context
activity()
    ->on($post)
    ->did('updated')
    ->with([
        'title' => $post->title,
        'scheduled_at' => now()->addDays(7),
        'editor' => 'tinymce'
    ])
    ->log();

// Using helper methods for common actions
activity()
    ->created($post) // Equivalent to ->on($post)->did('created')
    ->by($user)
    ->success()
    ->log();

Automatic Actor Resolution

By default, the package automatically attaches auth()->user() to any activity. You can override this manually:

// Manual actor specification
activity()
    ->by($admin)
    ->on($user)
    ->did('suspended')
    ->with(['reason' => 'Policy violation'])
    ->log();

// System-performed activities
activity()
    ->bySystem()
    ->did('backup_completed')
    ->with(['size' => '2.5GB', 'duration' => '45s'])
    ->log();

// Guest user activities
activity()
    ->byGuest()
    ->on($product)
    ->did('viewed')
    ->log();

// Job/Queue context
activity()
    ->byJob('ProcessVideoUpload')
    ->on($video)
    ->did('processed')
    ->log();

Model Integration

Automatic Model Logging

Add the LogsActivity trait to your model for automatic auditing of created, updated, and deleted events:

<?php

namespace App\Models;

use Dibakar\ActivityScope\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use LogsActivity;
    
    protected $fillable = ['title', 'content', 'status'];
}

Manual Model Integration

Use the HasActivities trait to gain access to relationships and a model-contextual activity builder:

<?php

namespace App\Models;

use Dibakar\ActivityScope\Traits\HasActivities;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasActivities;
    
    protected $fillable = ['name', 'email', 'role'];
}

// Log an activity performed BY this user
$user->newActivity()
    ->on($post)
    ->did('reviewed')
    ->with(['rating' => 5, 'comments' => 'Excellent post'])
    ->log();

// Access relationships
$user->actions;    // Activities performed BY this user
$user->activities; // Activities performed ON this user

Advanced Model Configuration

Customize what gets logged and when:

<?php

namespace App\Models;

use Dibakar\ActivityScope\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use LogsActivity;
    
    protected $fillable = ['customer_id', 'total', 'status'];
    
    /**
     * Customize which events to log
     */
    protected function getActivityEvents(): array
    {
        return ['created', 'updated'];
    }
    
    /**
     * Customize what data gets logged
     */
    protected function getActivityData(): array
    {
        return [
            'total' => $this->total,
            'status' => $this->status,
            'customer_name' => $this->customer->name,
        ];
    }
    
    /**
     * Conditionally skip logging
     */
    protected function shouldLogActivity(string $event): bool
    {
        return $this->status !== 'draft';
    }
}

Query Scopes

Filtering Activities

Use the powerful query scopes to retrieve specific activities:

use Dibakar\ActivityScope\Models\Activity;

// Filter by Actor (User, System, etc.)
$activities = Activity::by($user)->get();
$systemActivities = Activity::bySystem()->get();
$guestActivities = Activity::byGuest()->get();

// Filter by Subject (Post, Order, etc.)
$postActivities = Activity::onSubject($post)->get();
// or using the alias
$orderActivities = Activity::forSubject($order)->get();

// Filter by Action
$updateActivities = Activity::did('updated')->get();
$multipleActions = Activity::did(['created', 'updated', 'deleted'])->get();

// Filter by Status
$failedActivities = Activity::failed()->get();
$warningActivities = Activity::warning()->get();

// Filter by Date Range
$recentActivities = Activity::today()->get();
$lastWeekActivities = Activity::lastWeek()->get();
$customRange = Activity::between($startDate, $endDate)->get();

// Combine multiple filters
$userPostUpdates = Activity::by($user)
    ->onSubject($post)
    ->did('updated')
    ->where('status', 'success')
    ->orderBy('created_at', 'desc')
    ->get();

Advanced Querying

// Filter by tags
$securityEvents = Activity::withTag('security')->get();
$authEvents = Activity::withTags(['auth', 'login'])->get();

// Filter by category
$billingActivities = Activity::category('billing')->get();

// Filter by metadata
$highValueOrders = Activity::whereMeta('total', '>', 1000)->get();
$specificUser = Activity::whereMeta('user_agent', 'like', '%Chrome%')->get();

// Complex relationships
$userActionsOnPosts = Activity::by($user)
    ->whereHas('subject', function ($query) {
        $query->where('subject_type', 'App\\Models\\Post');
    })
    ->get();

Advanced Features

Control Flow Logic

Utilize fluent methods to control when and how activities are logged:

activity()
    ->when($user->isAdmin(), function ($builder) {
        $builder->warning('Admin override detected');
    })
    ->unless($post->isPublished(), function ($builder) {
        $builder->info('Draft post accessed');
    })
    ->tap(function ($builder) use ($request) {
        // Access builder for additional setup
        $builder->with(['request_id' => $request->id]);
    })
    ->silent() // Prevent saving to DB, useful for conditional dry-runs
    ->log();

// Conditional logging
$shouldLog = $user->hasPermission('audit-access');
activity()
    ->when($shouldLog, fn($b) => $b->did('accessed_sensitive_data'))
    ->when(!$shouldLog, fn($b) => $b->silent())
    ->on($report)
    ->log();

Security & Privacy

Built-in tools to handle sensitive data and privacy requirements:

activity()
    ->private() // Marks activity as private
    ->sensitive() // Flags as containing sensitive info
    ->ip('1.2.3.4') // Manually set IP (auto-handled usually)
    ->with(['api_key' => 'sk_live_123456']) // Automatically redacted to [REDACTED]
    ->log();

// Privacy-first logging with automatic data scrubbing
activity()
    ->by($user)
    ->on($payment)
    ->did('processed')
    ->with([
        'card_number' => '4242-4242-4242-4242', // Auto-redacted
        'cvv' => '123', // Auto-redacted
        'amount' => 99.99, // Safe to log
        'currency' => 'USD', // Safe to log
    ])
    ->sensitive()
    ->log();

Status & Severity

Categorize activities by outcome and importance:

// Success activities (default)
activity()
    ->success()
    ->on($order)
    ->did('shipped')
    ->log();

// Failed activities with reasons
activity()
    ->failed('Invalid OTP') // Status: failed, Reason: Invalid OTP
    ->on($user)
    ->did('login_attempt')
    ->log();

// Warning levels
activity()
    ->warning('Rate limited')
    ->by($ip)
    ->did('api_request')
    ->log();

// Custom severity levels
activity()
    ->severity('critical') // Custom severity level
    ->bySystem()
    ->did('database_backup_failed')
    ->log();

// Info levels
activity()
    ->info('User session extended')
    ->by($user)
    ->log();

Metadata & Context

Rich context management for detailed audit trails:

activity()
    ->with(['browser' => 'Chrome'])     // Merge metadata
    ->context(['os' => 'Linux'])        // Alias for with()
    ->changes(['role' => 'editor'])     // Track changes
    ->tags(['auth', 'security'])        // Tagging support
    ->category('access_control')
    ->correlationId('req_123456')       // Cross-service tracking
    ->requestId('req_789012')           // Request identification
    ->externalRef('stripe_ch_123')      // External reference
    ->log();

// Tracking before/after states
activity()
    ->on($user)
    ->did('profile_updated')
    ->oldNew($oldAttributes, $newAttributes)
    ->log();

// Using changes helper
activity()
    ->on($order)
    ->did('status_changed')
    ->changes([
        'old_status' => 'pending',
        'new_status' => 'confirmed',
        'changed_by' => auth()->user()->id,
    ])
    ->log();

Events & Hooks

Human-Readable Messages

Transform any activity record into a readable string using the integrated MessageBuilder:

$activity = Activity::first();
echo $activity->message(); // "System published Post 'Laravel Rocks'"

// Custom message formatting
echo $activity->getMessageBuilder()
    ->actorFirst()
    ->includeTime()
    ->build();
// "John Doe published Post 'Laravel Rocks' 2 minutes ago"

Event System

Listen to activity events to trigger additional actions:

use Dibakar\ActivityScope\Events\ActivityLogged;

// In your EventServiceProvider
protected $listen = [
    ActivityLogged::class => [
        SendActivityNotification::class,
        LogToExternalService::class,
        UpdateAnalytics::class,
    ],
];

// Custom listener example
class SendActivityNotification
{
    public function handle(ActivityLogged $event)
    {
        $activity = $event->activity;
        
        if ($activity->isCritical()) {
            // Send email notification
            Mail::to('admin@example.com')
                ->send(new CriticalActivityAlert($activity));
        }
        
        if ($activity->hasTag('security')) {
            // Send to security team
            $this->securityService->alert($activity);
        }
    }
}

Custom Event Handlers

// In your service provider
Event::listen(ActivityLogged::class, function ($event) {
    $activity = $event->activity;
    
    // Log to external monitoring
    if ($activity->status === 'failed') {
        Sentry::captureMessage($activity->message());
    }
    
    // Update user metrics
    if ($activity->subject_type === 'App\Models\User') {
        Cache::increment("user_activity_{$activity->subject_id}");
    }
});

πŸ“š Examples {#examples}

Check out the examples/ directory for comprehensive usage examples:

Quick Example

<?php

// Complete e-commerce order workflow
class OrderService
{
    public function processOrder(Order $order, User $user)
    {
        // Log order creation
        activity()
            ->by($user)
            ->created($order)
            ->with(['total' => $order->total, 'items' => $order->items->count()])
            ->tags(['order', 'ecommerce'])
            ->log();
        
        try {
            // Process payment
            $payment = $this->paymentService->charge($order);
            
            activity()
                ->by($user)
                ->on($payment)
                ->did('processed')
                ->success()
                ->with(['amount' => $payment->amount, 'gateway' => 'stripe'])
                ->log();
            
            // Update order status
            $order->update(['status' => 'confirmed']);
            
            activity()
                ->by($user)
                ->on($order)
                ->did('confirmed')
                ->changes(['status' => 'confirmed'])
                ->success()
                ->log();
                
        } catch (PaymentException $e) {
            activity()
                ->by($user)
                ->on($order)
                ->failed($e->getMessage())
                ->severity('critical')
                ->tags(['payment', 'error'])
                ->log();
            
            throw $e;
        }
    }
}

πŸ§ͺ Testing {#testing}

Basic Assertions

You can use the built-in test helper to assert activities were logged:

use Dibakar\ActivityScope\Models\Activity;

// Basic database assertions
$this->assertDatabaseHas('activities', [
    'action' => 'shipped',
    'subject_id' => $order->id,
    'subject_type' => 'App\\Models\\Order',
]);

// Count assertions
$this->assertEquals(3, Activity::count());
$this->assertEquals(1, Activity::by($user)->count());

// Model relationship assertions
$this->assertTrue($user->actions->contains('action', 'created'));
$this->assertTrue($order->activities->contains('action', 'updated'));

Advanced Testing

public function test_activity_logging_flow()
{
    $user = User::factory()->create();
    $post = Post::factory()->create();
    
    // Perform action that should log activity
    $this->actingAs($user)
        ->post("/posts/{$post->id}/publish");
    
    // Assert activity was logged correctly
    $activity = Activity::where('action', 'published')
        ->by($user)
        ->onSubject($post)
        ->first();
    
    $this->assertNotNull($activity);
    $this->assertEquals('success', $activity->status);
    $this->assertArrayHasKey('published_at', $activity->meta);
    
    // Test message generation
    $expectedMessage = "{$user->name} published Post '{$post->title}'";
    $this->assertEquals($expectedMessage, $activity->message());
}

Test Helpers

// Custom test helper methods
trait ActivityTestHelpers
{
    protected function assertActivityLogged($action, $subject = null, $actor = null)
    {
        $query = Activity::where('action', $action);
        
        if ($subject) {
            $query->onSubject($subject);
        }
        
        if ($actor) {
            $query->by($actor);
        }
        
        $this->assertTrue(
            $query->exists(),
            "Activity '{$action}' was not logged"
        );
    }
    
    protected function assertNoActivityLogged($action, $subject = null)
    {
        $query = Activity::where('action', $action);
        
        if ($subject) {
            $query->onSubject($subject);
        }
        
        $this->assertFalse(
            $query->exists(),
            "Activity '{$action}' was unexpectedly logged"
        );
    }
}

Run package tests with:

composer test

For code analysis:

composer analyse

πŸ“– API Reference {#api-reference}

For a comprehensive list of all available methods, scopes, and advanced features, check out our detailed API Documentation.

Quick Reference

// Global helper
activity()                    // Get ActivityBuilder instance

// Actor methods
->by($user)                   // Set actor
->bySystem()                  // System actor
->byGuest()                   // Guest actor
->byJob('job-name')           // Job context

// Subject methods
->on($model)                  // Set subject
->for($model)                 // Alias for on()
->onMany([$model1, $model2]) // Multiple subjects

// Action methods
->did('action')               // Set action
->created($model)             // Created action shortcut
->updated($model)             // Updated action shortcut
->deleted($model)             // Deleted action shortcut

// Metadata methods
->with($data)                 // Add metadata
->changes($data)              // Track changes
->tags(['tag1', 'tag2'])     // Add tags
->severity('high')            // Set severity

// Status methods
->success()                   // Success status
->failed('reason')            // Failed status with reason
->warning('message')          // Warning status
->info('message')             // Info status

// Execution
->log()                       // Save activity
->silent()                    // Don't save (dry run)

🀝 Contributing {#contributing}

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup

# Clone the repository
git clone https://github.com/dibakarmitra/activity-scope.git
cd activity-scope

# Install dependencies
composer install

# Run tests
composer test

# Run static analysis
composer analyse

Guidelines

  • Follow PSR-12 coding standards
  • Add tests for new features
  • Update documentation for any API changes
  • Ensure all tests pass before submitting

πŸ“ Changelog {#changelog}

Please see CHANGELOG.md for more information on what has changed recently.

πŸ“„ License {#license}

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™‹β€β™‚οΈ Support {#support}

Made by Dibakar Mitra