CakePHP 4 で Authentication を Application.php の肥大化を防ぎつつ導入する方法

はじめに

以前「CakePHP 4 で Authentication プラグインを使ってユーザー認証を実装」で、CakePHP 4 でのユーザー認証の実装方法をご紹介しました。

その際は公式ドキュメントを参考にしつつ実装を進めたのですが、
Application.php が use や implements などの追加で、ちょっと大きくなっています。

そこで今日は CakePHP 4 で Application.php の肥大化を防ぎながら Authentication プラグインを導入する方法をご紹介します。

cakephp/app
4.1.2
cakephp/cakephp
4.1.5
cakephp/authentication
2.0.0
目次
  1. 下準備
  2. Authentication プラグイン導入
  3. ログイン と ログアウト
  4. ホームにユーザー情報表示
  5. おわりに

1. 下準備

動作確認に使用した users テーブルの SQL です。

ユーザー名: member
パスワード: p@ssw0rd
CREATE TABLE users (
  id int(10) UNSIGNED NOT NULL,
  username varchar(255) NOT NULL,
  password varchar(255) NOT NULL
);

ALTER TABLE users
  ADD PRIMARY KEY (id),
  MODIFY id int(10) UNSIGNED NOT NULL AUTO_INCREMENT;

INSERT INTO users
  (id, username, password)
VALUES
  (1, 'member', '$2y$10$chRR/dnRQgyJ4gVlscsIc.aiDsFs1QUT/.AiCfPf.Rru5LixtAfP6');

2. Authentication プラグイン導入

下記 composer コマンドで CakePHP Authentication プラグインをインストールします。

$ cd /path/to/cakephp4
$ composer require cakephp/authentication:^2.0

そしてここからがドキュメントの手順と異なる部分です。

まず独自のクラス AppAuthenticationServiceProvider を実装します。

/src/Authentication/AppAuthenticationServiceProvider.php
<?php
declare(strict_types=1);

namespace App\Authentication;

use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

class AppAuthenticationServiceProvider implements AuthenticationServiceProviderInterface
{
    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();
        $service->setConfig([
            'unauthenticatedRedirect' => Router::url(['controller' => 'users', 'action' => 'login']),
            'queryParam' => 'redirect',
        ]);
        $service->loadAuthenticator('Authentication.Session');
        $service->loadAuthenticator('Authentication.Form');
        $service->loadIdentifier('Authentication.Password');

        return $service;
    }
}

次に Application.php で Authentication のプラグインとミドルウェアを有効化します。そのミドルウェア追加の際に AppAuthenticationServiceProvider クラスのインスタンスを指定します。

/src/Application.php
...
// ↓ 追加
use App\Authentication\AppAuthenticationServiceProvider;
use Authentication\Middleware\AuthenticationMiddleware;
...
    public function bootstrap(): void
    {
        ...
        // ↓ 追加
        $this->addPlugin('Authentication');
    ...

    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            ...
            // ↓これを追加(直前の ->add(~); のセミコロン削除を忘れずに)
            ->add(new AuthenticationMiddleware(
                new AppAuthenticationServiceProvider()
            ));
    ...

最後に AppController.php に1行追記します。

/src/Controller/AppController.php
public function initialize(): void
{
    ...
    // ↓ これを追加
    $this->loadComponent('Authentication.Authentication');
}

ここまでについて補足です。

Application.php の new AuthenticationMiddleware() の引数には、下記いずれかの型のオブジェクトを入れます。

\Authentication\AuthenticationServiceInterface
\Authentication\AuthenticationServiceProviderInterface

これは下記ファイルのコンストラクタを見るとわかります。
/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php

公式ドキュメントのコードでは引数に $this を入れていますが、
その Application クラスを見ると imprements で
\Authentication\AuthenticationServiceProviderInterface が実装されているので、引数に入れる型と一致しています。

AuthenticationServiceProviderInterface について下記ソースコードをみると、
getAuthenticationService() 関数だけが必要であることが分かります。

/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php

そこで今回は Application.php への追記を減らすために、
AuthenticationServiceProviderInterface を implements した
AppAuthenticationServiceProvider クラスを作り、
そこに getAuthenticationService() を実装しました。

3. ログイン と ログアウト

UsersController に ログイン と ログアウト の機能を追加します。

/src/Controller/UsersController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class UsersController extends AppController
{
    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);
        $this->Authentication->allowUnauthenticated(['login', 'logout']);
    }

    public function login()
    {
        $result = $this->Authentication->getResult();
        // 認証成功
        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');
        }
    }

    public function logout()
    {
        $this->Authentication->logout();
        return $this->redirect(['action' => 'login']);
    }
}

テンプレは CakePHP 4 に採用されている CSS フレームワークの Milligram に対応しています。

/templates/Users/login.php
<form method="post">
    <fieldset>
        <label>Username</label>
        <input type="text" name="username">
        <label>Password</label>
        <input type="password" name="password">
        <input
            type="hidden" name="_csrfToken" autocomplete="off"
            value="<?= $this->request->getAttribute('csrfToken') ?>">
        <button class="button-primary" type="submit">Submit</button>
    </fieldset>
</form>

4. ホームにユーザー情報表示

ログイン後の遷移先として、ユーザー情報をダンプする画面を作りました。

/src/Controller/HomeController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class HomeController extends AppController
{
    public function index()
    {
        // ログインユーザー
        $user = $this->Authentication->getIdentity();
        debug($user);
        exit;
    }
}

これで完了です。
あとは /users/login にアクセスして動作を確認してみてください。

5. おわりに

個人的には /src/Application.php はなるべくコンパクトにしたいと思っています。

Application.php の役割はプラグインやミドルウェアなどの登録(有効化)に限定し、各機能の詳細は別ファイルに持たせることで、保守性がよくなると感じています。

今回は AppAuthenticationServiceProvider というクラス名で、
/src/Authentication に置いていますが、クラスやフォルダの名前は別のものでも差支えありません。
(その際は namespace の変更をお忘れなく)

様々な機能を追加すると Application.php は肥大化してきます。
肥大化してから分割するとコストが増えてしまいがちなので、早い段階から分割しておくのがオススメです。