【CakePHP3】beforeFilter()でリダイレクトが機能しない問題とその対策

2018/12/20

author

masyus

CakePHP3でController::beforeFilter()からAppController::beforeFilter()を呼び出し、その内部で$this->redirect()を実行しますと、なぜか子Controller::action()を通ってからリダイレクトされてしまう現象にハマりました。今回のその対処方法を紹介いたします。検証したCakePHPのバージョンは3.6.13、PHPは7.3です。

beforeFilter()で$this->redirect()を呼び出してもaction()が実行されてしまうケース

例として、AppControllerを継承するUsersControllerがあるとします。

<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\ORM\TableRegistry;
use Cake\Event\Event;

class UsersController extends AppController
{
    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
    }

    public function index()
    {
        $users = $this->Users->find()
            ->where(['deleted' => 0])
            ->all();

        $this->set(compact('users'));
    }
}

AppControllerは下記です。

<?php
namespace App\Controller;

use Cake\Controller\Controller;
use Cake\Event\Event;
use Cake\Core\Configure;

class AppController extends Controller
{
    public function beforeFilter(Event $event)
    {
        if ($this->name == 'Users' && $this->request->action == 'index') {
            $this->Flash->error(__('アクセス権限がありません。'));
            return $this->redirect('/login');
        }
    }
}

この状態で/usersにアクセスすると、index()の処理を通過せずに/loginにリダイレクトされることが期待できそうですが、実際は下記の挙動になりました。

  • UsersController::index()の処理を通過する
  • /loginにリダイレクトする

原因: Controller:redirect()はレスポンスオブジェクトを返す挙動をする

Controller内で$this->redirect()を実行するとレスポンスオブジェクトを返しますが、returnを組み合わせることで即座にレスポンスオブジェクトを返し、後続の処理をスキップすることができます。但し

スーパークラスのメソッド内で呼び出される場合、呼び出し元へレスポンスオプジェクトを返す挙動になる

ため、呼び出し元でも同様にレスポンスオブジェクトをreturnさせる処理が必要になります。 今回の実装における処理の流れを時系列で細かく追ってみましょう。

  • UsersController::beforeFilter()が呼び出される
  • AppController::beforeFilter()が呼び出される
  • AppController::beforeFilter()の返り値が$this->redirect()となる
  • UsersController::beforeFilter()内のparent::beforeFilter($event);の実行結果は$this->redirect()を得る
  • UsersController::index()が呼び出される
  • UsersController::index()実行完了後に/loginへリダイレクトする

上記4.にて$this->redirect()returnできていないため、この問題が発生します。

対策: 呼び出し元でもレスポンスオブジェクトをreturnさせましょう

UsersController::beforeFilter()を下記のように書き換えることで、index()を呼び出さずにbeforeFilter()内で即座にリダイレクトできるようになります。

<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\ORM\TableRegistry;
use Cake\Event\Event;

class UsersController extends AppController
{
    public function beforeFilter(Event $event)
    {
        // parent::beforeFilter($event);の実行結果を変数で受け取り、中身があればreturnさせる
        $result = parent::beforeFilter($event);
        if ($result) {
            return $result;
        }
    }

    public function index()
    {
        $users = $this->Users->find()
            ->where(['deleted' => 0])
            ->all();

        $this->set(compact('users'));
    }
}

但し、AppController::beforeFilter()で$this->redirect()以外のものを返却する可能性がある場合はコードを調整してください。

参考