スポンサーリンク

CakePHP3のbakeによる自動生成をカスタマイズする5

今回は各テンプレートに散らばるコード定義の共通化と、左メニューの自動生成について以下の作業を実施しました。

コード定義をBakeするタスククラスを生成する

当初はModelTask内で一緒にコード定義を生成していましたが、後からコード定義だけメンテナンスが入ったときのために処理を切り分けることにしました。CodesTaskというクラスを作成しました。
BakeTaskを継承したクラスを作成すると自動でbake実行時に呼び出し可能なコマンドの一覧に加わるようです。

CodesTaskを実行すると既存のbake_codes.phpを読み込み→実行時のエイリアスに紐づくコード定義をマージして保存という処理が行われるようになっています。
以下は新規に作成したCodesTask.phpのメインとなるコード定義を生成している処理になります。bake all実行時にコード定義も併せて作成できるようBakeShell.php自体にも手を加えました。

/**
   * コード定義ファイルの生成を行う
   *
   * @param string $name The model name to generate.
   * @return void
   */
  public function bake($name)
  {
    // bakeの詳細設定を取得
    $detail_config = $this->getBakeDetailConfig($name);

    // 選択肢項目の選択肢情報を取得
    $append_codes = [];
    foreach ($detail_config['columns'] as $column_name => $config) {
      if (in_array($config['input_type'], ['select', 'radio', 'checkbox'], true)) {
        $append_codes[$name][$column_name] = $config['selections'];
      }
    }
    if (empty($append_codes)) {
      $this->out('Bake Codes Target Not found...');
      return;
    }

    // 既存の設定を読み込み
    $current_codes = [];
    if (file_exists(BAKED_CONFIG_FILE)) {
      $current_codes = include (BAKED_CONFIG_FILE);
      if (isset($current_codes[$name])) {
        unset($current_codes[$name]);
      }
    }

    // 既存の設定と新しく追加する設定のマージ
    $current_codes = array_merge($current_codes, $append_codes);

    // ファイル生成
    $out  = "<?php" . PHP_EOL;
    $out .= "return [" . PHP_EOL;
    foreach ($current_codes as $alias_name => $codes) {
      $out .= "		'{$alias_name}' => [" . PHP_EOL;
      foreach ($codes as $code_name => $code_data) {
        $out .= "				'{$code_name}' => [" . PHP_EOL;
        foreach ($code_data as $code_key => $code_value) {
          $out .= "						'{$code_key}' => '{$code_value}'," . PHP_EOL;
        }
        $out .= "				]," . PHP_EOL;
      }
      $out .= "		]," . PHP_EOL;
    }
    $out .= "];" . PHP_EOL;

    // ファイル出力
    $this->out("\n" . sprintf('Baking codes for %s...', $name), 1, Shell::QUIET);
    $this->createFile(BAKED_CONFIG_FILE, $out);

    return $out;
  }
