条件

CakePHP4はPHP7.2以上が必要です。
今回はデータベースはSQLiteを使用するので、とりあえずPHP 7.2とComposerが入っていることを前提とします。

現時点で最新版であるCakePHP4を使用しますが、CakePHP3と比べてもこの記事の範囲内ではそんなに変わらないので、3の知識が必要な方も知っていただくには問題ないと思います。

インストール

Composerを使ってインストールしましょう。

$ composer self-update && composer create-project --prefer-dist cakephp/app:4.* blog

作成したディレクトリに移動して、一度ビルドインサーバーを起動して確認してみましょう。

$ cd blog
$ bin/cake server

次のメッセージが表示されるので、ブラウザで表示されたURLにアクセスしましょう。

built-in server is running in http://localhost:8765/

「Welcome to CakePHP」みたいな画面が表示されれば正常です。
「Database」の部分が赤くなっていると思いますが、これから設定するので問題ありません。

初期設定

configディレクトリの.env.example.envにリネームして、ローケールとタイムゾーンの変更をしましょう。
SECURITY_SALTにはランダムな文字列を入力します。

config/.env

export APP_NAME="MyBlog"
export DEBUG="true"
export APP_ENCODING="UTF-8"
export APP_DEFAULT_LOCALE="ja_JP"
export APP_DEFAULT_TIMEZONE="Asia/Tokyo"
export SECURITY_SALT="3mPhHebbrC9cTH7Kjg9MU5d_bXuBXcUUyGRbgJHe"

初期状態ではenvファイルは読み込まれないので、下記ファイルのコメントアウトされている部分を解除します。

config/bootstrap.php

if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
	$dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
	$dotenv->parse()
		->putenv()
		->toEnv()
		->toServer();
}

データベースの設定をします。
基本の設定はapp.phpを使いますが、ローカル開発の場合はapp_local.phpが優先されるようなのでこちらを設定します。

config/app_local.php

