September 27, 2022 • ☕️☕️ 8 min read
【 環境 】
Laravel のバージョン: 8.61.1
PHP のバージョン: 8.0.16
MySQL のバージョン: 5.7.32
「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 にオブジェクトを追加。
public function boot()
{
\App\Models\Sample::observe(\App\Observers\SampleObserver::class);
}
こんな感じでレコードを作成したり更新したりする処理を記述すると、自動で Observer にて定義したメソッドをコールしてくれる。
$sample = new Sample();
$sample->name = 'name01';
$sample->save();
この例だと、「created」メソッドがコールされる。
「insert した時」「delete した時」といったイベントを条件に様々なメソッドを自動でコールできる事が分かったけど、具体的にはどんなメソッドが使用可能なの?
と思って調べてみても、公式には記載はない模様。
なので、Laravel のソースを直接調べてみます。
/**
* 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
);
}
以下のメソッドが使用できるようです。
具体的には、こんな感じでイベントが追加されていました。
/**
* 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)イベントが発火する
$sample = Sample::find(1);
$sample->name = 'updated 1';
$sample->save();
[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
// (update)イベントが発火しない
Sample::where('id', 2)->update(['name' => 'updated 2']);
(ログ出力なし)
こんな感じで、saveメソッドにて更新するとイベントが発火するが、updateメソッドで更新すると、イベントが発火しない。
公式サイトを見てみると、こんな記述があった。 https://readouble.com/laravel/6.x/ja/eloquent.html
Eloquentの複数モデル更新を行う場合、更新モデルに対するsaving、saved、updating、updatedモデルイベントは発行されません。
その理由は複数モデル更新を行う時、実際にモデルが取得されるわけではないからです。
らしい。
where で 1レコードに絞った場合でも「複数モデルを扱う」という処理になるのだろうか。
何にせよ、マニュアルやソースに記述されたコメントを読んで理解するよりも、実際に動かして動作確認しながら作っていった方が、後で意図しない挙動に悩まされずに済みそうです。
update メソッドの謎挙動について、もう少し調べてみました。
where メソッドではなく、find もしくは findOrFail メソッドを使用すると、Observer のイベントが発火するようです。
Sample::find(2)->update(['name' => 'updated 3']);
// "findOrFail" でも可
// Sample::findOrFail(2)->update(['name' => 'updated 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回以上動かすと、結果は以下のようになります。
[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 メソッドの起動を確認するコードを書きたかったので、こんな感じで対処してみました。
Sample::find(2)->update(['name' => 'updated 3' . date('YmdHis')]);
[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 イベントが起動しています。
挙動に妙な部分が多く、「全然動かねーぞ・・?」と困惑する人が多そうな気がするので、マニュアルに書いてあると嬉しい所なのですが、こんな謎挙動をマニュアルに詳しく書くのが面倒くさいうえ、色々と突っ込みが入りそうだから放置してたりするのだろうか。