Lesson 7

bakeコマンド

Lesson 7 Chapter 1
bakeコマンドについて

bakeコマンドの概要

Lesson5 ではCRUDを持つ基本的な画面を手動で作成しましたが、CakePHPには「bake」と呼ばれるコード生成ツールが用意されており、CRUD のアクションを含む、コントローラー、モデル、ビューのひな形を自動生成するコマンドを使用することが出来ます。

自動生成されるのは、あくまでひな形となるため、アプリケーションを完成させるには、ほとんどの場合で生成されたファイルを確認し、必要な修正を加える必要があります。
また、アプリケーション固有のビジネスロジックやバリデーションなどは手動で実装していく必要があります。

とはいえ、CRUDと基本の画面をノーコードで自動生成できることは非常に魅力的であり、使いこなすことで効率よく開発をすすめることが出来ます。

そのため、この Lesson7 では bakeコマンドの使い方と、自動生成されるファイルがどのような内容になっているのかを理解し、滞りなくアプリケーション開発を進めていくための基礎を身に着けていきます。

bakeの素を準備する

bakeコマンドを使用するために、まずはデータベース上に新たなテーブルを作成します。テーブル作成に関しては手動となります。

今回は次の Lesson8 で行う「認証機能」の実装で使用する usersテーブル を作成します。

コマンドライン上で、MySQLにログインした状態で、以下のSQL文を入力し、実行して下さい。先に「use todo;」で操作するデータベースを選択しておくことを忘れないようにして下さい。

CREATE TABLE users (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  created DATETIME DEFAULT CURRENT_TIMESTAMP,
  modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) DEFAULT CHARSET=utf8mb4;

作成が完了したら、show tables;と入力し、usersテーブルが追加されたことを確認しておきましょう。

7_1_1.png

最初にテーブルを作成するのは、手動の時と同様に、テーブルの名前がMVCを連携させるための「命名規約」に必要なためとなります。逆にテーブルの名前さえ決まっていれば、MVCの各ファイルやクラス名、ファイルの置き場所などが定まるために、bake コマンドで自動生成を行うことが出来ます。これは規約を重んじる「CakePHP」ならではのメリットとも言えます。

7_1_2.png

Lesson 7 Chapter 2
bakeコマンドの使い方

bakeの呼び出し

bakeはコマンドライン上で操作を行うため、現在CakePHPの内臓サーバと、MySQLサーバ用の2つの画面のみを開いている場合は、新規の画面を開き cake_app/todo4 の配下にcdコマンドで移動しておきましょう。

移動ができたら以下のコマンドを入力します。

bin\cake bake --help

使用可能なコマンド一覧が表示されていれば、問題なくbakeコマンドを使うことができます。

また、bakeコマンドの型は以下となります。

bin\cake bake オプション パラメータ

helpの内容を見ると、様々なbakeコマンドがあることが分かると思いますが、本教材ではモデル・コントローラ・ビューのひな形を作成するコマンドを中心に学んでいきます。

モデルの作成

早速、先ほど作成したusersテーブルをパラメータに指定してモデルを作成しましょう。

bin/cake bake model users

7_2_2.png

最初にテーブルとエンティティが作成されていることが確認できます。また、その後にはテスト用のファイルが作られています。テストに関しては Lesson12 で触れていきます。

自動生成されたエンティティクラス

コントローラの作成に移る前に、自動生成されたファイルの中身を確認してみましょう。src/Model/Entity/User.phpを開いてください。

<?php
declare(strict_types=1);

namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher;
use Cake\ORM\Entity;

/**
 * User Entity
 *
 * @property int $id
 * @property string $username
 * @property string $password
 * @property \Cake\I18n\FrozenTime|null $created
 * @property \Cake\I18n\FrozenTime|null $modified
 */
class User extends Entity
{
  ...

    protected $_accessible = [
        'username' => true,
        'password' => true,
        'created' => true,
        'modified' => true,
    ];

    /**
     * Fields that are excluded from JSON versions of the entity.
     *
     * @var array
     */
    protected $_hidden = [
        'password',
    ];
}

