BakeしたModelを継承するModelを生成してBakableな環境を作る

# 目的
開発の途中でテーブル定義が変更されることがよくある。（カラム追加とか）
その際に、CakePHPのBakeコマンドを再実行して、Model(Table,Entity)を最新化すると独自に記述したロジックが上書きされてしまうので、独自ロジックはBakeしたModelを継承したクラスに記述して、BakeしたModelを更新しても独自ロジックに影響しない状態にする。

※実運用に達していないので、参考程度と考えていただきたい。

# ソースコード

## bake用のModelを生成するコマンドとテンプレート
通常の`bake model`を利用すると`src/Model/Entity(またはTable)`にModelがBakeされる。
ここにはBakeしたModelを継承したModelを配置したいので、`ModelCommand `を継承したCommandを作り、`public $pathFragment = 'Model/Baked/';`とすることで、`src/Model/Baked/Entity(またはTable)にModelをBakeするようにする。

```src/Command/BakedModelCommand.php
<?php
declare(strict_types=1);

namespace App\Command;

use Bake\Command\ModelCommand;
use Bake\Utility\TemplateRenderer;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Core\Configure;
use Cake\ORM\Table;

class BakedModelCommand extends ModelCommand
{
    public $pathFragment = 'Model/Baked/';

    /**
     * {@inheritDoc}
     */
    public function bake(string $name, Arguments $args, ConsoleIo $io): void
    {
        parent::bake($name, $args, $io);
    }
}

```

上だけだと、Bakeのデフォルトのテンプレートを使われてしまい、BakeされたModelの`namespace`がデフォルトになってしまうので、`vendor/cakephp/bake/templates/bake/Model`配下の`entity.twig`、`table.twig`をコピーして、`templates/plugin/Bake/Model`配下に配置します。２ファイルの`namespace`を修正します。

## bakeしたModelを継承するクラスを生成するコマンドとテンプレート

同じく`ModelCommand`を継承しつつ、`ModelCommand`の`bakeTable`と`bakeTable`を元に出力先ディレクトリや利用するテンプレートを変更した`bakeExtendedTable`と`bakeExtendedEntity`を作成した。
もっといい方法はないものか・・？
（`ModelCommand`が出力先ディレクトリや利用するテンプレートを差し替えれるようにしておいてくれると継承しやすいのに。。）

```src/Command/ExtendedModelCommand.php
<?php
declare(strict_types=1);

namespace App\Command;

use Bake\Command\ModelCommand;
use Bake\Utility\TemplateRenderer;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Core\Configure;
use Cake\ORM\Table;

class ExtendedModelCommand extends ModelCommand
{
    /**
     * {@inheritDoc}
     */
    public function bake(string $name, Arguments $args, ConsoleIo $io): void
    {
        $table = $this->getTable($name, $args);
        $tableObject = $this->getTableObject($name, $table);
        $data = $this->getTableContext($tableObject, $table, $name, $args, $io);
        $this->bakeExtendedTable($tableObject, $data, $args, $io);
        $this->bakeExtendedEntity($tableObject, $data, $args, $io);

    }

    public function bakeExtendedTable(Table $model, array $data, Arguments $args, ConsoleIo $io): void
    {
        if ($args->getOption('no-table')) {
            return;
        }

        $namespace = Configure::read('App.namespace');
        $pluginPath = '';
        if ($this->plugin) {
            $namespace = $this->_pluginNamespace($this->plugin);
        }

        $name = $model->getAlias();
        $entity = $this->_entityName($model->getAlias());
        $data += [
            'plugin' => $this->plugin,
            'pluginPath' => $pluginPath,
            'namespace' => $namespace,
            'name' => $name,
            'entity' => $entity,
            'associations' => [],
            'primaryKey' => 'id',
            'displayField' => null,
            'table' => null,
            'validation' => [],
            'rulesChecker' => [],
            'behaviors' => [],
            'connection' => $this->connection,
        ];

        $renderer = new TemplateRenderer($this->theme);
        $renderer->set($data);
        $out = $renderer->generate('Bake.Model/extended_table');

        $path = $this->getPath($args);
        $filename = $path . 'Table' . DS . $name . 'Table.php';
        $io->out("\n" . sprintf('Baking table class for %s...', $name), 1, ConsoleIo::QUIET);
        $io->createFile($filename, $out, false);

        // Work around composer caching that classes/files do not exist.
        // Check for the file as it might not exist in tests.
        if (file_exists($filename)) {
            require_once $filename;
        }
        $this->getTableLocator()->clear();

        $emptyFile = $path . 'Table' . DS . '.gitkeep';
        $this->deleteEmptyFile($emptyFile, $io);
    }

