a2zwebltd / auditable-relations
Automatic auditing for Eloquent relationship changes (attach, detach, sync) using Laravel Auditing
Requires
- php: ^8.2
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- owen-it/laravel-auditing: ^13.0|^14.0
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^2.0|^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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-auditingfor 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 attacheddetached- When models are detachedsynced- 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
- Trait Application:
AuditsRelationshipstrait wraps relationship definitions - Relationship Proxy: Creates auditable versions of
BelongsToManyandMorphToMany - Operation Interception: Intercepts
attach(),detach(), andsync()calls (andtoggle(),save(),create(), which call them) - State Capture: Records relationship state before and after the operation
- Event Dispatch: Fires
AuditCustomevent with the captured data - 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_valuesandnew_values, so large relations produce large audit rows. sync()also runs the queries and writes the audits for its innerdetach()andattach()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
- Verify auditing is enabled:
config('audit.enabled'); // Should be true
- Check model implements
Auditable:
class Post extends Model implements \OwenIt\Auditing\Contracts\Auditable { use \OwenIt\Auditing\Auditable; }
- 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.