初心者用CakePHPでブログサイト⑤管理画面・CRUD機能・ログイン機能の作成

初心者用CakePHPでブログサイト⑤管理画面・CRUD機能・ログイン機能の作成

190 回閲覧されました

みなさんこんにちは、jonioです。

今回は管理画面を作成してCRUDができるようにしてログイン機能を作成します。

まずは管理画面を作成します。

管理画面用のコントローラーの作成

管理画面用のコントローラーの作成する為に下記のコマンドを叩きます。

bin/cake bake controller posts --prefix admin 

「–prefix admin」と書くことで「CakePHPのプロジェクト > src > Controller > Admin」の下の階層に「PostsController.php」が作成されます。

Admin」は「–prefix admin」の「admin」のことです。

次は管理画面用のビューを作成します。

管理画面用のビューの作成

下記のコマンドを叩きます。

bin/cake bake template posts --prefix admin

posts –prefix admin」と書くことで「CakePHPのプロジェクト > templates > Admin > Posts」の下の階層に「edit.php」・「add.php」・「view.php」・「index.php」が作成されます。(背景色で対応させて下さい)

今の状態ではまだコントローラーが認識されていないので認識をさせます。

コントローラーの認識

「CakePHPのプロジェクト > config > routes.php」のコードを下記にします。

<?php
/**
 * Routes configuration.
 *
 * In this file, you set up routes to your controllers and their actions.
 * Routes are very important mechanism that allows you to freely connect
 * different URLs to chosen controllers and their actions (functions).
 *
 * It's loaded within the context of `Application::routes()` method which
 * receives a `RouteBuilder` instance `$routes` as method argument.
 *
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */

use Cake\Routing\Route\DashedRoute;
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;                    //この行を追加

/*
 * The default class to use for all routes
 *
 * The following route classes are supplied with CakePHP and are appropriate
 * to set as the default:
 *
 * - Route
 * - InflectedRoute
 * - DashedRoute
 *
 * If no call is made to `Router::defaultRouteClass()`, the class used is
 * `Route` (`Cake\Routing\Route\Route`)
 *
 * Note that `Route` does not do any inflections on URLs which will result in
 * inconsistently cased URLs when used with `:plugin`, `:controller` and
 * `:action` markers.
 */
/** @var \Cake\Routing\RouteBuilder $routes */
$routes->setRouteClass(DashedRoute::class);

$routes->scope('/', function (RouteBuilder $builder) {
    /*
     * Here, we are connecting '/' (base path) to a controller called 'Pages',
     * its action called 'display', and we pass a param to select the view file
     * to use (in this case, templates/Pages/home.php)...
     */
    $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);

    /*
     * ...and connect the rest of 'Pages' controller's URLs.
     */
    $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);


    //ここから追加
    $builder->prefix('admin', function($routes) {
      $routes->fallbacks('DashedRoute');
    });
    //ここまで追加


//以下の行は変更がない為省略

これで「http://localhost:8888/cakephp/CakeBlog1/admin/posts/index」にアクセスすると下記の表示になります。

画面右上の「NEW POST」から記事の作成ができます。

画面の「Actions」の項目の「View」・「Edit」・「Delete」の項目も動作しますので確認してください。

私はこの画面をchromeで確認しましたが「NEW POST」から記事を作成しようとしたら「Missing or invalid CSRF cookie.」とエラーが表示されました。

chromeの設定からcookieを削除すればエラーがなくなりますので万が一同じエラーが出たら試してみて下さい。

管理画面・トップページのURLを変更

まずは管理画面のURLを変更します。

「CakePHPのプロジェクト > config > routes.php」のコードを下記に修正します。(変更がある部分のみ掲載します)

$builder->prefix('admin', function($routes) {
  $routes->connect('/', ['controller' => 'Posts', 'action' => 'index']);   //この行を追加

  $routes->fallbacks('DashedRoute');
});

2行目の「Posts」はコントローラー名で「index」はメソッド名です、さっき作成した奴です。

これで「http://localhost:8888/cakephp/CakeBlog1/admin」にアクセスすると管理画面になります。

次はトップページのURLを変更します。

今の状態だと記事一覧を表示するURLは「http://localhost:8888/cakephp/CakeBlog1/posts/index」でしたが「http://localhost:8888/cakephp/CakeBlog1/」(トップページのURL)に変更します。

