かきノート

【Laravel】Observer には、どんな種類のメソッドが? ソースから解析してみた

September 27, 2022 • ☕️☕️ 8 min read

【 環境 】
Laravel のバージョン: 8.61.1
PHP のバージョン: 8.0.16
MySQL のバージョン: 5.7.32

Laravel の Observer

「Modelを操作する時、自動でこういう事をしたい」といった処理を書くとき、Observer が便利です。

例えば、「Model に “created_by” と作成者の IDを記録するカラムがあって、そこにログイン中のユーザのIDを入れたい(毎回 Model ごとに書くのは面倒なので、まとめておきたい)」といった場面ですね。

以下のコマンドで、「Sample」という Model に対応する「SampleObserver」を生成しています。

php artisan make:observer SampleObserver --model=Sample

コマンドを実行すると、こんな感じのソースが生成されます。

class SampleObserver
{
    /**
     * Handle the Sample "created" event.
     *
     * @param  \App\Models\Sample  $sample
     * @return void
     */
    public function created(Sample $sample)
    {
        //
    }

    /**
     * Handle the Sample "updated" event.
     *
     * @param  \App\Models\Sample  $sample
     * @return void
     */
    public function updated(Sample $sample)
    {
        //
    }

//(以下略)

作成後、EventServiceProvider にオブジェクトを追加。

app\Providers\EventServiceProvider.php

    public function boot()
    {
        \App\Models\Sample::observe(\App\Observers\SampleObserver::class);
    }

こんな感じでレコードを作成したり更新したりする処理を記述すると、自動で Observer にて定義したメソッドをコールしてくれる。

    $sample = new Sample();
    $sample->name = 'name01';
    $sample->save();

この例だと、「created」メソッドがコールされる。

Observer で使用可能なメソッド

「insert した時」「delete した時」といったイベントを条件に様々なメソッドを自動でコールできる事が分かったけど、具体的にはどんなメソッドが使用可能なの?

と思って調べてみても、公式には記載はない模様。

なので、Laravel のソースを直接調べてみます。

src\Illuminate\Database\Eloquent\Concerns\HasEvents.php

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php#L95

    /**
     * Get the observable event names.
     *
     * @return array
     */
    public function getObservableEvents()
    {
        return array_merge(
            [
                'retrieved', 'creating', 'created', 'updating', 'updated',
                'saving', 'saved', 'restoring', 'restored', 'replicating',
                'deleting', 'deleted', 'forceDeleted',
            ],
            $this->observables
        );
    }

以下のメソッドが使用できるようです。

  • retrieved
  • creating
  • created
  • updating
  • updated
  • saving
  • saved
  • restoring
  • restored
  • replicating
  • deleting
  • deleted
  • forceDeleted

具体的には、こんな感じでイベントが追加されていました。

