Framework: Eloquent Model events are not triggered on inheritance (event bubbling)

Created on 10 Sep 2018  路  9Comments  路  Source: laravel/framework

  • Laravel Version: 5.7.1
  • PHP Version: 7.2.2
  • Database Driver & Version: mysql

Description:

i use model inheritance , imagine that you have a parent model and many child models.
if you fire a child model's event , parent model event will not be triggered and cant be observed by parent model's observer! how can i resolve this? i want "creating" event, to be triggered in parent model when child "creating" event triggered. (similar to event bubbling in ECMA scripts).

Most helpful comment

Hi @khanzadimahdi I think I found a way to do it that is already supported by Laravel.

Instead of registering the observer in the AppServiceProvider you can register the observer in the parent model boot static method:

~~~php
namespace AppModels;

use IlluminateDatabaseEloquentModel;

class Config extends Model
{
protected $table='configs';

protected $fillable=['name','value'];

protected static function boot()
{
    // you MUST call the parent boot method 
    // in this case the \Illuminate\Database\Eloquent\Model
    parent::boot(); 

    // note I am using static::observe(...) instead of Config::observe(...)
    // this way the child classes auto-register the observer to their own class
    static::observe( ConfigObserver::class );
}

}
~~~

You don't need to add this method to the child classes. Please don't forget to remove the observer registration from your AppServiceProvider.

Note that this not a event-bubbling mechanism, here we are resorting to a PHP feature in which static resolves to the current class. As every model calls the boot method when initialized, the child classes will inherit the Config boot method and will register the observer to themselves.

All 9 comments

It will be good if you will provide some code examples of what you want to do.

my parent model :

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Config extends Model
{
    protected $table='configs';

    protected $fillable=['name','value'];

}

my first child

<?php

namespace App\Models\Config;

use App\Models\Config;
use Illuminate\Database\Eloquent\Builder;

class Email extends Config
{
    /**
     * available configs
     *
     * @var array
     */
    public static $configs=[
        'mail_host' => 'mail.host',
        'mail_port' => 'mail.port',
        'mail_username' => 'mail.username',
        'mail_password' => 'mail.password',
    ];

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('type', function (Builder $builder) {
            $builder->where('type', '=', self::class);
        });
    }

}

my second child :

<?php

namespace App\Models\Config;

use App\Models\Config;
use Illuminate\Database\Eloquent\Builder;

class User extends Config
{
    /**
     * available configs
     *
     * @var array
     */
    public static $configs=[
        'must_verify_email' => 'user.mustVerifyEmail',
        'must_verify_mobile' => 'user.mustVerifyMobile',
        'must_verify_account' => 'user.mustVerifyAccount',

        'registration' => 'user.registration.enabled',
        'registration_rules' => 'user.registration.rules',

        'require_email' => 'user.required.email',
        'require_mobile' => 'user.required.mobile',
        'require_username' => 'user.required.username',
        'require_name' => 'user.required.name',
    ];

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('type', function (Builder $builder) {
            $builder->where('type', '=', self::class);
        });
    }

}

i have an observer for my parent model like this:

namespace App\Observers;

use App\Models\Config;

class ConfigObserver
{
    /**
     * Handle the user "creating" event.
     *
     * @param  \App\Models\Config $config
     * @return void
     */
    public function creating(Config $config){
        $config->type=get_class($config);
    }
}

then i add my observer in AppServiceProvider like below:

Config::observe(ConfigObserver::class);

when i run the below code using child model, the creating event must be fired in parent model :

$config=Email::firstOrCreate(['name'=>$name],['value'=>$value]);

but , nothing happens! parent's "creating" event not triggered! laravel doesn't support event bubbling?

@taylorotwell what's your idea ? would you add event bubbling in next versions?

You have to specify the child models in AppServiceProvider:

Email::observe(ConfigObserver::class);
User::observe(ConfigObserver::class);

@staudenmeir i khow that. i must write repetitive line of codes in this way.
in the best way , events must bubbled to their parents (similar to JavaScript events)

I don't think there are plans to support this, it would require bigger changes.

It's my impression that Eloquent isn't really meant to be used with child models.

First: Javascript doesn鈥檛 have native event bubbling, the DOM API in browser has.

Second: even in DOM the event does not bubble to a parent class of an element which is not declared explicitly in the DOM (similar to an instance), most DOM events bubbles up to any element which contains the target element. That doesn鈥檛 have anything to do with type inheritance.

For example : if a HTML document has two forms , the events triggered inside the first form are not bubbled to the second form.

I am not saying your idea is not valid, but the example you gave is not comparable.

It is doable , for sure, to a child class inherit the events from a super class, but I don鈥檛 think it is worth, for example, if we have a long class inheritance, Laravel would have to look up th whole inheritance chain on each model instantiation fot event listeners and observers.

And what should be the behavior if the user assigns an observer both to a super class and a child class? Should both listeneres be executed? In which order? And with current implementation there would be no generic way to stop propagation of a model event on several listeners.

Hi @khanzadimahdi I think I found a way to do it that is already supported by Laravel.

Instead of registering the observer in the AppServiceProvider you can register the observer in the parent model boot static method:

~~~php
namespace AppModels;

use IlluminateDatabaseEloquentModel;

class Config extends Model
{
protected $table='configs';

protected $fillable=['name','value'];

protected static function boot()
{
    // you MUST call the parent boot method 
    // in this case the \Illuminate\Database\Eloquent\Model
    parent::boot(); 

    // note I am using static::observe(...) instead of Config::observe(...)
    // this way the child classes auto-register the observer to their own class
    static::observe( ConfigObserver::class );
}

}
~~~

You don't need to add this method to the child classes. Please don't forget to remove the observer registration from your AppServiceProvider.

Note that this not a event-bubbling mechanism, here we are resorting to a PHP feature in which static resolves to the current class. As every model calls the boot method when initialized, the child classes will inherit the Config boot method and will register the observer to themselves.

@rodrigopedra tnx bro.

Was this page helpful?
0 / 5 - 0 ratings