これもroutes.phpを修正すればいいです、下記に変更します。

$routes->scope('/', function (RouteBuilder $builder) {
    /*
     * Here, we are connecting '/' (base path) to a controller called 'Pages',
     * its action called 'display', and we pass a param to select the view file
     * to use (in this case, templates/Pages/home.php)...
     */
    $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);   //この行は削除  
    $builder->connect('/', ['controller' => 'Posts', 'action' => 'index']); ;      //この行に変更

    /*
     * ...and connect the rest of 'Pages' controller's URLs.
     */
    $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);

これで「http://localhost:8888/cakephp/CakeBlog1/」にアクセスすると記事一覧が表示されます。

ログインした人のみ閲覧できるページの作成

今の状態は誰でも管理画面にアクセスできてCRUD(記事の投稿・編集・削除)を実行できますがログインに通った人だけができるようにします。

CakPHPのチュートリアルを使っています。

ユーザー登録用のテーブルを作成

まず認証用のプラグインをインストールする為に下記のコマンドを叩きます。

composer require cakephp/authentication:^2.0

そしてユーザーを登録するテーブルを作成しますがまずはマイグレーションファイルの作成です。

下記のコマンドを叩きます。

bin/cake bake migration CreateUsers

マイグレーションファイルに下記の記述をします。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateUsers extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('users');
        
        
        //ここから追加
        $table->addColumn('username', 'string', [
                    'default' => null,
                    'limit' => 50,
                    'null' => false
                ])
                ->addColumn('password', 'string', [
                    'default' => null,
                    'limit' => 255,
                    'null' => false
                ])
                ->addColumn('created', 'datetime')
                ->addColumn('modified', 'datetime')
                ->create();
        //ここまで追加
        
        
    }
}

ユーザー名は「username」カラムとしパスワードは「password」カラムとします。

そしてusersテーブルに挿入するデータの作成をします。

「CakePHPのプロジェクト > config > Seeds」の下の階層に「UsersSeed.php」を作成して中身に下記の記述をします。

<?php
declare(strict_types=1);

use Migrations\AbstractSeed;
use Authentication\PasswordHasher\DefaultPasswordHasher;         //この行を追加

/**
 * Users seed.
 */
class UsersSeed extends AbstractSeed
{
    /**
     * Run Method.
     *
     * Write your database seeder using this method.
     *
     * More information on writing seeds is available here:
     * https://book.cakephp.org/phinx/0/en/seeding.html
     *
     * @return void
     */
    public function run(): void
    {
        $data = [
            [
                'username' => 'admin',
                'password' => $this->_setPassword('admin'),
                'created' => '2020-05-02 10:00:00',
                'modified' => '2020-05-02 10:00:00',
            ],            
            [
                'username' => 'yamada',
                'password' => $this->_setPassword('yamada'),
                'created' => '2020-05-02 10:00:00',
                'modified' => '2020-05-02 10:00:00',
            ],            
        ];

        $table = $this->table('users');
        $table->insert($data)->save();
    }


    //ここから追加
    protected function _setPassword(string $password) : ?string
    {
        if (strlen($password) > 0) {
            return (new DefaultPasswordHasher())->hash($password);
        }
    }
    //ここまで追加


}

PostsSeed.phpに記述した時に5行目・45行目〜50行目はなかったのですが今回は必要なので記述しています。

ログインに使うパスワードをハッシュ化(ランダムな文字列にして元々のパスワードが何か分からなくする)するのに必要です。

実際のパスワードのハッシュ化は27行目・33行目のvalueで行っています。

それではusersテーブルを作成してSeederのデータをテーブルに挿入する為に下記のコマンドを叩きます。

bin/cake migrations migrate
bin/cake migrations seed --seed UsersSeed

これでusersテーブルにSeederのデータが入った状態になりました。

次はモデルを使ってビューにユーザー一覧を表示します。

ユーザー用のモデルの作成

モデルを作成する為に下記のコマンドを叩きます。

bin/cake bake model users

「CakePHPのプロジェクト > src > Model > Entity > User.php」にパスワードのハッシュ化をする為の記述をします。

<?php
declare(strict_types=1);

namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher;          //この行を追加
use Cake\ORM\Entity;