ファイルの先頭にはdeclare(strict_types=1);の記述があります。これはPHP7から使用可能となった明確な型宣言を有効にするためのものです。CakePHPの話からは逸れてしまうので、型宣言についての詳細は省きますが、何のために書かれているのかを理解しておきましょう。

次に$_accessible プロパティですが、これは各フィールドへのアクセス制限を設定するもので、記述がされていない場合は全てのフィールドが許可されている状態になります。

今回だと「id」フィールドへのアクセスのみ許可されていないため、「id」フィールドの値を取得することは出来ますが、値を直接代入したり、変更したりすることは出来なくなります。

最後に$_hiddenが置かれていますが、これはサーバーからのレスポンスや、APIとデータのやり取りをする際に使用される「JSON形式」でエンティティのデータを送信する場合に、特定のフィールドを除外するために使用されます。

「password」フィールドの値は、そもそもクライアントなどの他者に見せる必要が無いため、セキュリティの面から $hiddenプロパティにセットされています。

自動生成されたテーブルクラス

次にテーブルクラスです。src/Model/Table/UsersTable.phpを開きます。

UsersTable.php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

...

class UsersTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp', [
            'created' => 'created_on',
            'modified' => 'modified_on',
            'events' => [
                'Model.beforeSave' => [
                    'created_on' => 'new',
                    'modified_on' => 'always',
                ]
            ]
        ]);
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->scalar('username')
            ->maxLength('username', 255)
            ->requirePresence('username', 'create')
            ->notEmptyString('username');

        $validator
            ->scalar('password')
            ->maxLength('password', 255)
            ->requirePresence('password', 'create')
            ->notEmptyString('password');
            
        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
    {
        $rules->add($rules->isUnique(['username']), ['errorField' => 'username']);

        return $rules;
    }
}

メソッドの内容を一つずつ見ていきましょう。最初にinitializeメソッドです。

UsersTable.php
public function initialize(array $config): void
{
    parent::initialize($config);
    $this->setTable('users');
    $this->setDisplayField('id');
    $this->setPrimaryKey('id');
    $this->addBehavior('Timestamp', [
        'created' => 'created_on',
        'modified' => 'modified_on',
        'events' => [
            'Model.beforeSave' => [
                'created_on' => 'new',
                'modified_on' => 'always',
            ]
        ]
    ]);
}

initializeはクラスのインスタンス生成時に自動的に発動するメソッドで、クラスの初期設定などに使用されます。内容を見ると Lesson6 のfind('list')の説明で触れた $this->setDisplayField('id'); などがありますが、ここでは新しく出てきた機能を中心に解説をしていきます。

  • $this->setTable('users');

    モデルが扱うテーブルを指定するものですが、今回はCakePHPの命名規則に従っているため、扱うテーブルはsetTableを使わずとも定まっています。そのためこの記述はなくとも処理自体には問題ありません。モデルの扱うテーブルが命名規則に従っていなかったり、別のデータベースに存在する場合など、例外的な場合に手動で対象のテーブルを指定できるようにするためのものです。

  • $this->setPrimaryKey('id');

    プライマリキー(主キー)として扱うフィールドの名前を指定します。設定しない場合にはidというフィールドを主キーとして扱うので、今回のようにidを主キーとしている場合には記述しなくとも問題ありません。ただ、明示していることでわかりやすくなります。主キーとするフィールドの名前をid以外にする場合や、複合キーが存在する場合などに使用されます。複合キーの詳細は省きますが、2つのフィールドの値を組み合わせることで一意のデータとなることの出来る構造のキーとなります。

  • $this->addBehavior('Timestamp');

    BehaviorについてはLesson10で詳しく見ていきますが、addBehaviorメソッドを使うことでモデルに追加の機能を付け加えることが可能となります。今回のTimestampはテーブルにレコードが作成された日時と、レコードが更新された日時を自動的に更新する機能を持ちます。

続いてvalidationDefaultメソッドを見ていきましょう。 Lesson5 でも設定を行いましたが、エンティティにデータを入れるタイミングで行われるバリデーションの内容を設定します。今回は「username」と「password」のバリデーションが記述されていることがわかります。