    public function bakeExtendedEntity(Table $model, array $data, Arguments $args, ConsoleIo $io): void
    {
        if ($args->getOption('no-entity')) {
            return;
        }
        $name = $this->_entityName($model->getAlias());

        $namespace = Configure::read('App.namespace');
        $pluginPath = '';
        if ($this->plugin) {
            $namespace = $this->_pluginNamespace($this->plugin);
            $pluginPath = $this->plugin . '.';
        }

        $data += [
            'name' => $name,
            'namespace' => $namespace,
            'plugin' => $this->plugin,
            'pluginPath' => $pluginPath,
            'primaryKey' => [],
        ];

        $renderer = new TemplateRenderer($this->theme);
        $renderer->set($data);
        $out = $renderer->generate('Bake.Model/extended_entity');

        $path = $this->getPath($args);
        $filename = $path . 'Entity' . DS . $name . '.php';
        $io->out("\n" . sprintf('Baking entity class for %s...', $name), 1, ConsoleIo::QUIET);
        $io->createFile($filename, $out, false);

        $emptyFile = $path . 'Entity' . DS . '.gitkeep';
        $this->deleteEmptyFile($emptyFile, $io);
    }
}

```

上記コマンドから呼び出すテンプレートが以下２つ。

```templates/plugin/Bake/Model/extended_table.twig
{#
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @since         2.0.0
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
#}
{% set annotations = DocBlock.buildTableAnnotations(associations, associationInfo, behaviors, entity, namespace) %}
<?php
declare(strict_types=1);

namespace {{ namespace }}\Model\Table;

use {{ namespace }}\Model\Baked\Table\{{ name }}Table as BakedTable;
{% set uses = ['use Cake\\ORM\\Query;', 'use Cake\\ORM\\RulesChecker;', 'use Cake\\ORM\\Table;', 'use Cake\\Validation\\Validator;'] %}
{{ uses|join('\n')|raw }}

{{ DocBlock.classDescription(name, 'Model', annotations)|raw }}
class {{ name }}Table extends BakedTable
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator): Validator
    {
        parent::validationDefault($validator);

        return $validator;
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules): RulesChecker
    {
        parent::buildRules($rules);

        return $rules;
    }

    /**
     * Returns the database connection name to use by default.
     *
     * @return string
     */
    public static function defaultConnectionName(): string
    {
        return '{{ connection }}';
    }
}


```

```templates/plugin/Bake/Model/extended_entity.twig
{#
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @since         2.0.0
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
#}
<?php
declare(strict_types=1);

namespace {{ namespace }}\Model\Entity;

use {{ namespace }}\Model\Baked\Entity\{{ name }} as BakedEntity;

class {{ name }} extends BakedEntity
{
}

```

# 利用方法
```
bin/cake bake baked_model Xxxs
bin/cake bake extended_model Xxxs
```

# 感想
一旦はこれで開発してみて問題があれば修正する。
マニュアルに記載されている`SimpleBakeCommand`継承版も作ってみたが、引数の`Xxxs`からファイル名やクラス名を生成するのが手間なので、`ModelCommand`継承にすることにした。
ただこの実装だと`ModelCommand`に変更が入った時にコピペし直しが必要なので、もっといい方法がありそう。

# 参考
https://book.cakephp.org/bake/2/en/development.html

この考え方自体は以下を見て知った。
https://wp.tech-style.info/archives/1519
https://qiita.com/ymm1x/items/12ced37212636b09de19