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 に則っていたでしょう。

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

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

static:: による遅延静的束縛

という言語仕様を先日初めて認識したので、ざっくり調べてみました。

公式いわく

https://www.php.net/manual/ja/language.oop5.late-static-bindings.php

PHP 5.3.0 以降、PHP に遅延静的束縛と呼ばれる機能が搭載されます。

これを使用すると、静的継承のコンテキストで呼び出し元のクラスを参照できるようになります。

より正確に言うと、遅延静的束縛は直近の "非転送コール" のクラス名を保存します。…

驚くほど意味がつかめないですね。「非転送コール」ってなんやねん状態です。

さっと目を通してとりあえず static:: を使う場合に発揮されるなにか、ということは掴めたので、サンプルコードを書いていきましょう。

self::static:: の挙動の違い

まずは self:: の挙動を確認します。

<?php

class Animal
{
    public static int $legs = 4;

    public static function walk(): void
    {
        echo self::$legs. '本足で歩く';
    }
}

class Human extends Animal
{
    public static int $legs = 2;
}

Human::walk(); // => 4本足で歩く

self:: で参照されるのは、常に self:: が書かれたクラスになります。

そのため、継承先のクラスで static な変数やメソッドを定義し直しても、オーバーライドすることはできません

この制限を逃れるため使われるのが、 static:: による遅延静的束縛です。

では、 static:: の挙動を見ていきましょう。先程のコードを少し変更します。

<?php

class Animal
{
    public static int $legs = 4;

    public static function walk(): void
    {
        echo static::$legs. '本足で歩く';
    }
}

class Human extends Animal
{
    public static int $legs = 2;
}

Human::walk(); // => 2本足で歩く

変数の上書きができました! もちろん、static メソッドのオーバーライドも可能です。

実際に起こっていること

さて、遅延静的束縛が何をするものなのか、イメージはついたでしょうか?

もう少しだけ踏み込んで考えていきましょう。

公式ドキュメント を読み進めます。

静的メソッドの場合、これは明示的に指定されたクラス (通常は :: 演算子の左側に書かれたもの) となります。静的メソッド以外の場合は、そのオブジェクトのクラスとなります。 "転送コール" とは、self:: や parent::、static:: による静的なコール、 あるいはクラス階層の中での forward_static_call() によるコールのことです。 get_called_class() 関数を使うとコール元のクラス名を文字列で取得できます。 static:: はこのクラスのスコープとなります。

またややこしいですね。整理しましょう。

コールの種別 説明
非転送コール $class→hoge()Class::hoge() 等、クラス名を明示したコール
転送コール parent::, self::, static:: またはクラス階層中の forward_static_call() によるコール

冒頭の繰り返しになりますが、ドキュメントでは、 遅延静的束縛は、「直近の"非転送コール"」のクラス名を保存する と説明されています。

それってどういう状態なんだ? というのを理解するために、サンプルコードを変えます。

<?php

class Animal
{
    public static function walk(): void
    {
        self::skip(); // 転送コール
    }

    public static function skip(): void
    {
        static::run();  // static が参照するのは直近の「非転送コール」のクラス
    }

    public static function run(): void
    {
        echo '歩くことは走ることだ';
    }
}

class Human extends Animal
{
    public static function run(): void
    {
        echo '歩くな、走れ!';
    }
}

Animal::walk(); // => 歩くことは走ることだ
Human::walk(); // => 歩くな、走れ!

Human::walk() はこれ自体が非転送コールで、static:: から見ると直近の非転送コールなので、オーバーライドされた run() が実行されています。

まとめ

  • self:: のスコープは self:: を書いたクラスになり、オーバーライドできない
  • static:: を使うと遅延静的束縛により、オーバーライドが可能になる
  • static:: が参照するのは直近の非転送コールである

今まで意識していなかっただけに、仕様を整理していくのはおもしろーい!

わたし含め「とりあえず self:: にしていた」という方が、オーバーライドの必要性を考慮した上で、 static:: による遅延静的束縛もうまく利用していけるといいですね〜

【Laravel】Factory定義の中で Factoryのcreateはやめよう

Factoryの挙動を勘違いしていたせいで、無駄なデータを大量生成してしまったお話です。

こんなテーブル構成とします。

users
  - id
  - name ...
shops
  - id
  - name ...
orders
  - id
  - user_id
  - shop_id
  - amount ...

OrderFactory をこんな書き方にしている方は、今すぐやめましょう。

<?php
// OrderFactory.php
$this->define(Order::class, function (Faker $faker) {
    return [
        'user_id' => factory(User::class)->create()->id,
        'shop_id' => factory(Shop::class)->create()->id,
        'amount' => ...,
    ];
});

Feature テストや Seeder で使ったとき、あなたが意図しないデータが生成されてしまいます。

どういうことなのか?

実際に Feature テストを書くときは、Faker の値をそのまま利用せず、いくつかのプロパティはなんらかの値に固定するかと思います。

今回は User を生成する際に name を固定値にしています。

<?php
$user = factory(User::class)->create(['name' => 'test']);
factory(Order::class)->create([
    'user_id' => $user->id,
]);

この直後に User::all() すると、2つUser が取得できてしまいます!

つまり上のコードは、

<?php
$user = factory(User::class)->create();
$order = factory(Order::class)->create();
$order->fill(['user_id' => $user->id])->save();

こう書いているのと変わらない、ということですね。

単なるテストならこれでもまあ問題にはなりにくいですが、API 連携テスト環境の Seeder で呼ばれる、全ての Factory がこんな書き方をしていたら……

あら不思議、想定の 30 倍近いデータが出来上がります!!!(経験談)