UsersTable.php
public function validationDefault(Validator $validator): Validator
{
    $validator
        ->scalar('username')
        ->maxLength('username', 255)
        ->requirePresence('username', 'create')
        ->notEmptyString('username');
    $validator
        ->scalar('password')
        ->maxLength('password', 255)
        ->requirePresence('password', 'create')
        ->notEmptyString('password');
        
    return $validator; 
}
  • scalar('username')

    scalar()メソッドは引数の値がスカラー値になっているかを見ます。スカラー値とは整数、浮動小数点数、文字列、または真偽値のどれかとなりますが、つまりはスカラー値ではない配列やオブジェクト、nullなどが入っていないかをチェックしてくれます。

  • requirePresence('username', 'create')

    requirePresence()メソッドはこれからパッチしようとしているデータの中に、指定のフィールドの値が存在するかをチェックします。これは、フォーム送信などで入力必須の項目がある場合などに有用です。今回だと、エンティティを作成する際にusernameフィールドの値は必ず必要です。そのため、第一引数に「username」、第二引数のcreateで新規エンティティを作成する場合のみ確認するようにしています。常にチェックをする場合にはcreateの変わりに「always」、更新時のみチェックするには「update」を第二引数に渡します。

passwardに対してのバリデーションも同様の内容です。

最後にbuildRules()メソッドです。

UsersTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
    $rules->add($rules->isUnique(['username']), ['errorField' => 'username']);
    return $rules;
}

これは、validationDefault() 同様にバリデーションを行うためのメソッドとなりますが、バリデーションを行う「タイミング」が異なります。

validationDefault()メソッド はエンティティにデータが入力される時にバリデーションを行うのに対して、buildRules()メソッドは save()メソッド が呼び出され、エンティティが保存される直前でのデータ検証を行います。

buildRulesメソッドでは RulesCheckerクラスのインスタンスとして引数にセットされている $rules にadd()メソッドを使ってバリデーションルールの追加を行う形で実行するバリデーションの設定を行います。

今回追加されているisUnique()メソッドは、テーブル内にこれから投入しようとしているデータとすでに同じ値があるかどうかをチェックします。今回だと users テーブル内に、投入しようとしている username の値がすでに存在しているかのチェックを行います。

もし、usernameの値が一意でなかった場合にはエラーが返ります。このとき、add()メソッドの第二引数にある「errorfield => username」によって、エラーがusernameフィールドで発生していることを特定出来るようになっています。

コントローラの作成

次にコントローラを自動生成しましょう。コマンドは以下となります。

bin/cake bake controller users

無事に作成できたら、src/Controller/UsersController.php を開いて確認してみましょう。CRUDに必要なアクションが生成されていることが分かるかと思います。

手動で作成したものと処理の流れは変わらないので、ある程度読み解くことが出来ると思いますが、一部で初めて登場したメソッドなどもあるので、補足としての説明をしていきます。

compactメソッド

各アクションのsetメソッドを見るとcompact()というメソッドが使われています。

UsersController.php
public function index()
{
    $users = $this->paginate($this->Users);
    $this->set(compact('users'));
}

compact()メソッドは、引数に指定した変数名と同じ名前の変数を配列として返します。以下にcompact()の簡単な使用例とその結果を示します。

$name = 'John';

$data = compact('name');
print_r($data);
// 出力結果

Array
(
    [name] => John
)

このように、「[変数名] => 変数の値」の形を返すため、「this->set(compact('users'))」は「this->set(['users' => $users])」と同意となり、「users」を2回記述する冗長な書き方を短縮することができます。

ちなみにですが、コントローラのset()メソッドは以下の2つの書き方、どちらでも使うことができます。

$this->set('users',$users);

$this->set(['users' => $users]);

また、今回のコードでは見られないですが、compact()メソッド は複数の変数名を取ることも可能です。

$this->set(compact('data1','data2'));

次に、viewとeditアクションで使われている「'contain' => []」という記述についてです。

UsersController.php
public function view($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => [],
        ]);

        $this->set(compact('user'));
    }

テーブルクラスの getメソッドで第二引数として使用されている「'contain' => []」は、複数のテーブルを組合わせて使う「アソシエーション」の設定がある場合に必要となります。例えば「'contain' => ['tasks']」のように、関連を持たせた別のテーブルを指定することで、usersテーブルのデータと一緒に、tasksテーブルのデータも一緒に取得することが可能となります。(詳しくは Lesson9 で見ていきます)

