CakePHPでCRUDアプリケーション

CakePHP4のチュートリアルを試す① ~CRUD操作~
この記事では、モデルのコントローラーを通したCRUD操作、ビューへの反映までを行います。
環境
- Windows 10 Home 22H2
- WSL2 Ubuntu 22.04.2 LTS
- Docker Desktop 4.19.0
- CakePHP 4.4.13
データベース作成
Dockerのdbコンテナに入りテーブルを作成します。
~/cakephp
yoyoyo@wsl:~/cakephp$ docker exec -it cakephp-db-1 bash
docker-composeで記述したcakephptest
が作成されています。
cakephp-db-1
bash-4.4# mysql -u cake -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| cakephptest |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.09 sec)
mysql> use cakephptest
Database changed
チュートリアルどおりにusers
テーブル、articles
テーブル、tags
テーブル、articles_tags
テーブルを作成し、初期値をINSERTしておきます。
データベース作成
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
created DATETIME,
modified DATETIME
);
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
slug VARCHAR(191) NOT NULL,
body TEXT,
published BOOLEAN DEFAULT FALSE,
created DATETIME,
modified DATETIME,
UNIQUE KEY (slug),
FOREIGN KEY user_key (user_id) REFERENCES users(id)
) CHARSET=utf8mb4;
CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(191),
created DATETIME,
modified DATETIME,
UNIQUE KEY (title)
) CHARSET=utf8mb4;
CREATE TABLE articles_tags (
article_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY tag_key(tag_id) REFERENCES tags(id),
FOREIGN KEY article_key(article_id) REFERENCES articles(id)
);
INSERT INTO users (email, password, created, modified)
VALUES
('cakephp@example.com', 'secret', NOW(), NOW());
INSERT INTO articles (user_id, title, slug, body, published, created, modified)
VALUES
(1, 'First Post', 'first-post', 'This is the first post.', 1, now(), now());
Dockerを使っていても、データベースをいじるには直接コンテナに入ってSQLを実行するというのはなんだかアナログな気がします。(もっとスマートな方法があったら教えてください。)
CakePHPのフォルダ構成
CakePHPをインストールした直後のフォルダ構成はこのようになっています。

src
にアプリケーションに必要となるプログラムを書いていきます。

モデルの作成
モデルはデータの読み書きを行います。MVCのMですね。
Phalconでいうmodels
のようですが、Behavior
とEntity
とTable
がありなんだか複雑そうです。
Tableの作成
ひとまずチュートリアルにしたがってsrc/Model/Table/ArticlesTable.php
を作成しました。
src/Model/Table/ArticlesTable.php
<?php
// src/Model/Table/ArticlesTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
class ArticlesTable extends Table
{
public function initialize(array $config) : void
{
$this->addBehavior('Timestamp');
}
}
Tableクラスの名前を複数形+パスカルケース+Table
にすることで、データベース上のテーブル名と紐づけられるようです。
主キーはid
であると解釈されます。これは設計がややこしくなりそう…
$this->addBehavior('Timestamp');
でcreated
とmodified
カラムが自動的に更新されます。
DATETIME型のカラムが対象なのかな。
Entityの作成
また、Entity
クラスの作成も行います。
src/Model/Entity/Article.php
<?php
// src/Model/Entity/Article.php
namespace App\Model\Entity;
use Cake\ORM\Entity;
class Article extends Entity
{
protected $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
];
}
_accessible
プロパティでカラムへの操作の権限を制御するようです。
テーブル全体への操作がTable
、レコードごとの操作がEntity
といった感じでしょうか。
リストページの作成
ここからは機能ごとにコントローラーとビューを作成していきます。
まずはモデルから記事一覧を取得して表示するページです。
コントローラー
参考: CMS チュートリアル – Articles コントローラーの作成
MVCのCです。CakePHPへのHTTPリクエストを処理するビジネスロジックを記述します。
src/Controller/ArticlesController.php
<?php
// src/Controller/ArticlesController.php
namespace App\Controller;
class ArticlesController extends AppController
{
public function index()
{
$this->loadComponent('Paginator');
$articles = $this->Paginator->paginate($this->Articles->find());
$this->set(compact('articles'));
}
}
/articles/indexへのアクセスがindex()
メソッドで処理されます。paginate()
でページネーションされた記事を取得し、set()
でビューに渡しています。
ビュー
MVCのV。CakePHPのビューは素のPHPファイルで記述するようです。
PhalconでいうVolt
にあたるテンプレートエンジンはなさそう。
場所はtemplates
以下。あれ…?src/View/
は…?
templates/Articles/index.php
<!-- File: templates/Articles/index.php -->
<h1>記事一覧</h1>
<table>
<tr>
<th>タイトル</th>
<th>作成日時</th>
</tr>
<!-- ここで、$articles クエリーオブジェクトを繰り返して、記事の情報を出力します -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
</td>
<td>
<?= $article->created->format(DATE_RFC850) ?>
</td>
</tr>
<?php endforeach; ?>
</table>
ArticlesController
から渡された$articles
変数が使えます。$this->Html
オブジェクトのlink()
メソッドで、article
テーブルのtitle
とslug
からリンク(HTMLのa要素)を生成している模様。
この時点で/articles/indexにアクセスすると、記事のリストが表示されるようになりました!

しかし、記事のリンクをクリックしても、対応するアクションが実装されていないためエラーになってしまいました。