いちいち関連データのcreate書くのが面倒

とはいえ、Order に関する全てのテストで User の値を固定したい」なんてことはないでしょう。 何でもいいから User と紐付けたい、ということのほうが多いかもしれません。

そういうときは、別名をつけるか、 state を使っています。

別名をつける場合

<?php
// OrderFactory.php
$this->define(Order::class, function (Faker $faker) {
    return [
        'user_id' => factory(User::class)->create()->id,
        'amount' => ...
    ];
}, 'with_user_id');

// test, seeder
factory(Order::class, 'with_user_id')->create();

state を使う場合

https://laravel.com/docs/7.x/database-testing#factory-states

<?php
// OrderFactory.php
$this->define(Order::class, function (Faker $faker) {
    return [
        ...,
    ];
});
$this->state(Order::class, 'with_user_id', [
        'user_id' => factory(User::class)->create()->id,
    ];
});

// test, seeder
factory(Order::class)->state('with_user_id')->create();

まとめ

  • factory(Eloquent::class)->create() にはプロパティを渡せるが、元のFactoryで定義した値を無視して生成してくれるわけではない
  • 関連データの生成を書くのが面倒なら、別名をつけるか、state を使うのが手軽

シナリオによっては関連データもそれぞれ細かに定義しなきゃいけない場合がありますが、そうなった場合のベストプラクティスはまだ見つかっていません。

"SOFT SKILLS ソフトウェア開発者の人生マニュアル" を第4部まで読んだ

ので、ブログを開設しました。

記念すべき1記事目を、自身の振り返りの記録という形でこの本に捧げます。

感想の前に

この本に感銘を受けたわたしという人間について触れておきます。

今は福岡でPHPを用いた開発をしていますが、前職はほぼC#でデスクトップアプリを作っていました。

転職の大きなきっかけは、ある日突然「このままここで5年働いたら、もう次の職場は見つからないのでは?」という危機感が降って湧いたことでした。

とはいえ、明確なビジョンなど全くなく。

とにかく福岡に出たい!!!あわよくば今より給与のいいところで働きたい!!!

という気持ちだけで転職活動を始めました。

弊社に応募した理由は「なんとなく楽しそう」というカンです。最終面接でもそう言ったし、入社から10ヵ月目を迎える今も、楽しいから続けていきたいな〜、ぐらいの感覚でした。

そうです。お察しの通り、わたしは自分のキャリアを真剣に考えたことがありません。

それでいいのか??

たぶん良くない。いや、ふつうに絶対良くない。

でも、わたしはそういう生き物なのだ……これからも浮草だかクラゲだかのようにふわふわ漂って流れていくんだ……。

と、ある種開き直っていましたが。

そういう浮草人間を雑草人間に変えてくれる本です。

ふわふわ漂っている方、ぜひご一読を〜〜!

感想と覚書

第1部「キャリアを築こう」

痛いところをズブズブ突き刺されます。

  • 自分を1人の個人事業主として捉え、ソフトウェアを作る能力をサービスとして提供しているのだと考える。

わたしが自分のキャリアを人任せにできていたのは、このマインドがなかったからですね。

それから、

  • 自分のキャリアの大きな目標を1つ考える。

未だこの大きな目標は定量的でわかりやすいものではないのですが、 そこに至るまでの小さな目標はなんとなく見えてきた気がしています。 内省&内省、大事!

第2部「自分を売り込め!」

「エンジニアはアウトプットが大事」。よく聞く言葉ですね。

その意味も価値も、もちろん理解しています。しかし今日まで行動に移さなかった人間です……。

いい加減二の足を踏むのはやめよう!

ということで、ハードルの低いところからやりました。

  • GitHubアカウントのアイコンを設定
  • はてなにユーザ登録
  • Twitter アカウントの開設
  • ブログ記事を書く <= now!

アカウント名統一したいな〜と思ってGitHubのアカウント名変更したのに、 twitter- が使えないと知ったときは絶望しました

第3部「学ぶことを学ぼう」

これは耳が痛かった。

転職して以来、「わたしが人に教えられることなんて…」という気持ちが強くありました。Gitさえ触ったことなかったし。

でも確かに、自分がつよつよエンジニアでなくても、誰かにものを教えることはできますよねー。

変に萎縮しすぎて、自分も周囲も育たない環境を作り上げちゃわないようにがんばろーと思いました。

  • 第35章「知識のなかの隙間を見つける」

これも耳が痛かった〜!!

C#を書いて間もない頃、LINQの概念を理解しきれないまま、先人のコードから「察して」書いていました。 あの時間には無駄が多かったし、その時間が終わったところでわたしの成長はほとんどありませんでした……反省。

第4部「生産性を高めよう」

ポモドーロテクニックと習慣に書かれていますね。

同僚がお仕事にポモドーロテクニックを持ち込んでいて、すごくいいという話は聞いていました。 だから自粛期間が始まってすぐに試してみたものの、効果はびみょう……。

著者と同じ失敗をしていました。

今はこの1記事を、きちんと見積もった上で、トラッキングしながら書いています。 すでに見積もり時間を超過していますけど、集中状態は確かに維持できている……気がします。

おわりに

初めに述べた通り、このエントリは "SOFT SKILLS" を通して自身の振り返りを行った記録、という位置づけです。

そのため、書籍ではこれ以降もお金や体、メンタルの話が続いていきますが、このブログではそこについての記事は投稿しません。

今は読了直後で意識高〜〜〜い感じです。 ちょっとずつ自分に合った形に落としていきたいですねー。

このエントリがこのブログ最後の投稿になりませんように!