最後にeditアクションの「$this->request->is(['patch', 'post', 'put'])」とdeleteアクションの「$this->request->allowMethod(['post', 'delete']);」部分についてです。

UsersController.php
public function edit($id = null)
{
    $user = $this->Users->get($id, [
        'contain' => [],
    ]);
    if ($this->request->is(['patch', 'post', 'put'])) {
        $user = $this->Users->patchEntity($user, $this->request->getData());
        if ($this->Users->save($user)) {
            $this->Flash->success(__('The user has been saved.'));
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('The user could not be saved. Please, try again.'));
    }
    $this->set(compact('user'));
}
public function delete($id = null)
{
    $this->request->allowMethod(['post', 'delete']);
    $user = $this->Users->get($id);
    if ($this->Users->delete($user)) {
        $this->Flash->success(__('The user has been deleted.'));
    } else {
        $this->Flash->error(__('The user could not be deleted. Please, try again.'));
    }
    return $this->redirect(['action' => 'index']);
}

tasksContorllerを手動で作成した時は、リクエストメソッドがpostかどうか、もしくはチェック無しで処理をおこなっていましたが、リクエストメソッドには「POST」「GET」以外にも様々な種類があり、「PUT」はデータの更新、「DELETE」はデータの削除など、メソッドのタイプによって役割が異なります。

これらは基本的に「POST」で代用可能なのですが、例えば「API」と呼ばれるアプリケーション間でのやり取りに使用される機能を使う場合などに「POST」以外のメソッドが使用されるケースが考えられるため、開発の内容によっては「POST」「GET」以外のリクエストメソッドが使われることを想定しておく必要があります。

今回は外部のアプリケーションと連携を行ったりする予定はないため、POSTのみの記述でも基本的には問題ありません。

次に、リクエストメソッドを判定している is()メソッドとallowMethod()メソッドですが、is()メソッドは判定結果として「true」か「false」を返すのに対して、allowMethod()メソッドの場合は指定したリクエストメソッドと一致しない場合「エラーを返す」という違いがあります。

そのため、is()メソッドはaddとeditアクションのif文で使われており false の場合もそのままset()メソッドが実行されていきますが、deleteアクションでは false の場合にフラッシュでエラーを出し、処理が止まる流れとなっています。

テンプレートの作成

最後にテンプレートの作成です。以下のコマンドを入力しましょう。

bin/cake bake template users

各アクションに対応したビューテンプレートが自動生成されたことが確認できたら、早速http://localhost:8765/usersをブラウザで開いてみましょう。

7_2_3.png

右上にnew userボタンがあるので、クリックしてユーザーを追加してみます。

7_2_4.png

適当なusernameとpasswardを入力してsubmitをクリックします。

7_2_5.png ユーザーが追加されたことが確認できます。右端の項目にActionsがあり、各アクションへのリンクをまとめて表示しています。viewとeditの画面もリンクをクリックして確認しておきましょう。

画面の構成が確認できたので、各テンプレートファイルを見ていきます。

index.php

まず上部と下部にpaginator(データを並べ替えたり、ページごとに分割して表示する機能)についての記述があり、上部ではカラム名をクリックすることで、データの並び順を昇順か降順か選択できるようにしています。

<h3><?= __('Users') ?></h3>
<div class="table-responsive">
    <table>
        <thead>
            <tr>
                <th><?= $this->Paginator->sort('id') ?></th>
                <th><?= $this->Paginator->sort('username') ?></th>
                <th><?= $this->Paginator->sort('created') ?></th>
                <th><?= $this->Paginator->sort('modified') ?></th>
                <th class="actions"><?= __('Actions') ?></th>
            </tr>
        </thead>
下部のほうでは、分割したページ間の移動を行うためのリンクを表示させています。
<div class="paginator">
    <ul class="pagination">
        <?= $this->Paginator->first('<< ' . __('first')) ?>
        <?= $this->Paginator->prev('< ' . __('previous')) ?>
        <?= $this->Paginator->numbers(['before' => '[','after' => ']']) ?>
        <?= $this->Paginator->next(__('next') . ' >') ?>
        <?= $this->Paginator->last(__('last') . ' >>') ?>
    </ul>
    <p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