'Datasources' => [
'default' => [
	'driver' => Sqlite::class,
	'database' => ROOT . DS . 'database' . DS . 'product.sqlite',
],

envファイルを編集した場合は、ビルドインサーバーを起動している場合は一度Ctrl + cで停止して再起動します。
再度、先ほどアクセスしたURLにアクセスして、「Database」の部分がグリーンになっていることを確認してください。

データベースの作成

CakePHPではマイグレーションという機能でデータベース構造をソースコードで作成することができます。
下記コマンドを実行して記事用のテーブル用のマイグレーションファイルを作成します。

$ bin/cake bake migration CreatePosts

config/Migrationsxxxxx_CreatePosts(xxxはタイムスタンプ)ファイルが作成されているので、changeメソッドを下記のように編集します。

config/Migrations/xxxxx_CreatePosts

public function change()
{
    $table = $this->table('posts');
    $table->addColumn('title', 'string', [
            'limit' => 150,
            'null' => false,
        ])
        ->addColumn('description', 'text', [
            'limit' => 255,
        ])
        ->addColumn('body', 'text')
        ->addColumn('published', 'boolean', [
            'default' => false,
        ])
        ->addColumn('created', 'datetime')
        ->addColumn('modified', 'datetime')
        ->create();
}

次のコマンドを実行するとマイグレーションが実行されデータベースにテーブルが作成されます。

$ bin/cake migrations migrate

その他よく使いそうなマイグレーションコマンドは下記のようなものがあります。

migrations migrate 最新まで実行
migrations migrate -t マイグレーションID 指定したマイグレーションIDまで実行
migrations rollback 1つ前に戻す
migrations rollback -t マイグレーションID 指定したマイグレーションIDまで戻す
migrations status 現在の状態を確認する

管理画面ファイルをBakeで作成

先ほどマイグレーションファイルをbakeで作成しましたが、bakeにはその他のファイルを作成する機能があります。
必要なファイルはModelControllerView(template)です。
次のコマンドで生成することができます。

$ bin/cake bake model posts
$ bin/cake bake controller posts --prefix admin
$ bin/cake bake template posts --prefix admin

実はbakeにはallコマンドがあり、これを実行すれば全てのファイルを生成できるのですが、今回はデータを編集するのは管理画面だけにしたいので、コントローラーとテンプレートには--prefix adminを付けて、Adminディレクトリに生成するようにしてます。

バリデーションの設定

基本的にはそのままで問題ないですが、初期状態では入力項目がすべて未入力でも投稿できてしまうので、バリデーションの設定を変更しましょう。

タイトルを入力必須にしたいのでallowEmptyStringnotEmptyに変更します。
その他のバリデーションも必要なら設定しておきましょう。

src/Model/Table/PostsTable.php

public function validationDefault(Validator $validator): Validator
{
    $validator
        ->integer('id')
        ->allowEmptyString('id', null, 'create');

    $validator
        ->scalar('title')
        ->notEmpty('title', 'タイトルは必ず入力してください')
        ->maxLength('title', 150, '150文字以上で入力してください。')
        ->minLength('title', 5, '5文字以上で入力してください。');

    $validator
        ->scalar('description')
        ->maxLength('description', 255, '150文字以上で入力してください。')
        ->allowEmptyString('description');

    $validator
        ->scalar('body')
        ->allowEmptyString('body');

    $validator
        ->boolean('published')
        ->notEmptyString('published');

    return $validator;
}

管理画面(admin)のルーターの設定

CakePHPはコントローラーディレクトリにファイルを入れればルーターの設定は必要ないのですが、今回はAdminディレクトリの中に入れて、adminパスでアクセスしたいので設定する必要があります。

useでRouterを使えるようにしたら、$routes->scope('/')の中にadminの部分を追記します。
このように記述すればひとつひとつ記述する必要がなく、Adminディレクトリに入れたコントローラー全てにアクセスすることができます。
それと、/adminにアクセスした時はAdmin\PostsControllerindexが表示されるように追加します。

config/routes.php

use Cake\Routing\Router;
//...
$routes->scope('/', function (RouteBuilder $builder) {
    //...
    Router::prefix('admin', function ($routes) {
		$routes->fallbacks('DashedRoute');
		$routes->connect('/', ['controller' => 'Posts', 'action' => 'index']);
    });
});

この段階でCRUD機能が使えるはずなので、ブラウザでhttp://localhost:8765/admin/postsにアクセスしてデータが追加や削除ができるか確認してみましょう。

一般ユーザー用ファイルを作成

管理画面の機能が作成できたので今度は一般ユーザーが閲覧する画面を作成します。
コントローラーは同じようにbakeで作ります。
今度は/postsでアクセスするようにしたいので、prefixは必要ないですね。

$ bin/cake bake controller posts

一般ユーザーが閲覧できるのは公開されている記事(Publishedが1)だけにしたいので、下記のように編集します。
同時にデータの編集は機能は必要ないので、indexとshow以外は削除しておきましょう。

src/Controller/PostsController.php

class PostsController extends AppController
{
	public $paginate = [
        'limit' => 10,
        'order' => [
            'Posts.created' => 'desc'
        ]
	];
	
    public function index()
    {
        $posts = $this->paginate($this->Posts->findByPublished(1));
        $this->set(compact('posts'));
    }

    public function view($id = null)
    {
        $post = $this->Posts->get($id, [
            'conditions' => ['published' => 1]
		]);
		
        $this->set('post', $post);
    }
}

テンプレート(View)の作成

次はフロントのテンプレートを作成します。
管理画面はBakeで作りましたが、フロントは一般ユーザーが閲覧する部分独自のレイアウトを組む事が多いと思いますので手動で作成しましょう。
index.phpが一覧画面でview.phpが詳細画面です。

templates/Posts/index.php

<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\Post[]|\Cake\Collection\CollectionInterface $posts
 */
?>
<div class="content">
    <?php foreach ($posts as $post): ?>
        <p>投稿日:<time><?= h($post->created->i18nFormat('YYYY/MM/dd HH:mm:ss')) ?></time></p>
        <h3 style="margin-bottom:0"><?= h($post->title) ?></h3>
        <?= $this->Text->autoParagraph(h($post->description)); ?>
        <br>
        <?= $this->Html->link('記事を読む', ['action' => 'view', $post->id], ['class' => 'button']) ?>
        <hr>
    <?php endforeach; ?>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< 最初') ?>
            <?= $this->Paginator->prev('< 前へ') ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next('次へ >') ?>
            <?= $this->Paginator->last('最後 >>') ?>
        </ul>
    </div>
</div>

templates/Posts/view.php

<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\Post $post
 */
?>
<div class="posts view content">
	<?= h($post->created->i18nFormat('YYYY/MM/dd HH:mm:ss')) ?>
	<h2><?= h($post->title) ?></h2>
	<?= $this->Text->autoParagraph(h($post->body)); ?>
	<hr>
	<?= $this->Html->link('一覧へ戻る', ['action' => 'index'], ['class' => 'button']) ?>
</div>

フロントのトップページも作成したPostsControllerindexが表示されるように変更します。

config/routes.php

$builder->connect('/', ['controller' => 'Posts', 'action' => 'index']);

ユーザー管理機能の作成

現在の状態だと管理画面には誰でもアクセスできるので、誰でも編集できてしまいます。
認証機能を作って、ログインしないと編集できないようにしましょう。

CakePHPで認証機能を使用するにはユーザー管理機能が必要になります。
記事の管理機能と同じようにユーザーのデータベーステーブルから作ります。
bakeでマイグレーションファイルを生成します。

$ bin/cake bake migration CreateUsers

config/Migrationsディレクトリにファイルが作成されているので、changeメソッドを下記のように編集します。

config/Migrations/xxxxx_CreateUsers

public function change()
{
    $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();
}

CakePHPのログイン機能はデフォルトで「username」「password」カラムが使われるので、この2つを使うと後の設定が楽になります。

migrateを実行してテーブルを作成します。

$ bin/cake migrations migrate

管理画面用のその他のファイルも同じようにbakeします。

$ bin/cake bake model users
$ bin/cake bake controller users --prefix admin
$ bin/cake bake template users --prefix admin

ログインパスワードは平文で保存するのはよろしくないので、ハッシュ化されるようにします。
Userエンティティに下記のメソッドを追記しましょう。

src/Model/Entity/User.php

namespace App\Model\Entity;

	use Cake\ORM\Entity;
	use Cake\Auth\DefaultPasswordHasher;

	class User extends Entity
	{
		protected $_accessible = [
			'username' => true,
			'password' => true,
			'created' => true,
			'modified' => true,
		];

		protected $_hidden = [
			'password',
		];
	
		protected function _setPassword(string $password) : ?string
		{
			if (strlen($password) > 0) {
				return (new DefaultPasswordHasher())->hash($password);
			}
		}
}

ブラウザで/admin/users/addにアクセスして、試しにユーザーを追加してみましょう。
パスワードが入力した文字列ではなくランダムな文字列で保存されたはずです。

ユーザーバリデーションの設定

投稿でも設定しましたが、ユーザーも未入力の場合は追加されないようにバリデーションを変更しましょう。

src/Model/Table/UsersTable.php

public function validationDefault(Validator $validator): Validator
	{
	$validator
		->integer('id')
		->allowEmptyString('id', null, 'create');

	$validator
		->scalar('username')
		->notEmpty('username', 'ユーザー名は必ず入力してください')
		->maxLength('username', 50);

	$validator
		->scalar('password')
		->notEmpty('password', 'パスワードは必ず入力してください')
		->maxLength('password', 255);

	return $validator;
}

ユーザー認証機能の実装

CakePHPの認証機能はAuthコンポーネントが広く使用されていましたが、CakePHP4からは非推奨になりました。今後はauthorizationauthentication プラグインを使用することが推奨されています。

ここでは authentication プラグインを使用して、認証機能を実装します。
ターミナルで下記を実行してプラグインをインストールしましょう。

$ composer require cakephp/authentication:^2.0

src/Application.php次のように編集します。

src/Application.php

//...
// useを追加
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
	public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            // ...
            ->add(new RoutingMiddleware($this))
            ->add(new AuthenticationMiddleware($this)); // 追加

        return $middlewareQueue;
	}
	// ...

	// 下記を追加
	public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        // ログイン必須ページにアクセスしたときのリダイレクト先
        $authenticationService = new AuthenticationService([
            'unauthenticatedRedirect' => '/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' => '/admin/users/login',
        ]);

        return $authenticationService;
    }
}