<?php
return [
];
<?php
return [
    'Estates' => [
        'tikunen' => [
            '01' => '新築',
            '02' => '1年以内',
            '03' => '3年以内',
            '04' => '5年以内',
            '05' => '7年以内',
            '06' => '10年以内',
            '07' => '20年以内',
            '08' => '30年以内',
            '09' => '指定しない',
        ],
        'kodawari' => [
            '01' => '風呂・トイレ別',
            '02' => '2階以上',
            '03' => '駐車場あり',
            '04' => '室内洗濯機置場',
            '05' => 'エアコン付き',
            '06' => 'ペット相談可',
            '07' => 'オートロック',
            '08' => '洗面所独立',
        ],
    ],
];
public function all($name = null)
    {
        if ($this->param('connection') && $this->param('everything') &&
            $this->param('connection') !== 'default') {
            $this->warn('Can only bake everything on default connection');

            return false;
        }
        $this->out('Bake All');
        $this->hr();

        if (!empty($this->params['connection'])) {
            $this->connection = $this->params['connection'];
        }

        if (empty($name) && !$this->param('everything')) {
            $this->Model->connection = $this->connection;
            $this->out('Possible model names based on your database:');
            foreach ($this->Model->listUnskipped() as $table) {
                $this->out('- ' . $table);
            }
            $this->out('Run <info>`cake bake all [name]`</info> to generate skeleton files.');

            return false;
        }

        $allTables = collection([$name]);
        $filteredTables = $allTables;

        if ($this->param('everything')) {
            $this->Model->connection = $this->connection;
            $filteredTables = collection($this->Model->listUnskipped());
        }

+        foreach (['Model', 'Controller', 'Template', 'Codes'] as $task) {
            $filteredTables->each(function ($tableName) use ($task) {
                $tableName = $this->_camelize($tableName);
                $this->{$task}->connection = $this->connection;
                $this->{$task}->interactive = $this->interactive;
                $this->{$task}->main($tableName);
            });
        }

        $this->out('<success>Bake All complete.</success>', 1, Shell::QUIET);

        return true;
    }

コード定義をどこからでも呼び出せるグローバルな関数を作る

いちいちPHPの先頭にuseを書いたり、Configure::readでコード定義を読み込むのは手間なので以下の関数を作成しました。後述するテンプレートの修正でもこの関数を使用してコード定義を読み込んでいます。

<?php
use Cake\Core\Configure;

/**
 * コード定義を取得する
 * @param $code_key
 * @return bool|mixed
 */
function _code($code_key) {
  return Configure::read($code_key);
}

functions.phpはconfigディレクトリに置いてあるため、bootstrap.php内でコード定義のファイルと合わせてインクルードしています。

/*
 * Read configuration file and inject configuration into various
 * CakePHP classes.
 *
 * By default there is only one configuration file. It is often a good
 * idea to create multiple configuration files, and separate the configuration
 * that changes from configuration that does not. This makes deployment simpler.
 */
try {
    Configure::config('default', new PhpConfig());
    Configure::load('app', 'default', false);
+    Configure::load('bake_codes', 'default');
+    require_once (__DIR__ . '/functions.php');
} catch (\Exception $e) {
    exit($e->getMessage() . "\n");
}

各テンプレートを修正する

一覧画面は以下のような感じで修正しました。先頭で〇〇_selectionという変数を作成している箇所を消して、その変数を参照していた箇所を_code関数に置き換えるだけです。
edit.twigとview.twigも似たような感じで修正しました。

<?php
/**
 * @var \{{ namespace }}\View\AppView $this
 * @var \{{ entityClass }}[]|\Cake\Collection\CollectionInterface ${{ pluralVar }}
 */