/**
 * User Entity
 *
 * @property int $id
 * @property string $username
 * @property string $password
 * @property \Cake\I18n\FrozenTime $created
 * @property \Cake\I18n\FrozenTime $modified
 */
class User extends Entity
{
    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * Note that when '*' is set to true, this allows all unspecified fields to
     * be mass assigned. For security purposes, it is advised to set '*' to false
     * (or remove it), and explicitly make individual fields accessible as needed.
     *
     * @var array<string, bool>
     */
    protected $_accessible = [
        'username' => true,
        'password' => true,
        'created' => true,
        'modified' => true,
    ];

    /**
     * Fields that are excluded from JSON versions of the entity.
     *
     * @var array<string>
     */
    protected $_hidden = [
        'password',
    ];


    //ここから追加
    protected function _setPassword(string $password) : ?string
    {
        if (strlen($password) > 0) {
            return (new DefaultPasswordHasher())->hash($password);
        }
    }
    //ここまで追加

    
}

ユーザー一覧の表示

次はユーザーを表示するのに使うコントローラーを作成する為に下記のコマンドを叩きます。

bin/cake bake controller users --prefix admin

そしてビューを作成する為に下記のコマンドを叩きます。

bin/cake bake template users --prefix admin

そしてビューに追記します。

「CakePHPのプロジェクト > templates > Admin > Users > index.php」を下記に修正します。

<?php
/**
 * @var \App\View\AppView $this
 * @var iterable<\App\Model\Entity\User> $users
 */