先ほどのindex.phpの$this->Html->link($article->title, ['action' => 'view', $article->slug])
でview
アクションを指定していましたね。
ビューページの作成
それぞれの記事ごとのページです。
コントローラー
src/Controller/ArticlesController.php
にview()
メソッドを追加します。
src/Controller/ArticlesController.php
public function view($slug = null)
{
$article = $this->Articles->findBySlug($slug)->firstOrFail();
$this->set(compact('article'));
}
findBySlug()
でクエリーを書いているらしい。UNIQUEじゃなかった場合はどうなるのでしょうか。firstOrFail()
についても、どうやって最初と判断しているのか不思議。ORDER BY?
$slug
はCakePHPのルーティングとディスパッチャーから取得されます。
確かにリストページのURLも/articles/view/first-postとなっていました。
ビュー
templates/Articles/view.php
を追加します。
templates/Articles/view.php
<!-- File: templates/Articles/view.php -->
<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>作成日時: <?= $article->created->format(DATE_RFC850) ?></small></p>
<p><?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?></p>
ビューページも完成しました。

記事の追加ページ
新しい記事を作成するページを作ります。
コントローラー
src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEmptyEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// user_id の決め打ちは一時的なもので、あとで認証を構築する際に削除されます。
$article->user_id = 1;
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$this->set('article', $article);
}
データがPOSTされた場合、$this->request->getData()
でデータを取得して、save()
で保存します。
Flash
コンポーネントで、レイアウトの中にフラッシュメッセージを表示し、記事の追加に成功したか or 失敗したかをユーザーに知らせます。
ビュー
templates/Articles/add.php
<!-- File: templates/Articles/add.php -->
<h1>記事の追加</h1>
<?php
echo $this->Form->create($article);
// 今はユーザーを直接記述
echo $this->Form->control('user_id', ['type' => 'hidden', 'value' => 1]);
echo $this->Form->control('title');
echo $this->Form->control('body', ['rows' => '3']);
echo $this->Form->button(__('Save Article'));
echo $this->Form->end();
?>
$this->Form->create()
はいわゆるFormHelperですね。
Phalconはformsでクラスを定義しているので、ここは異なる点。
リストページにも記事の追加へのリンクを追加しておきます。
templates/Articles/index.php
<?= $this->Html->link('記事の追加', ['action' => 'add']) ?>

ところが追加に失敗してしまいます。

article->slugはNUT NULLでした。
モデルでNUT NULLチェック
src/Model/Table/ArticlesTable.php
にbeforeSave()
メソッドを追加します。
src/Model/Table/ArticlesTable.php
public function beforeSave(EventInterface $event, $entity, $options)
{
if ($entity->isNew() && !$entity->slug) {
$sluggedTitle = Text::slug($entity->title);
// スラグをスキーマで定義されている最大長に調整
$entity->slug = substr($sluggedTitle, 0, 191);
}
}
slug
が空であった場合、title
の値を登録するようにしています。
これで、記事の追加ができるようになりました。

Flash
コンポーネントの表示もされています。
記事の編集ページ
記事を編集するページを作ります。
コントローラー
src/Controller/ArticlesController.php
public function edit($slug)
{
$article = $this->Articles->findBySlug($slug)->firstOrFail();
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
$this->set('article', $article);
}
view
アクションと同じく、$slug
からfindBySlug()
とfirstOrFail()
で記事を取得します。
編集したデータのモデルへの反映はadd
アクションと同じですね。
ビュー
templates/Articles/edit.php
<!-- File: templates/Articles/edit.php -->
<h1>記事の編集</h1>
<?php
echo $this->Form->create($article);
echo $this->Form->control('user_id', ['type' => 'hidden']);
echo $this->Form->control('title');
echo $this->Form->control('body', ['rows' => '3']);
echo $this->Form->button(__('Save Article'));
echo $this->Form->end();
?>
リストページにも編集ページへのリンクを追加。
templates/Articles/index.php
<td>
<?= $this->Html->link('編集', ['action' => 'edit', $article->slug]) ?>
</td>

バリデーションの追加
Phalconではforms
に記載していますね
src/Model/Table/ArticlesTable.php。
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('title')
->minLength('title', 10)
->maxLength('title', 255)
->notEmptyString('body')
->minLength('body', 10);
return $validator;
}
validationDefault()
はsave()
メソッドが呼ばれる際にデータのバリデーションを行います。
だんだんPhalconとCakePHPの対応が分かってきました。
記事の削除ページ
最後に、記事を削除するページを作ります。
コントローラー
src/Controller/ArticlesController.php
public function delete($slug)
{
$this->request->allowMethod(['post', 'delete']);
$article = $this->Articles->findBySlug($slug)->firstOrFail();
if ($this->Articles->delete($article)) {
$this->Flash->success(__('The {0} article has been deleted.', $article->title));
return $this->redirect(['action' => 'index']);
}
}
$slug
からfindBySlug()
とfirstOrFail()
で記事を取得し、delete()
で削除。
シンプルでわかりやすいCRUDです。
ビュー
リストページに削除ページへのリンクを追加するだけ。
templates/Articles/index.php
<?= $this->Form->postLink(
'削除',
['action' => 'delete', $article->slug],
['confirm' => 'よろしいですか?'])
?>
ここではpostLink()
を使ってJavaScriptによる確認ダイアログを表示させています。

記事の削除もできました。

各プログラムの役割が規定のファイルに厳密に対応づけられており、「設定より規約」の思想が強く感じられました。
とはいうものの、MVCモデルに慣れた方はすぐ理解できたのではないでしょうか。
Phalconは割と自由にいじれる分、初心者には入りづらい部分もあるのですが、CakePHPはフレームワークに従って実装をするだけで、わかりやすく開発ができそうです。
参考記事
本記事では、CakePHPを使って、顧客管理システムを作ることを目的として、システムへの認証機能から順を追って説明していきます。
CakePHP4 で bakeable な開発環境の実現 [Model編]
CakePHP3で画像投稿機能付き掲示板作成 ~第一回 CRUDの実装~