    /**
     * Register a retrieved model event with the dispatcher.
     *
     * @param  \Closure|string  $callback
     * @return void
     */
    public static function retrieved($callback)
    {
        static::registerModelEvent('retrieved', $callback);
    }
$this->fireModelEvent('forceDeleted', false);

実験用のソース

ソースやコメントを見るよりも、実際に動かしてみてどんなイベントが発火しているかを見ていく方が理解しやすいかと思われます。

試しに、こんな感じのコードを書いて、適当に Model 操作をしてみました。

class SampleObserver
{
    public function retrieved(Sample $sample)   { \Log::debug('Called method : ' . __METHOD__); }
    public function creating(Sample $sample)    { \Log::debug('Called method : ' . __METHOD__); }
    public function created(Sample $sample)     { \Log::debug('Called method : ' . __METHOD__); }
    public function updating(Sample $sample)    { \Log::debug('Called method : ' . __METHOD__); }
    public function updated(Sample $sample)     { \Log::debug('Called method : ' . __METHOD__); }
    public function saving(Sample $sample)      { \Log::debug('Called method : ' . __METHOD__); }
    public function saved(Sample $sample)       { \Log::debug('Called method : ' . __METHOD__); }
    public function restoring(Sample $sample)   { \Log::debug('Called method : ' . __METHOD__); }
    public function restored(Sample $sample)    { \Log::debug('Called method : ' . __METHOD__); }
    public function replicating(Sample $sample) { \Log::debug('Called method : ' . __METHOD__); }
    public function deleting(Sample $sample)    { \Log::debug('Called method : ' . __METHOD__); }
    public function deleted(Sample $sample)     { \Log::debug('Called method : ' . __METHOD__); }
    public function forceDeleted(Sample $sample){ \Log::debug('Called method : ' . __METHOD__); }
}

Update 実行時の発火イベントについて

Update のイベントについては何だか妙な動きをしたりする。

実行コード1

// (update)イベントが発火する
$sample = Sample::find(1);
$sample->name = 'updated 1';
$sample->save();

結果1

[2022-09-27 07:08:44] local.DEBUG: Called method : App\Observers\SampleObserver::retrieved  
[2022-09-27 07:08:44] local.DEBUG: Called method : App\Observers\SampleObserver::saving  
[2022-09-27 07:08:44] local.DEBUG: Called method : App\Observers\SampleObserver::updating  
[2022-09-27 07:08:44] local.DEBUG: Called method : App\Observers\SampleObserver::updated  
[2022-09-27 07:08:44] local.DEBUG: Called method : App\Observers\SampleObserver::saved  

実行コード2

// (update)イベントが発火しない
Sample::where('id', 2)->update(['name' => 'updated 2']);

結果2

(ログ出力なし)

こんな感じで、saveメソッドにて更新するとイベントが発火するが、updateメソッドで更新すると、イベントが発火しない。

公式サイトを見てみると、こんな記述があった。 https://readouble.com/laravel/6.x/ja/eloquent.html

Eloquentの複数モデル更新を行う場合、更新モデルに対するsaving、saved、updating、updatedモデルイベントは発行されません。
その理由は複数モデル更新を行う時、実際にモデルが取得されるわけではないからです。

らしい。
where で 1レコードに絞った場合でも「複数モデルを扱う」という処理になるのだろうか。

何にせよ、マニュアルやソースに記述されたコメントを読んで理解するよりも、実際に動かして動作確認しながら作っていった方が、後で意図しない挙動に悩まされずに済みそうです。

(追加調査)

update メソッドの謎挙動について、もう少し調べてみました。

where メソッドではなく、find もしくは findOrFail メソッドを使用すると、Observer のイベントが発火するようです。

実行コード3

Sample::find(2)->update(['name' => 'updated 3']);

// "findOrFail" でも可
// Sample::findOrFail(2)->update(['name' => 'updated 3']);

結果3

[2022-09-28 01:50:22] local.DEBUG: Called method : App\Observers\SampleObserver::retrieved  
[2022-09-28 01:50:23] local.DEBUG: Called method : App\Observers\SampleObserver::saving  
[2022-09-28 01:50:23] local.DEBUG: Called method : App\Observers\SampleObserver::updating  
[2022-09-28 01:50:23] local.DEBUG: Called method : App\Observers\SampleObserver::updated  
[2022-09-28 01:50:23] local.DEBUG: Called method : App\Observers\SampleObserver::saved  

ところが、このコードを2回以上動かすと、結果は以下のようになります。

結果3-2

[2022-09-28 01:56:49] local.DEBUG: Called method : App\Observers\SampleObserver::retrieved  
[2022-09-28 01:56:49] local.DEBUG: Called method : App\Observers\SampleObserver::saving  
[2022-09-28 01:56:49] local.DEBUG: Called method : App\Observers\SampleObserver::saved  

Observer の update メソッドがコールされていませんでした。

さらに実験して分かったのですが、どうやら Observer の update メソッドがコールされるのは、値が更新された時のみのようです。

「2回以上同じ update 文を走らせたところで値の変化は無いので、update イベントは発生しない」という考え方みたいです。

今回のケースでは、毎回 update メソッドの起動を確認するコードを書きたかったので、こんな感じで対処してみました。

実行コード4

Sample::find(2)->update(['name' => 'updated 3' . date('YmdHis')]);

結果4

[2022-09-28 02:04:29] local.DEBUG: Called method : App\Observers\SampleObserver::retrieved  
[2022-09-28 02:04:30] local.DEBUG: Called method : App\Observers\SampleObserver::saving  
[2022-09-28 02:04:30] local.DEBUG: Called method : App\Observers\SampleObserver::updating  
[2022-09-28 02:04:30] local.DEBUG: Called method : App\Observers\SampleObserver::updated  
[2022-09-28 02:04:30] local.DEBUG: Called method : App\Observers\SampleObserver::saved  
[2022-09-28 02:04:38] local.DEBUG: Called method : App\Observers\SampleObserver::retrieved  
[2022-09-28 02:04:38] local.DEBUG: Called method : App\Observers\SampleObserver::saving  
[2022-09-28 02:04:38] local.DEBUG: Called method : App\Observers\SampleObserver::updating  
[2022-09-28 02:04:38] local.DEBUG: Called method : App\Observers\SampleObserver::updated  
[2022-09-28 02:04:38] local.DEBUG: Called method : App\Observers\SampleObserver::saved  

こんな感じで、2回・3回とコールしても、無事 update イベントが起動しています。

挙動に妙な部分が多く、「全然動かねーぞ・・?」と困惑する人が多そうな気がするので、マニュアルに書いてあると嬉しい所なのですが、こんな謎挙動をマニュアルに詳しく書くのが面倒くさいうえ、色々と突っ込みが入りそうだから放置してたりするのだろうか。


Relative Posts:

【Laravel】ダウンロード処理の途中に、echo や var_dump 等の標準出力処理があった場合、正常に動かなくなる事がある2

July 21, 2022

福岡の物流エンジニアが、七転び八起きしたあと九回転び、寝っ転がったまま何かやってる事を垂れ流しているブログ

RotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-icon