zairakai/laravel-activity

Enhanced activity logging helpers for Spatie Laravel Activity Log with pivot table support

Fund package maintenance!
Patreon
Other

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/zairakai/laravel-activity

v3.2.0 2026-02-23 16:16 UTC

README

Main Develop Coverage

GitLab Release Packagist Downloads License

PHP Laravel Spatie Static Analysis Code Style

Zero-friction pivot activity logging for Spatie Laravel Activity Log. Track attach/detach/sync operations on many-to-many relationships with a fluent API and automatic before/after state snapshots.

$user->roles()
    ->activity()
    ->by(auth()->user())
    ->withMessage('Assigned admin roles')
    ->sync([1, 2, 3]);

// Activity logged with complete before/after state

What it does

Extends BelongsToMany with an ->activity() method that wraps every pivot operation and logs the change to Spatie's activity log automatically.

  • attach, detach, sync, syncWithoutDetaching, toggle, updateExistingPivot — all covered
  • Only logs actual changes — no-ops are silently skipped
  • Records a complete before/after diff in the activity properties
  • Fluent causer attribution and custom messages
  • Optional TraceActivities trait to query activities on any model

Requirements

Installation

composer require zairakai/laravel-activity

Spatie's activity log must be installed and migrated first:

composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate

Quick Start

Pivot logging

Call ->activity() on any BelongsToMany relationship:

// Attach
$user->roles()->activity()->attach([1, 2, 3]);

// Detach
$user->roles()->activity()->detach([2]);

// Sync (replaces the full pivot set)
$user->roles()->activity()->sync([1, 5, 7]);

// Sync without detaching (additive only)
$user->roles()->activity()->syncWithoutDetaching([4, 5]);

// Toggle (attach missing, detach existing)
$user->roles()->activity()->toggle([1, 2, 3]);

// Update pivot columns
$user->roles()->activity()->updateExistingPivot($role, ['granted_at' => now()]);

Query activities

Add the TraceActivities trait to any model:

use Zairakai\LaravelActivity\Traits\TraceActivities;

class User extends Model
{
    use TraceActivities;
}

Then query:

// Activities caused BY this user
$user->activities()->by()->get();

// Activities performed ON this user
$user->activities()->on()->get();

// Both directions
$user->activities()->all()->get();

API Reference

Pivot methods

MethodDescription
activity()Enter fluent logging mode on a BelongsToMany
attach($ids, $attrs)Attach IDs not already present
detach($ids)Detach specified IDs (or all if null)
sync($ids)Replace pivot set, logs changes only
syncWithoutDetaching($ids)Additive sync, logs new additions only
toggle($ids)Attach missing, detach existing
updateExistingPivot($id, $attrs)Update extra pivot columns
by($model)Set causer (defaults to Auth::user() if called with no argument)
byAnonymous()Mark as anonymous — no causer
withMessage($text)Override the auto-generated log message

Activity properties

Every logged activity stores these properties:

{
  "operation": "attached|detached|synced|synced_without_detaching|toggled|updated_pivot",
  "relation":  "roles",
  "attributes": [1, 2, 3],
  "old":        [1, 2]
}

For updateExistingPivot, attributes and old contain pivot column maps keyed by model ID:

{
  "operation":  "updated_pivot",
  "relation":   "roles",
  "attributes": { "5": { "granted_at": "2024-01-15" } },
  "old":        { "5": { "granted_at": "2023-06-01" } }
}

TraceActivities query methods

MethodReturns
activities()->by()Activities where this model is the causer
activities()->on()Activities where this model is the subject
activities()->all()Union of both

All return an Eloquent Builder<Activity> — chain any query method on top.

Causer resolution

Priority order (highest first):

  1. ->byAnonymous() — no causer recorded
  2. ->by($model) — explicit model passed as argument
  3. ->by() with no argument — resolves Auth::user()
  4. No by() call at all — falls back to the parent model itself

withMessage() is reset automatically after each logged operation. The causer set via by() persists for the lifetime of the fluent instance.

Advanced usage

Reuse the same instance across operations

$pivot = $user->roles()
    ->activity()
    ->by(auth()->user())
    ->withMessage('Role reorganization');

$pivot->detach([5, 6]);
$pivot->attach([1, 2]);

withMessage applies to the next operation only and is cleared after logging. The causer set via by() persists across all operations on the same instance.

Models and Collections as IDs

$user->roles()->activity()->attach($adminRole);
$user->roles()->activity()->sync(Role::whereIn('id', [1, 2, 3])->get());

Filtering activity queries

$user->activities()
    ->on()
    ->where('properties->operation', 'attached')
    ->where('properties->relation', 'roles')
    ->latest()
    ->paginate(20);

Internal classes

Not part of the public API — documented for contributors:

ClassRole
PivotChangeSetImmutable value object — computes added/removed IDs from before/after arrays
PivotIdNormalizerNormalizes int, string, Model, array, or Collection to array<int\|string>
PivotSyncSummaryFormats a sync() result into a human-readable string

Running tests and quality checks

make quality        # all quality checks (pint + phpstan + rector + insights + markdownlint)
make test           # full test suite
make test-coverage  # tests with coverage report

Or via Composer scripts directly:

composer test          # full test suite
composer test:unit     # unit suite only
composer test:coverage # with coverage report
composer cs            # code style check
composer analyse       # static analysis
composer quality       # all quality checks

Documentation

Getting Help

License Security Policy Issues

Built with ❤️ by the Zairakai team for Laravel developers