CakePHP3のbakeによるコード自動生成のプラクティス

この記事は [CakePHP3 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/cakephp3) の 9日目の記事です。

CakePHP といえば必要なソースコードを自動生成してくれる bake コマンドがとっても便利です。CakePHP3 になってより使いやすくなりました。

普段の業務で使いはじめて結構経ちましたので、いくつかプラクティスを紹介してみたいと思います。~~（ベストとは書かない）~~

## bake で生成したクラスは継承して使おう

bake で生成されるコードの中には **DB スキーマの情報を元にして生成されるコード** が含まれます。

なので、以下のようなケースのときに困ります。（上から時系列）

1. DB に `users` テーブルを作成する
2. `bake model` して `UsersTable` クラスを生成する
3. 生成された `UsersTable` クラスに何らかの追加実装を行う
4. DB の `users` テーブルにカラムを追加する
5. スキーマ情報を元にして生成されたコードを更新するため、再度 `bake model` して上書きする
6. `UsersTable` に追加した実装が実装が消え去る😢

このような事態を避けるため、自動生成されたコードは常に継承して使うようにしています。

継承するとこのような感じです。

```php:src/Model/Entity/ExtUser.php
// Entity クラスの場合
class ExtUser extends User {
    // 省略
}
```

```php:src/Model/Table/ExtUsersTable.php
// Table クラスの場合
class ExtUsersTable extends UsersTable
    public function initialize(array $config)
    {
        parent::initialize($config);

        // Entity クラスは自動生成されたクラスではなく継承したクラスを使う
        $this->entityClass('App\Model\Entity\ExtUser');
    }
{
```

自動生成クラスと拡張クラスを識別できるようにするため、クラス名に固定の接頭辞 `Ext` を付けるようにしています。

同じような要領で、テストコードで使用する Fixture も継承して利用しています。Fixture のコードは DB スキーマを元にしたコードが多く含まれるため、継承して利用する方法が有効です。

詳しい方法は割愛しますが、イメージとしては下記のような感じです。

```php
class ExtUsersFixture extends UsersFixture
{
    public function init()
    {
        // テストデータを上書き
        $this->records = [
            ['id' => 101, 'name' => '鈴木', 'age' => 24],
            ['id' => 102, 'name' => '上田', 'age' => 55],
            ['id' => 103, 'name' => '山本', 'age' => 17],
        ];
    }
}
```

これでいつでも Bakeable な状態を保てます🍰

## bake のテンプレートをカスタマイズしよう

テンプレートファイルを `src/Template/Bake/` 以下に設置するだけで簡単に生成されるコードの内容を書き換えられるので、必要に応じてどんどんカスタマイズするといいと思います。

具体的な方法は他のサイトでも紹介されていますのでここでは割愛します。

- [CakePHP3 Cookbook - Bake の拡張](http://book.cakephp.org/3.0/ja/bake/development.html)
- [CakePHP3のBakeコマンドで生成されるファイルをカスタマイズする](http://sousaku-memo.net/php-system/1481) (過去に私が書いた記事です)

## 自動生成されるクラスの継承関係にワンクッション挟もう

Bake でテーブルクラスを生成すると `Cake\ORM\Table` を継承した状態でクラスが生成されるため、**テーブルクラスの共通処理を挟み込む余地** がありません。

そこで Bake のカスタマイズして、`App\Model\AppTable` など自分のクラスを継承させてワンクッション挟むようにしておくと便利です。

```php:src/Model/Table/UsersTable.php
class UsersTable extends Table {}
```
👇 (テンプレート書き換え後)

```php:src/Model/Table/UsersTable.php
class UsersTable extends AppTable {}
```

ワンクッション挟んだあとの最終的な継承関係は以下のようになりました。

- `App\Model\ExtUsersTable`
    - 👇 継承
- `App\Model\UsersTable` (bake で自動生成されるクラス)
    - 👇 継承
- `App\Model\AppTable` (ワンクッション挟み込んだクラス)
    - 👇 継承
- `Cake\ORM\Table`

全テーブルの共通処理を `AppTable` クラスに、テーブル固有の処理を `ExtUsersTable` に実装していきます。生成された `UsersTable` は編集しないようにします。

## bake の実行を自動化しよう

ここまでやるとスキーマ更新後の bake コマンドの実行も自動化したくなりませんか？

[CakePHP3 のマイグレーション](https://github.com/cakephp/migrations) を使用している前提ですが、マイグレーションコマンド実行後に bake コマンドが実行されるようにすれば自動化出来ます。

今回はそれを行うシェルスクリプトを作ってみました。`bin/cake migrations` をオーバーラップしたスクリプトなので、引数などの仕様は `bin/cake migrations` と同じになります。

```bash:bin/migration_bake.sh
#!/bin/bash
set -eu

CAKE_BIN_DIR=$(cd $(dirname $0); pwd)

# マイグレーションコマンドを実行 （引数は全て引き渡す）
${CAKE_BIN_DIR}/cake migrations "$@"

if [[ $# == 0 ]]; then
  echo "[ERROR] コマンド名が指定されていないため終了します。" 1>&2
  exit 1
fi

# マイグレーションコマンド名
# e.g. create, dump, help, list, mark_migrated, migrate, rollback, seed, status
readonly COMMAND_NAME="$1"

if [[ ! ${COMMAND_NAME} =~ ^(migrate|rollback)$ ]]; then
  echo "migrate か rollback コマンドではないため bake は行わず終了します。"
  exit 0
fi

# 全テーブルを対象として bake 実行 (Table, Entity, Fixture, Test が生成される)
# --force を付けると非インタラクティブに実行できる
${CAKE_BIN_DIR}/cake bake model all --force
```

使い方の例:

- マイグレーションする場合: `bin/migration_bake.sh migrate`
- ロールバックする場合: `bin/migration_bake.sh rollback`

## まとめ

bake でどんどん仕事を減らしていきましょう🍰

## 参考リンク

- [デザインパターン紹介 - Generation Gapパターン](http://www.hyuki.com/dp/dpinfo.html#GenerationGap)
- [CakePHP3 Cookbook - Bake でコード生成](https://book.cakephp.org/bake/1.x/ja/usage.html)
- [CakePHP3 Cookbook - Bake の拡張](https://book.cakephp.org/bake/1.x/ja/development.html)
- [CakePHP3 Cookbook - Bakeコンソール](https://book.cakephp.org/bake/1.x/ja/)
- [CakePHP3 の bake が 2.x と比べてどう変わったのか。 - Qiita](http://qiita.com/waterada/items/1067a5a70e41510e3684)