Search by

a2zwebltd / auditable-relations

dawid-makowski

Automatic auditing for Eloquent relationship changes (attach, detach, sync) using Laravel Auditing

Package info

github.com/a2zwebltd/auditable-relations

pkg:composer/a2zwebltd/auditable-relations

Statistics

Installs: 2 917

Dependents: 1

Suggesters: 0

Stars: 4

Open Issues: 0

v1.2.0 2026-09-24 15:07 UTC

This package is auto-updated.

Last update: 2026-09-24 15:07:52 UTC


README

Automatic auditing for Eloquent relationship changes (attach, detach, sync) using Laravel Auditing.

Features

  • 🔍 Automatic Tracking: Captures before/after state of relationship changes
  • 📝 Detailed Logs: Stores complete related model data, not just IDs
  • 🎯 Event-Based: Uses owen-it/laravel-auditing for consistent audit logs
  • ⚡ Zero Configuration: Works out of the box after trait inclusion
  • 🔧 Flexible: Supports BelongsToMany and MorphToMany relationships
  • 🧩 Custom Pivots: Keeps your using() pivot class and can audit direct pivot saves and deletes

Installation

composer require a2zwebltd/auditable-relations

Requirements

  • PHP 8.2+
  • Laravel 10, 11, 12, or 13
  • owen-it/laravel-auditing 13/14

Quick Start

1. Implement Auditable on Your Model

use Illuminate\Database\Eloquent\Model;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;

class Post extends Model implements Auditable
{
    use AuditableTrait;
}

2. Add the Trait and Wrap Your Relationships

use A2ZWeb\AuditableRelations\Traits\AuditsRelationships;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Post extends Model implements Auditable
{
    use AuditableTrait;
    use AuditsRelationships;

    public function tags(): BelongsToMany
    {
        return $this->auditableRelation(
            $this->belongsToMany(Tag::class)
        );
    }
}

That's it! Now all changes to the tags relationship will be automatically audited.

Pivot settings and constraints can be chained inside or outside the wrap. Both of these keep withPivot(), withTimestamps(), using(), as(), wherePivot() and orderBy():

return $this->auditableRelation(
    $this->belongsToMany(Tag::class)->withPivot('note')->withTimestamps()
);

return $this->auditableRelation($this->belongsToMany(Tag::class))
    ->withPivot('note')
    ->withTimestamps();

Only BelongsToMany and MorphToMany can be wrapped. Any other relation type throws an InvalidArgumentException.

Usage Examples

Basic Usage

$post = Post::find(1);

// These operations are automatically audited
$post->tags()->attach([1, 2, 3]);
$post->tags()->detach([2]);
$post->tags()->sync([1, 3, 4]);
$post->tags()->toggle([1, 5]);     // audited as the detach and attach it runs
$post->tags()->save($tag);         // audited as an attach

sync() writes a synced audit, and also the detached and attached audits for the rows it removes and adds.

What Gets Logged

Each operation creates an audit log entry like:

[
    'event' => 'synced', // or 'attached', 'detached'
    'auditable_type' => 'App\Models\Post',
    'auditable_id' => 1,
    'old_values' => [
        'tags' => [
            ['id' => 1, 'name' => 'Laravel', 'created_at' => '...'],
            ['id' => 2, 'name' => 'PHP', 'created_at' => '...'],
        ]
    ],
    'new_values' => [
        'tags' => [
            ['id' => 1, 'name' => 'Laravel', 'created_at' => '...'],
            ['id' => 3, 'name' => 'Vue', 'created_at' => '...'],
            ['id' => 4, 'name' => 'Tailwind', 'created_at' => '...'],
        ]
    ],
    'user_id' => 123,
    'user_type' => 'App\Models\User',
]

Multiple Relationships

You can audit multiple relationships on the same model:

class Post extends Model implements Auditable
{
    use AuditableTrait;
    use AuditsRelationships;

    public function tags(): BelongsToMany
    {
        return $this->auditableRelation(
            $this->belongsToMany(Tag::class)
        );
    }

    public function categories(): BelongsToMany
    {
        return $this->auditableRelation(
            $this->belongsToMany(Category::class)
        );
    }

    public function attachments(): MorphToMany
    {
        return $this->auditableRelation(
            $this->morphToMany(File::class, 'attachable')
        );
    }
}

Polymorphic Relationships

Works seamlessly with polymorphic relationships:

class Post extends Model implements Auditable
{
    use AuditableTrait;
    use AuditsRelationships;

    public function images(): MorphToMany
    {
        return $this->auditableRelation(
            $this->morphToMany(Image::class, 'imageable')
        );
    }
}

