Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

randm's avatar

How to trigger ModelObserver for all Model Events

Hi, I am new to Laravel. I am trying to learn it in order to convert a large project to it.

I wrote a ModelObserver that will do couple of things

  1. populate a column called "created_by" when a new record is added to the db
  2. populate a column called "modified_by" when existing record in updated
  3. populate a column called "purged_by" when existing record is soft deleted
  4. Prior Saving it will trim any string a user passes
  5. Prior Saving It will convert any empty string to null

so I created a file "app\Observers\ModelObserver.php." This file contains the following code

<?php 

namespace App\Observers;

class ModelObserver {

    $userID;

    public function __construct($userID){
        $this->userID =  Auth::id();
    }


    public function updating($model)
    {
        $model->modfied_by = $this->userID;
    }


    public function creating($model)
    {
        $model->created_by = $this->userID;
    }


    public function removing($model)
    {
        $model->purged_by = $this->userID;
    }



    public function saving($model)
    {

        $model->modfied_by = $this->userID;
        $values = $model->attributesToArray();
        
        //dd($values);

        foreach($values as $attribute => $value)
        {
            $model->{$attribute} = emptyToNull($value);
        }
    }

    /**
     * return numm value if the string is empty
     *
    */
    private function emptyToNull($string)
    {
        //trim every value
        $string = trim($string);

        if ($string === ''){
           return null;
        }

        return $string;
    }

}

Then I created another file with the same name in "app\ModelObserver.php" this file contains the following code

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use app\Observers\ModelObserver;

class ModelObserver  extends Model
{
    

    public static function boot()
    {
        $class = get_called_class();
        $class::observe(new ModelObserver);
    
        parent::boot();
    }
}

But this observer does not seems to be firing off like I expected.

How can I automatically call this Observer on every Model?

Additionally, is there an easy why where I can exclude some models from this?

One last question: When will the ModelObserver be called, is it before or after the validation process?

Please give me your feedback on the code and how to make it more better. Again, all I want to do not is to learn the correct way of doing thing and learn how to write better/cleaner code before converting my large project to Laravel.

0 likes
13 replies
phildawson's avatar

Well I'd go with making a trait. Forget 'app\ModelObserver.php' make a 'app\ObservantTrait.php'

<?php

namespace App;

use App\Observers\ModelObserver;

trait ObservantTrait
{
    public static function bootObservantTrait()
    {
        static::observe(new ModelObserver);
    }
}

then for any models that need it.

class Blah extends Model
{
    use ObservantTrait;

  // ...
}
2 likes
randm's avatar

@phildawson Thank you so much for that. What is a trait? will the trait be used before or after Validation?

phildawson's avatar

a trait is just a piece of reusable code http://php.net/manual/en/language.oop5.traits.php

Another example with Models would be.

class Attachment
{
    public function attachable()
    {
        return $this->morphTo();
    }
}

trait Attachable
{
    public function attachments()
    {
        return $this->morphMany(Attachment::class, 'attachable');
    }
}

class Person {
    use Attachable;

}

class Event {
    use Attachable;
    
}

Both Person and Event Models will have the attachments method as if it was:

class Attachment
{
    public function attachable()
    {
        return $this->morphTo();
    }
}

class Person {
    public function attachments()
    {
        return $this->morphMany(Attachment::class, 'attachable');
    }

}

class Event {
    public function attachments()
    {
        return $this->morphMany(Attachment::class, 'attachable');
    }
}

think of it as copy+paste.

The Laravel specific thing to take note of that I mentioned is:

If an Eloquent model uses a trait that has a method matching the bootNameOfTrait naming convention, that trait method will be called when the Eloquent model is booted, giving you an opportunity to register a global scope, or do anything else you want.

1 like
randm's avatar

@phildawson Thank you for your help.

I made the change but it is not working like expected.

Here is my Account Model code

in my ModelObserver class I tried to do dd($values) in side my saving event and I did not get any values. it is like the observer never fires off.

phildawson's avatar

@malhayek the use trait needs to be inside the class you want to use it. The use outside the class is for importing into the current namespace.

namespace App\Models;

...
use App\ObservantTrait;

class Account extends Model
{
    use ObservantTrait;

If ObservantTrait.php is put in the same Models folder and namespace App\Models then you can omit the use outside the class.

In my example above I should have added the App namespace to make clearer where Blah was.

namespace App;

class Blah extends Model
{
    use ObservantTrait;

