Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trouble with multiple model observers in Laravel

I'm stuck on a weird issue. It feels like in Laravel, you're not allowed to have multiple model observers listening to the same event. In my case:

Parent Model

class MyParent extends Eloquent {
   private static function boot()
   {
      parent::boot();
      $called_class = get_called_class();
      $called_class::creating(function($model) {
         doSomethingInParent();
         return true;
      }
   }
}

Child Model

class MyChild extends myParent {
   private static function boot()
   {
      parent::boot();
      MyChild::creating(function($model) {
         doSomethingInChild();
         return true;
      }
   }
}

In the above example, if I do:

$instance = MyChild::create();

... the line doSomethingInChild() will not fire. doSomethingInParent(), does.

If I move parent::boot() within the child after MyChild::creating(), however, it does work. (I didn't confirm whether doSomethingInParent() fires, but I'm presuming it doesn't)

Can Laravel have multiple events registered to Model::creating()?

like image 705
Anthony Avatar asked Oct 07 '14 20:10

Anthony


1 Answers

This one is tricky. Short version: Remove your return values from you handlers and both events will fire. Long version follows.

First, I'm going to assume you meant to type MyParent (not myParent), that you meant your boot methods to be protected, and not private, and that you included a final ) in your create method calls. Otherwise your code doesn't run. :)

However, the problem you describe is real. The reason for it is certain Eloquent events are considered "halting" events. That is, for some events, if any non-null value is returned from the event handlers (be it a closure or PHP callback), the event will stop propagating. You can see this in the dispatcher

#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
public function fire($event, $payload = array(), $halt = false)
{
}

See that third parameter $halt? Later on, while the dispatcher is calling event listeners

#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
    foreach ($this->getListeners($event) as $listener)
    {
        $response = call_user_func_array($listener, $payload);

        // If a response is returned from the listener and event halting is enabled
        // we will just return this response, and not call the rest of the event
        // listeners. Otherwise we will add the response on the response list.
        if ( ! is_null($response) && $halt)
        {
            array_pop($this->firing);

            return $response;
        }

    //...

If halt is true and the callback returned anything that's not null (true, false, a sclaer value, an array, an object), the fire method short circuits with a return $response, and the events stop propagating. This is above and beyond that standard "return false to stop event propagation". Some events have halting built in.

So, which Model events halt? If you look at the definition of fireModelEvent in the base eloquent model class (Laravel aliases this as Eloquent)

#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function fireModelEvent($event, $halt = true)
{
    //...
}

You can see a model's events default to halting. So, if we look through the model for firing events, we see the events that do halt are

#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('deleting') 
$this->fireModelEvent('saving')
$this->fireModelEvent('updating')
$this->fireModelEvent('creating')

and events that don't halt are

#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('booting', false);
$this->fireModelEvent('booted', false);
$this->fireModelEvent('deleted', false);
$this->fireModelEvent('saved', false);
$this->fireModelEvent('updated', false);
$this->fireModelEvent('created', false);

As you can see, creating is a halting event, which is why returning any value, even true, halted the event and your second listener didn't fire. Halting events are typically used when the Model class wants to do something with the return value from an event. Specifically for creating

#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function performInsert(Builder $query)
{
    if ($this->fireModelEvent('creating') === false) return false;
    //...

if you return false, (not null) from your callback, Laravel will actually skip performing the INSERT. Again, this is different behavior from the standard stop event propagation by returning false. In the case of these four model events, returning false will also cancel the action they're listening for.

Remove the return values (or return null) and you'll be good to go.

like image 107
Alan Storm Avatar answered Nov 15 '22 04:11

Alan Storm