0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

イベントリスナー beforeSave() での return false は 6.x で破壊的になる - CakePHP 5.2 Deprecation

0
Posted at

CakePHP 5.2 にアップグレード後に、

deprecated: 16384 :: Since 5.2.0: Returning a value from event listeners is deprecated.
Use `$event->setResult()` instead in `Model.beforeSave` of `App\Model\Table\ArticlesTable::beforeSave()`

のような deprecation の警告が多数表示されていたため、調べてみました。

放置したまま 6.x にアップグレードすると、メソッドが無くなって即エラーで落ちるのではなく、エラーが一切出ないまま深刻な事態が発生することが判明したので、記録として残します。

(上記は beforeSave() の例です。他のイベントリスナー、例えば beforeFilter() で値を返している場合は Controller.initialize of App\Controller\XxxController::beforeFilter() のように出ます。)

サマリ

  • CakePHP 5.2.0 で「イベントリスナー/コールバックが値を返す」ことが deprecated になりました。
    従来長いこと、例えば beforeSave()return false することで、save をキャンセルするような使い方が公式ドキュメントでも説明されてきましたが、使用できなくなります。
    代替は $event->setResult()(結果を返す)/ $event->stopPropagation()(イベント伝播停止)。
  • 5.x では従来どおり動きますreturn false も戻り値も有効で、警告が出るだけです)。
  • ただし、6.0.0 で削除され、戻り値が完全に無視されて挙動が変わってしまいます。
    結果、beforeSave()return false している場合、何もエラーが発生しないどころか、save されてしまうという問題が発生します。
    beforeSave の保存中断、beforeFilter の認証リダイレクトが気づかないうちに効かなくなる
  • 5.x のうちに、戻り値(return)の代わりに Event のメソッドを呼ぶよう直しておくことが必須です。

何が変わったのか(4.x → 5.2 → 6.x のコード変遷)

EventManager の実装を見てみます。

4.x:戻り値の解釈は dispatch() 側で処理

// 4.x EventManager::_callListener … 戻り値を返すだけ
protected function _callListener(callable $listener, EventInterface $event)
{
    $data = (array)$event->getData();
    return $listener($event, ...array_values($data));
}

// 4.x EventManager::dispatch() のループ
$result = $this->_callListener($listener['callable'], $event);
if ($result === false) {
    $event->stopPropagation();   // return false → 伝播停止
}
if ($result !== null) {
    $event->setResult($result);  // 戻り値 → 結果へ
}

公式ドキュメント(「イベントの中止」)にも、こう書かれていました(この記述は 5.x 版でも 2026年6月現在そのままです。変更は移行ガイドにのみ記載されているので要注意):

イベントを中止するためには、コールバックで false を返すか、stopPropagation() を呼ぶ
コールバックが null や false 以外の値を返すたびに、それは $result プロパティーに格納されます

つまり return false でイベントを中止するのは、長いこと公式ドキュメントに記載されてきた正規のやり方です。beforeSave() の場合は、これでデータの保存がキャンセルされます。

5.2:判定を _callListener に移し、deprecation を追加

// 5.2 EventManager::_callListener (: void)
$result = $listener($event, ...array_values($event->getData()));
if ($result !== null) {
    deprecationWarning(
        '5.2.0',
        'Returning a value from event listeners is deprecated. '
        . 'Use `$event->setResult()` instead in `' . $event->getName() . '` of `' . $class . '`',
    );
    $event->setResult($result);
}
if ($event->getResult() === false) {
    $event->stopPropagation();   // 互換のため false→中断は維持
}

実装は若干変わりましたが、4.x までと挙動は変わりません。

ただし、deprecation warning が発生するように変更されています。

6.x ブランチ:戻り値を無視

6.x ブランチは開発中のもので今後変わる可能性はありますが、2026年6月現在は次のようになっています。

// 6.x EventManager::callListener … return $result の評価は完全に削除されている
protected function callListener(Closure $listener, EventInterface $event): void
{
    $listener($event, ...array_values($event->getData()));   // 戻り値は捕捉しない

    if ($event->getResult() === false) {
        $event->stopPropagation();   // getResult()===false は setResult(false) を呼んだ時だけ
    }
}

6.0 では return に関する処理が完全に削除されます。

結果、例えば beforeSave()return false してもイベントのキャンセル(=データを保存しない)が行われなくなります。そのままだとコードの意図に反して、save はキャンセルされず保存されてしまいます。また、その間何のエラーも発生しないままです。

5.x のうちに対処しておかないと、6.x では気づかないまま致命的なバグを抱え込むことになります。


deprecated になった理由

CakePHP PR #18196「Deprecate returning values from event listeners.」の本文(和訳):

This allows callback methods to have a consistent return type void.
(コールバックの戻り型を一貫して void にできる)

  • #16786「Simplify event callback signature」… コールバックの戻り型が不統一でドキュメント不足、
    IDE helper や静的解析で扱いづらい。全部 void にして Event API に寄せよう、という提案。
  • #16854「Strictness of event dispatch result / return values」… 戻り値ベースの結果処理が型的に脆い
    checkRules(): boolnull を受けて型エラー、など)。

false を返すと暗黙に伝播停止」という挙動についても、PR で ADmad 自身が
「I am not a fan of this behavior(好きではないが 5.x では変えられない)」とコメントしています。
“結果設定” と “伝播停止” を戻り値という 1 本のチャンネルに混載していたのを、明示 API に分離する
というのが趣旨のようです。


移行方針

原則: コールバックの方を : void にし、return の代わりに Event のメソッドを呼ぶ。

やりたいこと 4.x までの書き方(deprecated) 5.2 以降の書き方
値(Response・entity 等)を結果として返す return $value; $event->setResult($value);
イベントを止める(後続リスナー停止 / beforeSave 中断) return false; $event->stopPropagation();
両方(結果を設定しつつ中断) return false;(false のみ兼用) 両方呼ぶ

setResult() / stopPropagation() は「イベントへの伝達」、return; は「自分の関数を抜ける」で役割が別です。

beforeFilter で redirect する場合の対応例

// Before(5.2 で警告 / 放置した場合 6.0 でバグ化)
public function beforeFilter(EventInterface $event)
{
    if ($notAllowed) {
        return $this->redirect(['_name' => 'login']);
    }
}

// After(5.2 でも 6.0 でも OK)
public function beforeFilter(EventInterface $event): void
{
    if ($notAllowed) {
        $event->setResult($this->redirect(['_name' => 'login']));
        return;
    }
}

beforeSave で保存中断の場合の対応例

// Before(5.2 で警告 / 6.0 で中断が効かず保存されてしまいバグ化)
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): ?bool
{
    if ($invalid) {
        return false;
    }
    return null;
}

// After(5.2 でも 6.0 でも OK)
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
{
    if ($invalid) {
        $event->stopPropagation();   // 中断
        // 結果を明示するなら $event->setResult(false);
        return;
    }
}

注意点

  • 公式ドキュメントのイベント説明ページは更新されておらず、return false のまま記載されています。変更は移行ガイドにしか書かれていません。
    Issue #18357 で「docs を直す」と言及されていますが、現時点(2026-06)で未反映です。
  • 6.0 はエラーを出さないreturn false / Response 返しは何の反応もしなくなります。
    何も表示されないのに挙動が変わるため要注意です。
    6.0 アップグレード前に、本 deprecation を解消しておく必要があります。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?