  // ...
}
randm's avatar

@phildawson, when I put use ObservantTrait

I get this error Trait 'App\Models\ObservantTrait' not found

phildawson's avatar

Is your file App\Models\ObservantTrait.php with a namespace App\Models; at the top of it?

If you are keeping it in App\ObservantTrait.php with namespace App; then you need to use 'use' outside to import the class into the current namespace and use inside the class. Example just above.

martinbean's avatar
  1. populate a column called "created_by" when a new record is added to the db
  2. populate a column called "modified_by" when existing record in updated
  3. populate a column called "purged_by" when existing record is soft deleted

@malhayek You do realise Eloquent does this automatically (except the column are named updated_at and deleted_at in the case of the latter two).

With regards to trimming user-submitted values, you could create a trait that overrides the model setAttribute() method to trim strings:

trait TrimScalarValues
{
    public function setAttribute($key, $value)
    {
        if (is_scalar($value)) {
            $value = trim($value);
        }

        return $this->setAttribute($key, $value);
    }
}

Usage:

class SomeModel extends Model
{
    use TrimScalarValues; // Don’t forget to import full namespace
}
randm's avatar

@martinbean
The trait TrimScalarValues seems to be not saving the records. when I save a record I should be routed to the app.dev/accounts page. But when using the TrimScalarValues I get a blank page and no routing take place.

Also, When using ObservantTrait trait suggested by @phildawson the field updated_by are not populated like expected.

Any ideas of the problem? Thank you

Here is my full code

I have a new folder "app\Traits" inside of it I have 2 files

  1. TrimScalarValues.php which contains the following code
<?php

namespace App\Traits;

trait TrimScalarValues
{
    public function setAttribute($key, $value)
    {
        if (is_scalar($value)) {
            $value = $this->emptyStringToNull(trim($value));
        }

        return $this->setAttribute($key, $value);
    }


    /**
     * return null value if the string is empty otherwise it returns what every the value is
     *
    */
    private function emptyStringToNull($string)
    {
        //trim every value
        $string = trim($string);

        if ($string === ''){
           return null;
        }

        return $string;
    }
}
  1. RecordSignature.php which contains the following code
<?php

namespace App\Traits;

use app\Observers\ModelSignature;

trait RecordSignature
{
    
    public static function bootObservantTrait()
    {
        static::observe(new ModelSignature);
    }
}

Then I have Observer "app\Observers\ModelSignature.php" which contains the following code

<?php 

namespace App\Observers;

class ModelSignature {

    protected $userID;

    public function __construct($userID){
        $this->userID =  Auth::id();
    }


    public function updating($model)
    {
        $model->modfied_by = $this->userID;
    }


    public function creating($model)
    {
        $model->created_by = $this->userID;
    }


    public function removing($model)
    {
        $model->purged_by = $this->userID;
    }

}

Finally I have a "app\Models\Account.php" files which contains the following code

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\industry;
use App\Traits\RecordSignature;
use App\Traits\TrimScalarValues;


class Account extends Model
{
    use RecordSignature, TrimScalarValues;
    /**
     * The database table used by the model.
     *
     * @var string
    */
    protected $table = 'accounts';

    protected $primaryKey = 'account_id';

    const CREATED_AT = 'created_on';

    const UPDATED_AT = 'modified_on';

    const REMOVED_AT = 'purged_on';


    /**
     * The attributes that are mass assignable.
     *
     * @var array
    */
    protected $fillable = ['client_id','account_name', 'company_code', 'legal_name', 'created_by','modified_by','instrucations'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
    */
    //protected $hidden = ['account_id', 'remember_token'];


    protected $guarded = ['account_id'];

    /**
     * Get the industry record associated with the account.
    */
    public function industry()
    {
        return $this->hasOne(industry, industry::primaryKey);
    }

    public function pk(){

        return $this->primaryKey;
    }
    
}
martinbean's avatar

@malhayek That seems to be more a problem with your routing/controller. The trait just trims values as they’re set on the model. It doesn’t handle any redirecting or anything like that (and a model shouldn’t).

randm's avatar

@martinbean but it works if I do not use the trait.

These are my routes

Route::bind('account', function($id){
    return Account::where('account_id', $id)->first();
});

Route::get('accounts', array(
    'as' => 'accounts_index_path',
    'uses' => 'AccountController@index')
);

Route::get('account/create', array(
    'as' => 'account_create_path',
    'uses' => 'AccountController@create')
);

Route::post('account/store', array(
    'as' => 'account_store_path',
    'uses' => 'AccountController@store')
);

Route::get('account/{account}', array(
    'as' => 'account_show_path',
    'uses' => 'AccountController@show')
)->where('account', '[0-9]+');

Route::get('account/{account}/edit', array(
    'as' => 'account_edit_path',
    'uses' => 'AccountController@edit')
)->where('account', '[0-9]+');

Route::put('account/{account}/update', array(
    'as' => 'account_update_path',
    'uses' => 'AccountController@update')
)->where('account', '[0-9]+');
randm's avatar

@martinbean, thank you so much the code finally worked after change the return value from $this->setAttribute .... to parent::setAttribute

<?php

namespace App\Traits;

trait TrimScalarValues
{
    public function setAttribute($key, $value)
    {
        if (is_scalar($value)) {
            $value = $this->emptyStringToNull( trim($value) );

        }

        return parent::setAttribute($key, $value);
    }


    /**
     * return null value if the string is empty otherwise it returns what every the value is
     *
    */
    public function emptyStringToNull($string)
    {
        //trim every value
        $string = trim($string);

        if ($string === ""){
           return null;
        }

        return $string;
    }
}

The only problem is the the created_by, modified_by, removing_by is not updating.

Please or to participate in this conversation.