Custom Pivot Models

A pivot class set with ->using() is kept as-is. Relation calls (attach, detach, sync, toggle) are always audited, whatever the pivot class.

To also audit a pivot you save or delete directly (e.g. $post->tags->first()->pivot->delete()), extend the package pivot or use its trait:

use A2ZWeb\AuditableRelations\Pivots\AuditablePivot;

class PostTag extends AuditablePivot
{
    // Use AuditableMorphPivot for morphToMany relations.
}

// Or, when the pivot already extends another class:
use A2ZWeb\AuditableRelations\Pivots\Concerns\HandlesAuditablePivot;
use Illuminate\Database\Eloquent\Relations\Pivot;

class PostTag extends Pivot
{
    use HandlesAuditablePivot;
}

A direct save of a new pivot row writes an attached audit and a direct delete writes a detached audit, each inside a database transaction. Updating an existing pivot row is not audited. The default pivot (no using()) already behaves this way.

Supported Relationships

  • ✅ BelongsToMany
  • ✅ MorphToMany
  • ⏳ Other relationship types (planned)

Configuration

The package respects Laravel Auditing's global configuration:

// config/audit.php
return [
    'enabled' => true,        // Disable to stop all auditing
    'console' => false,       // Audit console commands
    // ... other audit config
];

Advanced Usage

Conditional Auditing

You can control auditing at runtime:

// Temporarily disable auditing
config(['audit.enabled' => false]);
$post->tags()->sync([1, 2, 3]); // Not audited
config(['audit.enabled' => true]);

Custom Event Names

The package uses standard event names:

  • attached - When models are attached
  • detached - When models are detached
  • synced - When models are synced

Accessing Audit Logs

use OwenIt\Auditing\Models\Audit;

// Get all audits for a model
$audits = Audit::where('auditable_type', Post::class)
    ->where('auditable_id', 1)
    ->get();

// Get relationship change audits
$relationshipAudits = Audit::where('auditable_type', Post::class)
    ->whereIn('event', ['attached', 'detached', 'synced'])
    ->get();

How It Works

Architecture

  1. Trait Application: AuditsRelationships trait wraps relationship definitions
  2. Relationship Proxy: Creates auditable versions of BelongsToMany and MorphToMany
  3. Operation Interception: Intercepts attach(), detach(), and sync() calls (and toggle(), save(), create(), which call them)
  4. State Capture: Records relationship state before and after the operation
  5. Event Dispatch: Fires AuditCustom event with the captured data
  6. Audit Creation: Laravel Auditing processes the event and creates the audit log

Performance

  • Every audited operation runs two extra SELECTs: it loads the full related models before and after the change.
  • Each audit stores those full related-model arrays (with pivot columns) in old_values and new_values, so large relations produce large audit rows.
  • sync() also runs the queries and writes the audits for its inner detach() and attach() calls.
  • The audit itself goes through Laravel Auditing, so its queue setting applies.

Comparison with Alternatives

Unlike other solutions:

  • ✅ Complete Data: Stores full related model data, not just IDs
  • ✅ Native Integration: Uses Laravel Auditing's standard audit model
  • ✅ Zero Config: No additional tables or setup required
  • ✅ Framework-Native: Uses Laravel's event system

Troubleshooting

Audits Not Appearing

  1. Verify auditing is enabled:
config('audit.enabled'); // Should be true
  1. Check model implements Auditable:
class Post extends Model implements \OwenIt\Auditing\Contracts\Auditable
{
    use \OwenIt\Auditing\Auditable;
}
  1. Ensure relationship is wrapped:
public function tags(): BelongsToMany
{
    return $this->auditableRelation( // Don't forget this!
        $this->belongsToMany(Tag::class)
    );
}

Console Commands Not Audited

Enable console auditing in config/audit.php:

'console' => true,

Testing

composer test

Relationship audits obey audit.enabled and audit.console. Tests run in the console, so turn on config(['audit.console' => true]) before asserting audits in your app's tests.

AI agents (Laravel Boost)

The package ships a Laravel Boost guideline (resources/boost/guidelines/core.blade.php) that tells coding agents how to wrap relations and test the audits. It needs Boost 2 or newer. In the host app:

composer require laravel/boost --dev
php artisan boost:install          # first time
php artisan boost:update --discover # already using Boost

Select a2zwebltd/auditable-relations when Boost lists the packages.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details.

Security

If you discover a security vulnerability, please email contact@a2zweb.co.

Credits

License

The MIT License (MIT). Please see License File for more information.