$this->assign('title', "{{ detail_config.function_title }}");
-{# 選択肢の設定を出力 #}
-{% for field, column_config in detail_config.columns if field not in primaryKey and column_config.input_type in ['select', 'radio', 'checkbox'] and column_config.selections is defined %}
-${{ field }}_selections = [
-  '' => ' ',
-{% for selections_key, selections_value in column_config.selections %}
-  '{{ selections_key }}' => '{{ selections_value }}',
-{% endfor %}
-];
-{% endfor %}
{% set fields = Bake.filterFields(fields, schema, modelObject, indexColumns, ['binary', 'text']) %}
<div class="col-md-12 mb-12">
 <div class="card">
  <div class="card-header">
   <button type="button" class="btn btn-flat btn-outline-secondary" onclick="location.href='/{{ pluralVar }}/add/'">新規登録</button>
   <button type="button" class="btn btn-flat btn-outline-secondary" data-toggle="modal" data-target="#{{ pluralVar }}-search-form-modal">検索</button>
  </div>
  <div class="card-body">
   <table class="table table-bordered">
    <thead>
            <tr>
{% for field, column_config in detail_config.columns if column_config.listview == true and field not in ['delete_flag'] %}
                <th scope="col"><?= $this->Paginator->sort('{{ field }}', '{{ column_config.label }}') ?></th>
{% endfor %}
                <th scope="col" class="actions">操作</th>
            </tr>
    </thead>
    <tbody>
            <?php foreach (${{ pluralVar }} as ${{ singularVar }}) { ?>
            <tr>
{% for field, column_config in detail_config.columns if column_config.listview == true and field not in ['delete_flag'] %}
{% set isKey = false %}
{% if associations.BelongsTo %}
{% for alias, details in associations.BelongsTo if field == details.foreignKey %}
{% set isKey = true %}
                <td><?= ${{ singularVar }}->has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?></td>
{% endfor %}
{% endif %}
{% if isKey is not same as(true) %}
{% if column_config.input_type in ['select', 'radio'] and column_config.selections is defined %}
-                <td><?= @h(${{ field }}_selections[${{ singularVar }}->{{ field }}]) ?></td>
+                <td><?= @h(_code("{{ detail_config.alias_name }}.{{ field }}.{${{ singularVar }}->{{ field }}}")) ?></td>
{% elseif column_config.input_type in ['checkbox'] and column_config.selections is defined %}
-                <td><?= $this->displayCheckboxItem(${{ singularVar }}['{{ column_config.join_table_name }}'], ${{ field }}_selections, '{{ field }}') ?></td>
+                <td><?= $this->displayCheckboxItem(${{ singularVar }}['{{ column_config.join_table_name }}'], _code('{{ detail_config.alias_name }}.{{ field }}'), '{{ field }}') ?></td>
{% elseif column_config.input_type in ['date', 'datetime'] %}
                <td><?= h(${{ singularVar }}->{{ field }}) ?></td>
{% elseif column_config.input_type == 'number' %}
                <td><?= $this->Number->format(${{ singularVar }}->{{ field }}) ?>{% if column_config.unit_text is defined %}{{ column_config.unit_text }}{% endif %}</td>
{% else %}
                <td><?= h(${{ singularVar }}->{{ field }}) ?></td>
{% endif %}
{% endif %}
{% endfor %}
{% set pk = '$' ~ singularVar ~ '->' ~ primaryKey[0] %}
                <td class="actions">
                    <?= $this->Html->link('詳細', ['action' => 'view', {{ pk|raw }}], ['class' => 'btn btn-flat btn-outline-secondary']) ?>
                    <?= $this->Html->link('編集', ['action' => 'edit', {{ pk|raw }}], ['class' => 'btn btn-flat btn-outline-secondary']) ?>
                    <?= $this->Form->postLink('削除', ['action' => 'delete', {{ pk|raw }}], ['class' => 'btn btn-flat btn-outline-secondary', 'confirm' => __('ID {0} を削除します。よろしいですか?', {{ pk|raw }})]) ?>
                </td>
            </tr>
          <?php } ?>
    </tbody>
   </table>
  </div>
 </div>
 <?= $this->element('pager') ?>
</div>

左メニューを生成するタスククラスを生成する

CodesTaskと同じような感じでBakeTaskを継承したLeftSideMenuTaskクラスを作成。
LeftSideMenuTaskで生成したサイドメニューの設定を元にHTMLを出力するleft_side_menu.ctpを作成。
表示元のテンプレートはleft_side_menuをインクルードするだけ。

<?php
class LeftSideMenuTask extends BakeTask
{
  /**
   * Execution method always used for tasks
   *
   * @param string|null $name The name of the table to bake.
   * @return void
   */
  public function main($name = null)
  {
    parent::main();
    $name = $this->_getName($name);

    if (empty($name)) {
      $this->out('Choose a model to bake from the following:');
      foreach ($this->listUnskipped() as $table) {
        $this->out('- ' . $this->_camelize($table));
      }

      return;
    }

    $this->bake($this->_camelize($name));
  }

  /**
   * 左メニューの生成を行う
   *
   * @param string $name The model name to generate.
   * @return void
   */
  public function bake($name)
  {
    // bakeの詳細設定を取得
    $detail_config = $this->getBakeDetailConfig($name);

    // エイリアス名か機能名がなかったら処理を中止
    if (empty($name) || $detail_config['function_title']) {
      $this->out('Bake LeftSideMenu Setting Not found...');
      return;
    }

    $append_codes['LeftSideMenu'][$name] = [
        'controller' => $name,
        'label' => $detail_config['function_title'],
        'icon_class' => @$detail_config['options']['left_side_menu_icon_class'],
    ];

    // 既存の設定を読み込み
    $current_codes = [];
    if (file_exists(BAKED_LEFT_SIDE_MENU_FILE)) {
      $current_codes = include (BAKED_LEFT_SIDE_MENU_FILE);
      if (isset($current_codes['LeftSideMenu'][$name])) {
        unset($current_codes['LeftSideMenu'][$name]);
      }
    }

    // 既存の設定と新しく追加する設定のマージ
    $current_codes = array_merge_recursive($current_codes, $append_codes);

    // ファイル生成
    $out  = "<?php" . PHP_EOL;
    $out .= "return [" . PHP_EOL;
    $out .= "		'LeftSideMenu' => [" . PHP_EOL;
    foreach ($current_codes['LeftSideMenu'] as $alias_name => $codes) {
      $out .= "				'{$alias_name}' => [" . PHP_EOL;
        foreach ($codes as $code_key => $code_value) {
          $out .= "						'{$code_key}' => '{$code_value}'," . PHP_EOL;
        }
      $out .= "				]," . PHP_EOL;
    }
    $out .= "		]," . PHP_EOL;
    $out .= "];" . PHP_EOL;

    // ファイル出力
    $this->out("\n" . sprintf('Baking codes for %s...', $name), 1, Shell::QUIET);
    $this->createFile(BAKED_LEFT_SIDE_MENU_FILE, $out);

    return $out;
  }
}
<div class="sidebar">
      <nav class="mt-2">
        <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
-          <li class="nav-item">
-            <a href="<?= $this->Url->build(['controller' => 'Users', 'action' => 'index']) ?>" class="nav-link<?php if ($this->name == 'Users') { ?> active<?php } ?>">
-              <i class="fas fa-user mr-3"></i><p>Users</p>
-            </a>
-          </li>
+          <?= $this->element('left_side_menu') ?>
        </ul>
      </nav>
    </div>
<?php
$functions = _code('LeftSideMenu');
if (!empty($functions) && count($functions) > 0) {
  $html = "";
  foreach ($functions as $alias => $function) {
    $html .= "<li class=\"nav-item\">";
    $html .= "<a href=\"" . $this->Url->build(['controller' => "{$function['controller']}", 'action' => 'index']) . "\" class=\"nav-link";
    if ($this->name == $function['controller']) {
      $html .= " active";
    }
    $html .= "\">";
    $html .= "<i class=\"{$function['icon_class']} mr-2\"></i><p>{$function['label']}</p>";
    $html .= "</a>";
    $html .= "</li>";
  }
  echo $html;
}
<?php
return [
    'LeftSideMenu' => [
    ],
];
<?php
return [
    'LeftSideMenu' => [
        'Users' => [
            'controller' => 'Users',
            'label' => 'ユーザー',
            'icon_class' => 'fas fa-user',
        ],
        'Estates' => [
            'controller' => 'Estates',
            'label' => '物件',
            'icon_class' => 'fas fa-home',
        ],
    ],
];

メモ

コード定義の共通化の修正を行ったことでメンテナンス性が少しだけよくなった。
サイドメニューの方はBakeするたびに配列内の順番が変わってしまうため、bake allには含めませんでした。

タイトルとURLをコピーしました