この状態でコントローラーにコンポーネントをロードすると使えるようになります。
管理画面の全てのコントローラーをログイン必須にしたいので、管理画面用のベースコントローラーを作り、各コントローラーで継承します。

src/Controller/Admin/AdminController.php

namespace App\Controller\Admin;

use Cake\Controller\Controller;

class AdminController extends Controller
{
	public function initialize(): void
	{
		parent::initialize();

		$this->loadComponent('RequestHandler');
		$this->loadComponent('Flash');
		$this->loadComponent('Authentication.Authentication'); // 追加
	}
}

Admin/PostsController.phpAdmin/UsersController.phpAppControllerの部分をAdminControllerに変更します。

src/Controller/Admin/PostsController.php

use App\Controller\Admin\AdminController;

	class PostsController extends AdminController
	{
		// ...
	}

UsersControllerAdminController継承の他に、ログインとログアウトアクションを作ります。

src/Controller/Admin/UsersController.php

use App\Controller\Admin\AdminController;

class PostsController extends AdminController
{
	public function beforeFilter(\Cake\Event\EventInterface $event)
	{
		parent::beforeFilter($event);
		// ログインページは認証しなくてもアクセスできる
		$this->Authentication->addUnauthenticatedActions(['login']);
	}

	// ...

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

	public function logout()
	{
		$result = $this->Authentication->getResult();
		// ログインした場合はリダイレクト
		if ($result->isValid()) {
			$this->Authentication->logout();
			return $this->redirect(['controller' => 'Users', 'action' => 'login']);
		}
	}
}

