I have'nt found a way to do this with Laravel directly. I've built a solution using Application events and Relation inheritance.
I've added a trait
named App\Database\Eloquent\FollowUpdatedRelations
which have the goal to notify relation updates :
<?php
namespace App\Database\Eloquent;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Library\Decorator;
use App\Events\RelationUpdated;
trait FollowUpdatedRelations
{
/**
* The default error bag.
*
* @var string
*/
protected $updatedRelations = [];
/**
* Check if the belongs to many relation has been updated
* @param BelongsToMany $relation
* @param array $syncResult Result of the `sync` method call
* @return boolean
*/
protected function hasBeenUpdated(BelongsToMany $relation, array $syncResult)
{
if (isset($syncResult['attached']) && count($syncResult['attached']) > 0) {
$this->updatedRelations[$relation->getRelationName()] = true;
event(new RelationUpdated($relation));
} elseif (isset($syncResult['detached']) && count($syncResult['detached']) > 0) {
$this->updatedRelations[$relation->getRelationName()] = true;
event(new RelationUpdated($relation));
}
}
/**
* Decorate a BelongsToMany to listen to relation update
* @param BelongsToMany $relation
* @return Decorator
*/
protected function decorateBelongsToMany(BelongsToMany $relation)
{
$decorator = new Decorator($relation);
$decorator->decorate('sync', function ($decorated, $arguments) {
$updates = call_user_func_array([$decorated, 'sync'], $arguments);
$this->hasBeenUpdated($decorated, $updates);
return $updates;
});
return $decorator;
}
/**
* Retrieve the list of dirty relations
* @return array
*/
public function getDirtyRelations()
{
return $this->updatedRelations;
}
}
I've used this trait in the Model on which I need to follow relation updates and I've updated the relation definition :
<?php
...
class Product extends Model
{
use FollowUpdatedRelations;
....
/**
* Defines relationship with App\Applications model
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function applications()
{
return $this->decorateBelongsToMany(
$this->belongsToMany('App\Application', 'product_application')
);
}
}
The App\Library\Decorator
class wrap an object and add the ability to override methods :
<?php
namespace App\Library;
use Closure;
class Decorator
{
/**
* Decorated instance
* @var mixed
*/
private $decorated;
private $methods = [];
/**
* Decorate given instance
* @param mixed $toDecorate
*/
public function __construct($toDecorate)
{
$this->decorated = $toDecorate;
}
/**
* Decorate a method
* @param string $name
* @param Closure $callback Method to run instead of decorated one
*/
public function decorate($name, Closure $callback)
{
$this->methods[$name] = $callback;
return $this;
}
/**
* Call a method on decorated instance
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
if (isset($this->methods[$name])) {
return call_user_func_array($this->methods[$name], [$this->decorated, $arguments]);
}
return call_user_func_array([$this->decorated, $name], $arguments);
}
}
With that object I can create my custom sync
method on the BelongsToMany
Laravel relation. I use the sync method to follow updates because it returns the list of attached, detached and updated model in the pivot table.
I just need to count if there are attached or detached models and dispatched the corresponding event. My event is App\Events\RelationUpdated
and contains the updated relation as property.
Then I can add an event listener in the EventServiceProvider
like that :
<?php
namespace App\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use App\Events\RelationUpdated;
use App\Product;
class EventServiceProvider extends ServiceProvider
{
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
...
//When a listened relation is updated, we perform a Model save
$events->listen(RelationUpdated::class, function ($event) {
//Here I do my stuff
});
}
}
I can put all the stuff that must be executed when a relation is updated. Seems a bit complicated but I think that relying on something like that is lighter than adding logic on each model construction.
Hope this help !