chocolate Tech room

チョコを食べながら技術のことを考える

【CakePHP3.9】bake で生成される Controller をそのまま使いたくないという話

この2ヵ月ほど、業務で CakePHP 3.9 を使っています。

もともと PHPフレームワークFuelPHP (保守担当しただけ) => Laravel という変遷を経てきたので、 ゼロイチ開発で CakePHP を使用するのは今回が初めてです。

開発も一段落したところで、開発初期段階で抱いた

bake controller で生成されるコード、そのまま使いたくないなあ」

という思いとその改善方法を記しておきます。

気になった点

気になったのは2点です。

action 内のネストが深い

以下は bin/cake bake controller Sample で生成した SampleControlleradd() メソッドです。

<?php

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

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

ネストが深い!

GET でも POST でも同じ action を呼び出し、 action 内で HTTP リクエストメソッドを検証することで処理を分岐しているせいです。

結局リクエストメソッドで分岐するなら最初から action を分けたほうが見通しが良いし、 切り分けができているので拡張も容易なのでは? と思ってしまいます。

また、返り値に型をつけるというコーディングルールがあった場合、 Responsevoid が混在していて、PHP 7.4 系では戻り値の型を定義できなくなります。

ルート定義は connect の利用が強制される

上述の「HTTP リクエストメソッドに関わらず同一の action を呼ぶ」という処理は、 connect メソッドを利用して実現されています。

<?php
$routes->connect('/:controller', [], ['routeClass' => 'InflectedRoute']);
$routes->connect('/:controller/:action/*', [], ['routeClass' => 'InflectedRoute']);

各 Controller の各 action にいい感じにルーティングしてくれます。楽ですね。

しかし、この2行で済ませようとすると、ルートごとの HTTP リクエストメソッドを制限できません。

connect は DRY に基づいたもの、とのことですが、それで action のほうがごちゃごちゃするくらいなら、 action ルートごとに定義させてくれというのが本音です。

私的改善方法

action もルート定義もゴリゴリ記述します。

HTTP リクエストメソッドごとに action を定義

手っ取り早く GET 用と POST/PUT 用に分けます。

<?php

/**
 * GET: 新規作成画面
 */
public function add(): void {}

/**
 * POST: 新規作成処理
 */
public function create(): void {}

/**
 * GET: 編集画面
 */
public function edit(): void {}

/**
 * PUT: 更新処理
 */
public function update(): void {}

GET 関連メソッドは画面描画、 POST/PUT 関連メソッドは DB 操作、と action の役割を明確に切り分けられました。

返り値を void で揃えているのは、 create()update() でバリデーションエラーがあった場合、元画面を再描画する(= リダイレクトなく処理が終了する)からです。 redirect は返り値としてセットしなくても問題なく動作したので、こういう形に落ち着きました。

ルートで HTTP リクエストメソッドを指定する

action たちを 1 つ 1 つ定義していきます。

<?php

// 新規作成画面
$routes->get('/add', [
    'controller' => 'Sample', 'action' => 'add',
], 'sample:add');

// 新規作成処理
$routes->post('/add', [
    'controller' => 'Sample', 'action' => 'create',
], 'sample:create');

// 編集画面
$routes->get('/edit', [
    'controller' => 'Sample', 'action' => 'edit',
], 'sample:edit');

// 更新処理
$routes->put('/edit', [
    'controller' => 'Sample', 'action' => 'update',
], 'sample:update');

対になるメソッドの URI を同じものにしているのは、前の項でも触れている POST/PUT でバリデーションエラーがあった場合に備えてのことです。

画面描画のみでリダイレクトしないので、 URI を揃えておかないと、エラーが発生する前と後でユーザーに表示される URL が異なるという、ちょっと変な感じになっちゃいます。

また、第3引数のルート名は省略可能ですが、私は絶対に定義しています。

下記の通り view や各種 Controller でルートを呼び出すとき、ルート名を指定したほうが分かりやすいです。

<?php
// view での呼び出し
<?= $this->Url->build(['_name' => 'sample:create']) ?>
<?php
// controller での呼び出し
public function create(): void
{
    // 省略
    $this->redirect(['_name' => 'sample:add']);
}

まとめ

以上、bake コマンドを叩いたらいろいろ気になったのでこう修正しましたよ、というお話でした。

もちろん bin/cake bake all で単純な CRUD がパッとできちゃうのは、非常に強力な便利機能なので、否定はしません。

要は使い所だと思います。

私が初学者の頃は「Scaffold は絶対的な正解だ!」と思い込んでいました。

もしかつての私が CakePHP を触ったら、 将来的に拡張が見込まれたり、複数クラスを呼び出して複雑な処理をしていたり……という場合でも、 無理して Scaffold に則っていたでしょう。

そういう無理を通す必要はないんだよ、という例を提示してみました。

どなたかの参考になれば幸いです。