</div>

paginatorについては Lesson11 で詳しく見ていきます。

ほかに foreach構文中のlink()メソッドで「__()」が使われていますが、これは多言語対応を行う場合に必要となるヘルパーの一種になります。

<?php foreach ($users as $user): ?>
<tr>
    <td><?= $this->Number->format($user->id) ?></td>
    <td><?= h($user->username) ?></td>
    <td><?= h($user->created) ?></td>
    <td><?= h($user->modified) ?></td>
    <td class="actions">
        <?= $this->Html->link(__('View'), ['action' => 'view', $user->id]) ?>
        <?= $this->Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?>
        <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?>
    </td>
</tr>
<?php endforeach; ?>

__()を使うことで予め用意しておいた多言語対応用のファイルにアクセスし自動的に翻訳した内容を返すことができます。上記の場合だと、'View'や'Edit'というテキストを日本語で出力するようにしたり出来ます。多言語対応についても Lesson11 で改めて見ていきます。

また、その後の'Delete'のリンクとして使われているpostLink()メソッドですが、これは POST送信でのリンクを作成するためのヘルパーで、以下の形で使用します。

postLink(表示テキスト,リンク先、オプション);

今回だとテキストに'delete'、URLにdeleteアクション+idの値、第三引数であるオプションにはconfirmが指定されています。

第二引数のリンク先の指定にはいくつかの書き方があるので以下に示します。

  • アクション名を指定する方法

    ['action' => 'delete']

    現在のコントローラが持っているアクションを指定します。

  • URLを直接指定する方法

    '/users/delete'

    コントローラとアクションを含むURLを直接指定します。

  • ルーティングパラメータを含むURLを指定する方法

    ['controller' => 'users', 'action' => 'delete', $user->id]

    'controller'と'action'キーにコントローラ名とアクション名を指定し、その後に必要なパラメータを追加します。今回はこの形が使用され、コントローラの指定が省略された状態となっています。

第三引数の confirm は JavaScript の Confirm 関数を呼び出すオプションであり、ユーザーに実行して良いかの確認を行う際のテキスト内容が記述されています。

view.php

index.phpでの説明を踏まえると、ほぼ特筆する部分はありませんが、idの表示を行っている部分で使われている「$this->Number->format($user->id)」についてのみ補足をします。

<h3><?= h($user->id) ?></h3>
<table>
    <tr>
        <th><?= __('Username') ?></th>
        <td><?= h($user->username) ?></td>
    </tr>
    <tr>
        <th><?= __('Id') ?></th>
        <td><?= $this->Number->format($user->id) ?></td>
    </tr>
    <tr>
      ...

「$this->Number->format()」はNumberHelperメソッドの一つであり、「数字」を様々なフォーマットで出力させることが出来ます。今回は「フォーマット」を指定する第二引数が省略されているためデフォルト扱いとなり、小数点以下が表示されないフォーマットになっています。各種ヘルパーについては Lesson10 で改めて触れていきます。

add.phpとedit.php

この2つについては、HTMLの内容になってしまいますが、divタグのほかに、fieldsetとlegendタグを使用してフォームの構造をわかりやすく記述しているといった点以外には補足説明はありません。

<?= $this->Form->create($user) ?>
<fieldset>
    <legend><?= __('ユーザーの追加') ?></legend>
    <?php
        echo $this->Form->control('username');
        echo $this->Form->control('password');
    ?>
</fieldset>
<?= $this->Form->button(__('決定#039;)) ?>
<?= $this->Form->end() ?>

fieldsetタグは、例えばフォームの内容を「個人情報」「配送先」などいくつかのグループに分ける場合に使用し、legendタグでグループごとの見出しを作成することが出来ます。

以上で、bakeで自動作成されるファイルの確認と説明を終わります。

その他のコマンド

all

ここまで、bakeコマンドでmodel,controller,templateを一つずつ作成してきましたが、これらを一括で作成するための「all」があります。

bin\cake bake all
と打つだけで、model,controller,templateを一度に自動生成することが出来ます。

migration

DB構成などのデータをプログラムで管理するためのマイグレーションファイルを作成できます。 Lesson11 でマイグレーションについて詳しく見ていきます。