?>
<div class="users index content">
    <?= $this->Html->link(__('New User'), ['action' => 'add'], ['class' => 'button float-right']) ?>
    <h3><?= __('Users') ?></h3>
    <div class="table-responsive">
        <table>
            <thead>
                <tr>
                    <th><?= $this->Paginator->sort('id') ?></th>
                    <th><?= $this->Paginator->sort('username') ?></th>
                    <th><?= $this->Paginator->sort('password') ?></th>      //この行を追加
                    <th><?= $this->Paginator->sort('created') ?></th>
                    <th><?= $this->Paginator->sort('modified') ?></th>
                    <th class="actions"><?= __('Actions') ?></th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($users as $user): ?>
                <tr>
                    <td><?= $this->Number->format($user->id) ?></td>
                    <td><?= h($user->username) ?></td>
                    <td><?= h($user->password) ?></td>         //この行を追加
                    <td><?= h($user->created) ?></td>
                    <td><?= h($user->modified) ?></td>
                    <td class="actions">
                        <?= $this->Html->link(__('View'), ['action' => 'view', $user->id]) ?>
                        <?= $this->Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?>
                        <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?>
                    </td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< ' . __('first')) ?>
            <?= $this->Paginator->prev('< ' . __('previous')) ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next(__('next') . ' >') ?>
            <?= $this->Paginator->last(__('last') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
    </div>
</div>

これで「http://localhost:8888/cakephp/CakeBlog1/admin/users」にアクセスするとユーザー一覧が表示されパスワードがランダムな文字列になっているのが確認できます(下記赤枠)。

ログイン機能の作成

「CakePHPのプロジェクト > src > Application.php」に追記します。

<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link      https://cakephp.org CakePHP(tm) Project
 * @since     3.3.0
 * @license   https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App;

use Cake\Core\Configure;
use Cake\Core\Exception\MissingPluginException;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\Middleware\BodyParserMiddleware;
use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;


//ここから追加
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;
//ここまで追加


/**
 * Application setup class.
 *
 * This defines the bootstrapping logic and middleware layers you
 * want to use in your application.
 */
class Application extends BaseApplication implements AuthenticationServiceProviderInterface     //この行の「BaseApplication」のあとに「implements AuthenticationServiceProviderInterface」を追加している
{
    /**
     * Load all the application configuration and bootstrap logic.
     *
     * @return void
     */
    public function bootstrap(): void
    {
        // Call parent to load bootstrap from files.
        parent::bootstrap();

        if (PHP_SAPI === 'cli') {
            $this->bootstrapCli();
        }

        /*
         * Only try to load DebugKit in development mode
         * Debug Kit should not be installed on a production system
         */
        if (Configure::read('debug')) {
            $this->addPlugin('DebugKit');
        }

        // Load more plugins here
    }

    /**
     * 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
    {
        $middlewareQueue
            // Catch any exceptions in the lower layers,
            // and make an error page/response
            ->add(new ErrorHandlerMiddleware(Configure::read('Error')))

            // Handle plugin/theme assets like CakePHP normally does.
            ->add(new AssetMiddleware([
                'cacheTime' => Configure::read('Asset.cacheTime'),
            ]))

            // Add routing middleware.
            // If you have a large number of routes connected, turning on routes
            // caching in production could improve performance. For that when
            // creating the middleware instance specify the cache config name by
            // using it's second constructor argument:
            // `new RoutingMiddleware($this, '_cake_routes_')`
            ->add(new RoutingMiddleware($this))

            // RoutingMiddleware の後に認証を追加
            ->add(new AuthenticationMiddleware($this))              //この行を追加

            // Parse various types of encoded request bodies so that they are
            // available as array through $request->getData()
            // https://book.cakephp.org/4/en/controllers/middleware.html#body-parser-middleware
            ->add(new BodyParserMiddleware())

            // Cross Site Request Forgery (CSRF) Protection Middleware
            // https://book.cakephp.org/4/en/controllers/middleware.html#cross-site-request-forgery-csrf-middleware
            ->add(new CsrfProtectionMiddleware([
                'httponly' => true,
            ]));

        return $middlewareQueue;
    }


    //ここから追加
    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $authenticationService = new AuthenticationService([
            'unauthenticatedRedirect' => Router::url('/admin/users/login'),   //ログイン必須のページでログインしていない場合のリダイレクト先
            'queryParam' => 'redirect',
        ]);

        // identifiers を読み込み、username と password のフィールドを確認します
        $authenticationService->loadIdentifier('Authentication.Password', [
            'fields' => [
                'username' => 'username',
                'password' => 'password',
            ]
        ]);

        //  authenticatorsをロードしたら, 最初にセッションが必要です
        $authenticationService->loadAuthenticator('Authentication.Session');
        // 入力した username と password をチェックする為のフォームデータを設定します
        $authenticationService->loadAuthenticator('Authentication.Form', [
            'fields' => [
                'username' => 'username',
                'password' => 'password',
            ],
            'loginUrl' => Router::url('/admin/users/login'),
        ]);

        return $authenticationService;
    }
    //ここまで追加


    /**
     * Bootrapping for CLI application.
     *
     * That is when running commands.
     *
     * @return void
     */
    protected function bootstrapCli(): void
    {
        try {
            $this->addPlugin('Bake');
        } catch (MissingPluginException $e) {
            // Do not halt if the plugin is missing
        }

        $this->addPlugin('Migrations');

        // Load more plugins here
    }
}

「CakePHPのプロジェクト > src > Controller > Admin」の下の階層に「AdminController.php」を作成して中身を下記にします。

<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link      https://cakephp.org CakePHP(tm) Project
 * @since     0.2.9
 * @license   https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App\Controller\Admin;

use Cake\Controller\Controller;

/**
 * Application Controller
 *
 * Add your application-wide methods in the class below, your controllers
 * will inherit them.
 *
 * @link https://book.cakephp.org/4/en/controllers.html#the-app-controller
 */
class AdminController extends Controller
{
    /**
     * Initialization hook method.
     *
     * Use this method to add common initialization code like loading components.
     *
     * e.g. `$this->loadComponent('FormProtection');`
     *
     * @return void
     */
    public function initialize(): void
    {
        parent::initialize();

        $this->loadComponent('RequestHandler');
        $this->loadComponent('Flash');
        // 認証結果を確認し、サイトのロックを行うために次の行を追加します
        $this->loadComponent('Authentication.Authentication');

        /*
         * Enable the following component for recommended CakePHP form protection settings.
         * see https://book.cakephp.org/4/en/controllers/components/form-protection.html
         */
        //$this->loadComponent('FormProtection');
    }
}

次はAdminコントローラーと同じ階層にあるPostsコントローラーを修正します。

<?php
declare(strict_types=1);

namespace App\Controller\Admin;

use App\Controller\Admin\AdminController;            //この行を修正

/**
 * Posts Controller
 *
 * @property \App\Model\Table\PostsTable $Posts
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class PostsController extends AdminController          //AppControllerをAdminControllerに変更


//下の行は変更がない為省略

またAdminコントローラーと同じ階層にあるUsersコントローラーも修正します。

<?php
declare(strict_types=1);

namespace App\Controller\Admin;

use App\Controller\Admin\AdminController;                    //この行を修正

/**
 * Users Controller
 *
 * @property \App\Model\Table\UsersTable $Users
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class UsersController extends AdminController         //AppControllerをAdminControllerに変更


//下の行は変更がない為省略

UsersController.phpに追記します。

<?php
declare(strict_types=1);

namespace App\Controller\Admin;

use App\Controller\Admin\AdminController;                  

/**
 * Users Controller
 *
 * @property \App\Model\Table\UsersTable $Users
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class UsersController extends AdminController             
{


    //ここから追加
    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);
  
        $this->Authentication->addUnauthenticatedActions(['login']);
    }
    //ここまで追加


    /**
     * Index method
     *
     * @return \Cake\Http\Response|null|void Renders view
     */
    public function index()
    {
        $users = $this->paginate($this->Users);

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

    /**
     * View method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Renders view
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => [],
        ]);

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

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $user = $this->Users->newEmptyEntity();
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Edit method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => [],
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Delete method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $user = $this->Users->get($id);
        if ($this->Users->delete($user)) {
            $this->Flash->success(__('The user has been deleted.'));
        } else {
            $this->Flash->error(__('The user could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }


    //ここから追加
    public function login()
    {
        $this->request->allowMethod(['get', 'post']);
        $result = $this->Authentication->getResult();
        // POST, GET を問わず、ユーザーがログインしている場合はリダイレクトします
        if ($result->isValid()) {
            return $this->redirect('/admin');
        }
        // ユーザーが submit 後、認証失敗した場合は、エラーを表示します
        if ($this->request->is('post') && !$result->isValid()) {
            $this->Flash->error('ユーザー名かパスワードが正しくありません。');
        }
    }
    //ここまで追加

    
}

19行目〜24行目で認証を必要としないアクションの設定ができます。

今はUsersコントローラーにbeforeFilterメソッドを記述しているのでUsersコントローラーに対して認証を必要としないアクションの設定ができます。

他のコントローラーにも認証を必要としないアクションの設定をしたいならその度にbeforeFilterメソッドの記述をします。

これでログイン機能はできたので次はログイン用のビューを作成します。

ログイン用のビュー

「CakePHPのプロジェクト > templates > Admin > Users」の下の階層にlogin.phpを作成します。

login.phpの中身を下記にします。

<div class="users form">
    <?= $this->Flash->render() ?>
    <?= $this->Form->create() ?>
    <fieldset>
        <?= $this->Form->control('username', ['required' => true]) ?>
        <?= $this->Form->control('password', ['required' => true]) ?>
    </fieldset>
    <?= $this->Form->submit('ログイン'); ?>
    <?= $this->Form->end() ?>
</div>

これでログインが必須な「http://localhost:8888/cakephp/CakeBlog1/admin/」にログインするとログインのページにリダイレクトします。

これでログイン機能ができましたが今の状態はログアウトの機能がないので作成します。

ログアウト機能

UsersController.phpに追記します。

<?php
declare(strict_types=1);

namespace App\Controller\Admin;

use App\Controller\Admin\AdminController;                  

/**
 * Users Controller
 *
 * @property \App\Model\Table\UsersTable $Users
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class UsersController extends AdminController             
{
    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);
        // 認証を必要としないログインアクションを構成し、
        // 無限リダイレクトループの問題を防ぎます
        $this->Authentication->addUnauthenticatedActions(['login']);
    }

    /**
     * Index method
     *
     * @return \Cake\Http\Response|null|void Renders view
     */
    public function index()
    {
        $users = $this->paginate($this->Users);

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

    /**
     * View method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Renders view
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => [],
        ]);

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

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $user = $this->Users->newEmptyEntity();
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Edit method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => [],
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Delete method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null|void Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $user = $this->Users->get($id);
        if ($this->Users->delete($user)) {
            $this->Flash->success(__('The user has been deleted.'));
        } else {
            $this->Flash->error(__('The user could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }

    public function login()
    {
        $this->request->allowMethod(['get', 'post']);
        $result = $this->Authentication->getResult();
        // POST, GET を問わず、ユーザーがログインしている場合はリダイレクトします
        if ($result->isValid()) {
            return $this->redirect('/admin');
        }
        // ユーザーが submit 後、認証失敗した場合は、エラーを表示します
        if ($this->request->is('post') && !$result->isValid()) {
            $this->Flash->error('ユーザー名かパスワードが正しくありません。');
        }
    }


    //ここから追加
    public function logout()
    {
        $result = $this->Authentication->getResult();
        // POST, GET を問わず、ユーザーがログインしている場合はリダイレクトします
        if ($result && $result->isValid()) {
            $this->Authentication->logout();
            return $this->redirect(['controller' => 'Users', 'action' => 'login']);
        }
    }
    //ここまで追加


}

ログアウト機能の記述はそのまま使いますがログアウトに遷移するボタンがないので作成します。

ログアウトのボタンは管理画面の全てのページにあった方がいいので管理画面専用のレイアウト作ってそこに記述します。

「CakePHPのプロジェクト > templates > Admin」の下の階層に「layoutフォルダ」を作成してその下の階層に「default.php」を作成して中身に下記の記述をします。

<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         0.10.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 * @var \App\View\AppView $this
 */

$cakeDescription = 'CakePHP: the rapid development php framework';
?>
<!DOCTYPE html>
<html>
<head>
    <?= $this->Html->charset() ?>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>管理画面</title>
    <?= $this->Html->meta('icon') ?>

    <link href="https://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet">

    <?= $this->Html->css(['normalize.min', 'milligram.min', 'cake']) ?>

    <?= $this->fetch('meta') ?>
    <?= $this->fetch('css') ?>
    <?= $this->fetch('script') ?>
</head>
<body>
    <nav class="top-nav">
        <div class="top-nav-title">
            <a href="/cakephp/CakeBlog1/admin">ControlPanel</a>
        </div>
        <div class="top-nav-links">
            <a href="/cakephp/CakeBlog1/admin/posts">投稿</a>
            <a href="/cakephp/CakeBlog1/admin/users">ユーザー</a>
            <a href="/cakephp/CakeBlog1/admin/users/logout">ログアウト</a>
        </div>
    </nav>
    <main class="main">
        <div class="container">
            <?= $this->Flash->render() ?>
            <?= $this->fetch('content') ?>
        </div>
    </main>
    <footer>
    </footer>
</body>
</html>

これで「http://localhost:8888/cakephp/CakeBlog1/admin」にアクセスすると下記の画面になり「ログアウト」ができるボタンが表示されるのを確認できます。

多言語化

今回の内容とは関係ないですがおまけで付けます。

現在のサイトは日本語ですが言語の切り替えができるようにします。

切り替えができるページの確認

「cakePHPのプロジェクト > templates > Posts > view.php」を開きます。

<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\Post $post
 */
?>
<div class="row">
    <aside class="column">
        <div class="side-nav">
            <h4 class="heading"><?= __('Actions') ?></h4>
            <?= $this->Html->link(__('Edit Post'), ['action' => 'edit', $post->id], ['class' => 'side-nav-item']) ?>
            <?= $this->Form->postLink(__('Delete Post'), ['action' => 'delete', $post->id], ['confirm' => __('Are you sure you want to delete # {0}?', $post->id), 'class' => 'side-nav-item']) ?>
            <?= $this->Html->link(__('List Posts'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
            <?= $this->Html->link(__('New Post'), ['action' => 'add'], ['class' => 'side-nav-item']) ?>
        </div>
    </aside>
    <div class="column-responsive column-80">
        <div class="posts view content">
            <h3><?= h($post->title) ?></h3>
            <table>
                <tr>
                    <th><?= __('Title') ?></th>
                    <td><?= h($post->title) ?></td>
                </tr>
                <tr>
                    <th><?= __('Id') ?></th>
                    <td><?= $this->Number->format($post->id) ?></td>
                </tr>
                <tr>
                    <th><?= __('Created') ?></th>
                    <td><?= h($post->created) ?></td>
                </tr>
                <tr>
                    <th><?= __('Modified') ?></th>
                    <td><?= h($post->modified) ?></td>
                </tr>
                <tr>
                    <th><?= __('Published') ?></th>
                    <td><?= $post->published ? __('Yes') : __('No'); ?></td>
                </tr>
            </table>
            <div class="text">
                <strong><?= __('Description') ?></strong>
                <blockquote>
                    <?= $this->Text->autoParagraph(h($post->description)); ?>
                </blockquote>
            </div>
            <div class="text">
                <strong><?= __('Body') ?></strong>
                <blockquote>
                    <?= $this->Text->autoParagraph(h($post->body)); ?>
                </blockquote>
            </div>
        </div>
    </div>
</div>

「__(‘ ‘)」となっている部分が全て多言語化できます。

多言語化の設定

下記のコマンドを叩きます。

bin/cake i18n

すると質問が複数出てくるので答えます。

[E]xtract POT file from sources
[I]nitialize a language from POT file
[H]elp
[Q]uit
What would you like to do? (E/I/H/Q) 

これは「E」にします。

Current paths: None
What is the path you would like to extract?
[Q]uit [D]one

「Enter」を押します。

Current paths: /Applications/MAMP/htdocs/cakephp/CakeBlog2/src/
What is the path you would like to extract?
[Q]uit [D]one

「Enter」を押します。

Current paths: /Applications/MAMP/htdocs/cakephp/CakeBlog2/src/, /Applications/MAMP/htdocs/cakephp/CakeBlog2/templates/
What is the path you would like to extract?
[Q]uit [D]one

「Enter」を押します。

Would you like to extract the messages from the CakePHP core? (y/n) 

「Enter」を押します。

What is the path you would like to output?
[Q]uit

「Enter」を押します。

What would you like to do? (E/I/H/Q) 

「Q」を押します。

そして「cakePHPのプロジェクト > resources > locales」の下の階層に「ja_JP」フォルダを作成します。

同じ階層にある「default.pot」をコピーしてja_JPフォルダの下の階層において「default.po」にリネームします。

default.poを開きます。

# LANGUAGE translation of CakePHP Application
# Copyright YEAR NAME <EMAIL@ADDRESS>
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"POT-Creation-Date: 2024-02-03 19:49+0900\n"
"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
"Last-Translator: NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: ./src/Controller/Admin/PostsController.php:55
#: ./src/Controller/Admin/PostsController.php:79
#: ./src/Controller/PostsController.php:58
#: ./src/Controller/PostsController.php:82
msgid "The post has been saved."
msgstr ""

22行目の「msgstr ” “」の「” “」に訳にあたる文字列を入れることで言語の切り替えができます。

試しに163行目の「” “」の中に適当な文字列を入れてみます。

#: ./templates/Admin/Posts/view.php:27
msgid "Title"
msgstr "Titleをタイトルに変更"

変更するページのURLは「http://localhost:8888/cakephp/CakeBlog1/admin」で上記のコードより「Titleカラム」が変更になるはずですがページの確認をしても変更されていません。

これはページのキャッシュが残っているからなのでキャッシュファイルを削除します。

キャッシュファイルは「cakePHPのプロジェクト > tmp > cache > persistent」の下層にある名前に「translations」が付いているやつです、これを全て削除します。

これで「Titleカラム」が切り替わっているのが確認できます。

他の項目も変更します、変更した部分のみを下記に記載します。

#: ./templates/Admin/Posts/view.php:12
msgid "Delete Post"
msgstr "投稿の削除"

#: ./templates/Admin/Posts/view.php:27
msgid "Title"
msgstr "タイトル"

#: ./templates/Admin/Posts/view.php:31
#: ./templates/Admin/Users/view.php:26
msgid "Id"
msgstr "ID"

#: ./templates/Admin/Posts/view.php:35
#: ./templates/Admin/Users/view.php:30
msgid "Created"
msgstr "登録日"

#: ./templates/Admin/Posts/view.php:39
#: ./templates/Admin/Users/view.php:34
msgid "Modified"
msgstr "編集日"

#: ./templates/Admin/Posts/view.php:43
msgid "Published"
msgstr "公開"

#: ./templates/Admin/Posts/view.php:44
msgid "Yes"
msgstr "はい"

#: ./templates/Admin/Posts/view.php:44
msgid "No"
msgstr "いいえ"

#: ./templates/Admin/Posts/view.php:48
msgid "Description"
msgstr "概要"

これで変更が適用されます。

次回の解説

次回は1対多アソシエーションと多対多アソシエーションの説明です。