これで管理画面すべてのページで認証が必要になりました。
adminなどにアクセスして/admin/users/loginにリダイレクトされることを確認してください。

管理画面用レイアウトの作成

レイアウトはフロントと管理画面と使用目的が違うのでレイアウト異なることがほとんどだと思います。
管理画面専用のレイアウトを作成しましょう。

templates/Adminlayoutディレクトリを作りdefault.phpファイルを作るだけで自動的に読んでくれます。

templates/Admin/layout/default.php

<!DOCTYPE html>
<html>
<head>
	<?= $this->Html->charset() ?>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Controle Panel</title>
	<?= $this->Html->meta('icon') ?>
	<link href="https://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet">
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
	<?= $this->Html->css('milligram.min.css') ?>
	<?= $this->Html->css('cake.css') ?>
	<?= $this->fetch('meta') ?>
	<?= $this->fetch('css') ?>
	<?= $this->fetch('script') ?>
</head>
<body>
	<nav class="top-nav">
		<div class="top-nav-title">
			<a href="/admin/">Controle Panel</a>
		</div>
		<div class="top-nav-links">
			<a href="/admin/users/logout">ログアウト</a>
		</div>
	</nav>
	<main class="main">
		<div class="container">
			<?= $this->Flash->render() ?>
			<?= $this->fetch('content') ?>
		</div>
	</main>
	<footer>
	</footer>
</body>
</html>

言語ファイルの作成

bakeで生成したファイルは多言語対応したViewファイルで生成されます。
言語ファイルがない状態だと英語表記なのでこれを日本語で表示されるようにしてみましょう。

多言語設定が必要な箇所はViewファイルで<?= __('Actions') ?>のように、アンダースコア2つと括弧で記述してある部分です。
一つ一つ抜き出すのは大変なのでこれもbakeで一括で抜き出してファイルを生成しましょう。

ターミナルで下記を実行します。

$ bin/cake i18n

次のメセージが表示されます。

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

ソースを元に言語ファイルを作成する場合は、「e」を入力しエンターで実行しましょう。

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

元になるソースディレクトリパスを聞かれます。正しかったらそのままエンターします。

次はコアファイルからも抽出するかということですので、デフォルトのnでエンター。

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

最後に出力するディレクトリパスが表示されます。ここも基本的にそのままでエンターを実行します。

What is the path you would like to output?

最初の質問に戻るので、「q」で抜けます。

CakePHP4はデフォルトでは、resources/locales/default.potにファイルが生成されます。
このファイルをコピーして、どう階層にあらたにja_JPというディレクトリを作成して、ペーストします。その際ファイル名をdefault.poにリネームしてください。

次のような階層になります。

resources
├─ locales
│   ├─ locales
│   │   ├─ default.pot
│   │   └─ ja_JP
│   │        └─ default.po

default.poを編集します。
poファイルはmsgidmsgstrがセットで記述してあります。
msgstrが空欄になっているので、翻訳後の文字列を記述していきましょう。
たとえばTitleなら次のように記述します。

resources/locales/ja_JP/default.po

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

文字がかわらない場合

言語ファイルはキャッシュされるので、更新しても切り替わらないことがあります。
下記ディレクトリにあるキャッシュファイルを削除して再度確認してみましょう。

/tmp/cache/persistent/

CRUDの実装や、管理画面とフロントの分け方、認証機能と最低限の機能を実装することができたところで、今回は以上になります。
今後もう少し機能を追加してCakePHPの使い方を解説できたらなと思います。

YoutubeでもCakePHPやってます。ぜひ!

この記事のソースコード
CakePHPBlog
参考サイト
チュートリアルと例 – 4.x