CakePHP4を試す


AuthenticationプラグインとDebug Kitプラグイン

Authenticationプラグインの利用方法は公式ドキュメントのクイックスタート(本家)とチュートリアルを参考に導入してみました。
しかしDebug Kitのツールバーが表示されません。よく見ると右下になにかあります。どうやら、Debug Kitのツールバーを表示できない旨のエラー表示のようです。
今回は何とかして、Debug Kitを表示させたいと思います。

Authenticationプラグインをインストール

まずは次のようにAuthenticationプラグインをインストールします。

> cd <CakePHPプロジェクトのROOTディレクトリ> ↵
> composer require cakephp/authentication:^2.0 ↵

ユーザページを作成

次のようなSQLを使って、認証に必須のテーブルを作成します。

Users.sql
CREATE TABLE IF NOT EXISTS `users` (
  `id` VARCHAR(36) CHARACTER SET 'utf8' BINARY NOT NULL COMMENT 'ユーザID:',
  `organization_id` INT NOT NULL COMMENT '組織ID:',
  `account` VARCHAR(255) CHARACTER SET 'utf8' BINARY NOT NULL COMMENT 'アカウント:ログイン時のアカウント。主にメールアドレス',
  `name` VARCHAR(255) CHARACTER SET 'utf8' NOT NULL COMMENT 'ユーザ名:表示用',
  `password` VARCHAR(128) CHARACTER SET 'utf8' BINARY NULL COMMENT 'パスワード:',
  `deletable` TINYINT NOT NULL COMMENT '削除可能フラグ:通常 True, システムが作成したユーザの場合 False',
  PRIMARY KEY (`id`),
  INDEX `fk_users_organizations_idx` (`organization_id` ASC),
  INDEX `ik_users_01` (`name` ASC),
  UNIQUE INDEX `uk_users_01` (`organization_id` ASC, `deletable` ASC, `account` ASC),
  CONSTRAINT `fk_users_organizations`
    FOREIGN KEY (`organization_id`)
    REFERENCES `organizations` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

続いてbakeしてベースとなるページを作成します。

$ bin/cake bake all users ↵

このテーブルでは、プライマリーキーにUUIDを使います。
以前のバージョンでは、idフィールドのデータ型がVARCHAR(36)の場合は勝手にUUIDを割り当ててくれたと記憶しているのですが、手元のバージョンでは違うようです。
次のようにエンティティーに修正を入れて、UUIDを割り当てるようにします。

/src/Model/Entity/User.php
use Cake\Utility\Text;

    /**
     * constructor
     */
    public function __construct(array $properties = array(), array $options = array())
    {
        parent::__construct($properties, $options);
        if ($this->isNew())
        {
            // Set UUID to the ID when creating an entity.
            $this->id = Text::uuid();
        }
    }

セキュリティーを考えるとパスワードを平文で保存することはできません。次のようにして符号化しましょう。


    /**
     * Automatically hash passwords when they are changed.
     * 
     */
    protected function _setPassword(string $password)
    {
        $hasher = new DefaultPasswordHasher();
        return $hasher->hash($password);
    }

この段階で、ユーザを作成します。
ユーザの作成を忘れて先に進むと、認証ができないためにログイン画面しか表示できなくなります。
対処方法はありますが回り道となりますので、ここで済ませてしまいます。

認証機能を設定

次のようにしてプラグインをロードします。

/src/Application.php
    /**
     * Load all the application configuration and bootstrap logic.
     *
     * @return void
     */
    public function bootstrap(): void
    {
           :
        // Load more plugins here
        $this->addPlugin('Authentication');

次のようにして認証をミドルウェアに追加します。

/src/Application.php
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
//use Cake\Http\MiddlewareQueue;
use Psr\Http\Message\ServerRequestInterface;
use Cake\Routing\Router;

/**
 * Application setup class.
 *
 * This defines the bootstrapping logic and middleware layers you
 * want to use in your application.
 */
class Application extends BaseApplication
                    implements AuthenticationServiceProviderInterface
{
           :
    /**
     * Setup the middleware queue your application will use.
     *
     * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
     * @return \Cake\Http\MiddlewareQueue The updated middleware queue.
     */
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        // Create an authentication middleware object
        $authentication = new AuthenticationMiddleware($this);

        $middlewareQueue
           :
            // Authentication should be added *after* RoutingMiddleware.
            // So that subdirectory information and routes are loaded.
            ->add($authentication)
            ;

        return $middlewareQueue;
    }

    /**
     * Returns a service provider instance.
     * 
     * @param \Psr\Http\Message\ServerRequestInterface $request Request
     * @return \Authentication\AuthenticationServiceInterface
     */
    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $loginPath = Router::url(['controller' => 'Users', 'action' => 'login']);
        $service = new AuthenticationService();
        $service->setConfig([
            'unauthenticatedRedirect' => $loginPath,
            'queryParam' => 'redirect',
        ]);

        $fields = [
            'username' => 'account',
            'password' => 'password',
        ];

        // Load the authenticators, you want session first
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('Authentication.Form', [
            'fields' => $fields,
            'loginUrl' => $loginPath,
        ]);

        // Load identifiers
        $service->loadIdentifier('Authentication.Password', compact('fields'));

        return $service;
    }

公式ドキュメントに少し修正を加えています。
use Cake\Http\MiddlewareQueue;は多重宣言になりますので、コメントアウトしました。
'unauthenticatedRedirect' => '/users/login','loginUrl' => '/users/login'は認証が必要な時のリダイレクト先を指定しているのですが、エントリーポイントをサブディレクトリにした場合にエラーになってしまうことを回避する目的で、$loginPath = Router::url(['controller' => 'Users', 'action' => 'login']);のようにパスを生成して'unauthenticatedRedirect' => $loginPath,'loginUrl' => $loginPath,のように指定しています。
その際、use Cake\Routing\Router;の宣言も必要になります。

筆者の環境では、エントリーポイントをサブディレクトリにした場合に、Login画面にて認証した後のページにリダイレクトされる際に問題が発生しました。
現時点では、エントリーポイントをルートディレクトリにして作業を行っております。

認証コンポーネントをロードします。

/src/Controller/AppController.php
           :
    public function initialize(): void
    {
        parent::initialize();
           :
        $this->loadComponent('Authentication.Authentication');

ログインアクションを追加します。

/src/Controller/UsersController.php
class UsersController extends AppController
{
           :
    /**
     * Login method
     * 
     */
    public function login()
    {
        $result = $this->Authentication->getResult();
        // If the user is logged in send them away.
        if ($result->isValid()) {
            $target = $this->Authentication->getLoginRedirect() ?? '/home';
            return $this->redirect($target);
        }
        if ($this->request->is('post') && !$result->isValid()) {
            $this->Flash->error('Invalid username or password');
        }
    }
/templates/Users/login.php
<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\User $user
 */
?>
<div class="users form">
    <?= $this->Flash->render() ?>
    <h3>Login</h3>
    <?= $this->Form->create() ?>
    <fieldset>
        <legend><?= __('Please enter your username and password') ?></legend>
        <?= $this->Form->control('account', ['required' => true]) ?>
        <?= $this->Form->control('password', ['required' => true]) ?>
    </fieldset>
    <?= $this->Form->submit(__('Login')); ?>
    <?= $this->Form->end() ?>

    <?= $this->Html->link("Add User", ['action' => 'add']) ?>
</div>

単純なログアウトアクションも追加します。

/src/Controller/UsersController.php
class UsersController extends AppController
{
           :
    /**
     * Logout method
     * 
     */
    public function logout()
    {
        $this->Authentication->logout();
        return $this->redirect(['controller' => 'Users', 'action' => 'login']);
    }

このままではLogin画面の為にLogin画面を表示しようとして...とおかしなことになるので、Login画面は認証せずに表示できるように指定します。

/src/Controller/UsersController.php
class UsersController extends AppController
{
           :
    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);

        $this->Authentication->allowUnauthenticated(['login']);
    }

以上で認証機能の実装ができました。
任意のページを開いてみましょう。
Loginページが表示され、認証が済むと指定したページが表示されると思います。

Debug Kitのツールバーが表示されるように

ここで一つ問題に気が付きました。Debug Kitのツールバーが表示されていません。代わりに、右下に何かが表示されています。
よく見ると、Debug Kitのツールバーを表示しようとしたもののエラーになったようで、そのエラー表示でした。
エラーの内容は、A route matching "array ( 'controller' => 'Users', 'action' => 'login', 'plugin' => 'DebugKit', '_ext' => NULL, )" could not be found.とあります。
どうやらRoute機構の設定がうまく行われていないために該当するコントローラを見つけられないようです。
Login画面だけで発生してるのなら目をつむることもできますが、認証が通った後の画面でも発生していますので、見逃せません。
対応方法を検討する必要があります。
今回は、フォーラムのDebugkit not showing due to missing routeにヒントがありました。
$builder->connect('/:controller/:action/*', ['plugin' => 'DebugKit']);をどこかに入れれば回避できるようです。
そこで、/config/routes.phpに設定してみることにします。

/config/routes.php
$routes->scope('/', function (RouteBuilder $builder) {
           :
    $builder->fallbacks();
    $builder->connect('/:controller/:action/*', ['plugin' => 'DebugKit']);
});

ちょっと待ってください。現在は開発中ですからDebug Kitプラグインを使用していますが、リリース時には不要になりますよね。
ここに記述するのであれば、Debug Kitプラグインを使用するか否かを判定してセットするのが筋でしょう。
それより、Debug Kitプラグインがロードされるときに設定されるようにすべきなのでは?
そこで、/config/routes.phpに行った変更を取り消して、/vendor/cakephp/debug_kit/config/routes.phpに設定してみることにします。

/vendor/cakephp/debug_kit/config/routes.php
Router::plugin('DebugKit', ['path' => '/debug-kit'], function (RouteBuilder $routes) {
           :
    $routes->scope(
        '/',
        function (RouteBuilder $routes) {
            $routes->connect('/:controller/:action/*', ['plugin' => 'DebugKit']);
        }
    );

    $routes->scope(
        '/mail-preview',
           :

任意のページを開くと、Debug Kitのツールバーが表示されています。
未認証のLoginページでも、認証後のページでも表示されていますので、問題の回避ができたようです。
しかしここにも一つ問題があります。Debug Kitプラグインが更新されたらこの修正が破棄されてしまうことです。この点は、その時に今回と同様の対処すれば何とかなりますので、気にしないことにしましょう。

先ほどのヒントを頂いたページでは、「Debug Kitプラグインを更新してみたら?」とか、「loadPlugin構成をしてみたら?」とありましたが、筆者の行った範囲では問題の回避ができませんでした。
筆者はCakePHPのRoute機構を正確に理解しているわけではありません。対応方法が間違っていたのかもしれません。
どなたか正しい対処方法をご指導いただければ幸いです。