CakePHP4 で AppTable を継承する代わりに EventListener を使う

CakePHP4 がリリース されてから、これまで作ってきたものを CakePHP4 でいちから書き直してみてます。

この記事は EventListener の使い方を簡単に説明します。 CakePHP3 でも同じなハズです。

便利な Behavior を使いたい

Cookbook では Behavior の例として TimestampBehavior が記述されています。

https://book.cakephp.org/3/ja/orm/behaviors.html

TimestampBehavior は汎用性が高く、すべてのテーブルで利用したくなります。

これまでは AppTable を作って継承してた

これまでは、 AppTable を作成して initialize()TimestampBehavior を追加して、

<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Table;

class AppTable extends Table
{
    /**
     * @inheritDoc
     */
    public function initialize(array $config): void
    {
        // 登録時 created と modified の自動設定
        $this->addBehavior('Timestamp');
    }
}

各テーブルで AppTable を継承するという手段をとっていました。

<?php
declare(strict_types=1);

namespace App\Model\Table;

class ArticlesTable extends AppTable
{
}

AppTable には、他にも beforeSave, beforeDelete でログを出す処理を入れていました。

このやり方だと、TimestampBehavior を利用するためだけにわざわざ Table クラスを作る必要があります。

もっと楽にやれないかとググってたら issue がありました。

github.com

EventListener でイベントとして登録しておく

CakePHP にはイベントシステムがあります。

https://book.cakephp.org/3/ja/core-libraries/events.html

src/Event にリスナークラスを置きます。

<?php
declare(strict_types=1);

namespace App\Event;

use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\Event\EventListenerInterface;
use Cake\Log\Log;

class ModelListener implements EventListenerInterface
{
    /**
     * イベントに処理を設定する。
     *
     * @return array
     */
    public function implementedEvents(): array
    {
        return [
            'Model.initialize' => 'initializeEvent',
            'Model.beforeSave' => 'loggingEntity',
            'Model.beforeDelete' => 'loggingEntity',
        ];
    }

    /**
     * Model の initialize 時の処理。
     *
     * @param \Cake\Event\Event $event イベント
     * @return void
     */
    public function initializeEvent(Event $event)
    {
        $table = $event->getSubject();

        // 登録時 created と modified の自動設定
        $table->addBehavior('Timestamp');
    }

    /**
     * エンティティをログに出力します。
     *
     * @param \Cake\Event\Event $event イベント
     * @param \Cake\Datasource\EntityInterface $entity エンティティ
     * @param \ArrayObject $options オプション
     * @return void
     */
    public function loggingEntity(Event $event, EntityInterface $entity, \ArrayObject $options)
    {
        // DebugKit は除く
        if (strpos(get_class($entity), 'DebugKit') === 0) {
            return;
        }

        if (env('ENVIRONMENT') !== 'production') {
            Log::debug($event->getName() . ': ' . json_encode($entity, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }
    }
}

後はリスナーを EventManager に登録するだけです。

<?php
declare(strict_types=1);

namespace App;

use App\Event\ModelListener;
use Cake\Event\EventManager;

class Application extends BaseApplication
{
    public function bootstrap(): void
    {
        /* 中略 */
        
        // モデルに関するイベントを設定
        $listener = new ModelListener();
        EventManager::instance()->on($listener);
    }
}

僕は常に利用したいので src/Application.php で設定してますが、各コントローラなんかでも設定できます。

最後に

AppTable と各テーブルのクラスを作ってたけど、リスナークラス 1 つだけ作ればよくなった!