[CakePHP3] beforeFilter()でリダイレクトが機能しない問題とその対策
2018/12/20
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()
以外のものを返却する可能性がある場合はコードを調整してください。