CakePHP3の1歩目 #3

srcディレクトリをいじってCRUD操作

Controllerを編集する

規約で、[アプリケーション]Controller.php というファイル名にすることが義務付けられている。
今回は前回bakeで作成した、src/Controller/UsersController.php をいじっていく。

まずURLとControllerの関係性を確認する

class UsersController extends AppController
{
    public function index()
    {
        // $users = $this->paginate($this->Users);

        // $this->set(compact('users'));
        // $this->set('_serialize', ['users']);

        $this->autoRender = false;
        echo "hello!";
    }

すげえ雑に書き換えてみたら http://[ドメイン]/Cake/users のトップの表示が hello! になった。
上記URLはアドレスのアクションが省略されている状態であり、この場合はまずindex()が呼ばれるんだとか。

以下、同様にUsersControllerをいじる。

    public function index($param = 'empty', $param2 = 'emp!')
    {
        // $users = $this->paginate($this->Users);

        // $this->set(compact('users'));
        // $this->set('_serialize', ['users']);

        $this->autoRender = false;
        echo 'hello!<br>' . "\n";

        echo '第一パラメータ: ' . $param . '<br>' . "\n";
        echo '第二パラメータ: ' . $param2 . '<br>' . "\n";
    }

http://[ドメイン]/Cake/users はもちろん empty, emp! が表示される。

CakePHPの仕様として、URLのアクションの次の階層に文字列を書くと、それが引数としてアクションに渡される。
つまり、http://[ドメイン]/Cake/users/index/aiueo とすると、indexアクションにaiueoが引数として渡されるということになる。
http://[ドメイン]/Cake/users/index/sadone は sadone, emp! が表示されるし、
http://[ドメイン]/Cake/users/index/sadone/kanon は sadone, kanon が表示される。
また、indexをURLから省略してしまうと(/Cake/users/sadone)、もちろんusersアプリケーションのアクションとしてsadone()を探してしまうのでエラーが表示される。

パラメータの渡し方と取り出し方としては後述のクエリパラメータの方が分かりやすいとは思う。

この例で、URLにより指定されたControllerに処理が渡されるということが分かった。

setAction

あるアクションの実行中に他のアクションに移りたいときに使用するそうです。
URLで実行するアクションを指定できることは分かったけど、そのアクションの移行したい時にredirect()を使うとURLが書き換わってしまうのがとてもダサいので、そういった場合に使われるんだとか。

第1引数: 移動先アクション,
第2引数(可変長引数): 第1引数で指定したアクションに渡す引数

    public function index($param = 'empty', $param2 = 'emp!')
    {
        $this->autoRender = false;
        echo 'hello!<br>' . "\n";

        echo '第一パラメータ: ' . $param . '<br>' . "\n";
        echo '第二パラメータ: ' . $param2 . '<br>' . "\n";

        $this->setAction('test');
        echo 'after test()<br>';
    }

    public function test(){
        echo 'test<br>';
    }

パラメータの後に test, after test() と表示された。

    public function test(){
        echo 'test<br>';
        sleep(1);
        echo 'after sleep<br>';
    }

念のためsleepも挟んでみたが、test, after sleep, after test()の順で表示される。
というのもsetAction()が非同期(別スレッド)で実行されているのかどうかの確認。
普通に同期実行でした。

また、下記のようなフローで実行されるので扱いが若干トリッキー

  • 移動元アクションの実行
    • 移動先アクションの実行
      • 移動元アクションの続きを実行
        • 移動先アクションのViewを表示する
          • 移動元アクションのViewを表示する

$this->render()で任意のViewでレンダリングするなどの処理が取れるらしいが今はノータッチ

見た目を編集する

  • View: 表示の処理部分
  • Template: 表示するテンプレート
    • .ctpファイル(CakePHP Templateの略らしい)

ViewとTemplateの切り分けって俺の勝手な印象だと、いじるのは基本的にTemplateだけって気がするがどうなんだろう?

bakeした後のテンプレートを追ってみると、
src/Template/Layout/default.ctp(上部バー) の中で $this->fetch('content')の呼び出しを行い、src/Template/Users/index.ctp(コンテンツ部) が出力されている。

(いい加減bakeに縛られるのが嫌なので、空のプロジェクト test を作成します)

ControllerからViewに値を渡す

コントローラ側の $this->set() で変数を定義し、
ビュー側の <?= $var ?> で変数を出力するだけ。

// TestController.php
class TestController extends AppController
{
	public function index(){
		$this->set('welcome', 'ようこそ');
	}
}
// index.ctp
<div>
    <h3>Index Page</h3>
    <?= $welcome ?>
</div>

ようこそと表示される。

<?= ?> はsmartyのデリミタみたいなもんでしょうか
(あれもテンプレートに使うし)

Requestクラス: フォームからControllerに値を渡す

今回やりたいことの一つ。

ControllerのRequestプロパティが持つdata()を使って入力情報を取得します。
連想配列のdata[]も別に持ってるのでどちらを使うかは自由かな

// TestContoller.php
	public function index(){
		$this->set('welcome', 'ようこそ');

		$str = $this->request->data('text1');
		if ($this->request->is('get')) {
			$this->set('input', '');
		} else if (! empty($str)) {
			$this->set('input', $str);
		} else {
			$this->set('input', '入力されていません。');
		}
	}
}
// index.ctp
<div>
    <h3>Index Page</h3>
    <?= $welcome ?>

    <form method="post" action="/Cake/test/index">
        <input type="text" name="text1">
        <input type="submit">
    </form>

    <?= $input ?>
</div>

$inputに出力されます。

$this->request->is('get') 等でHTTPリクエストのメソッドがGETであるかどうかを判定できる。
今回、ページの表示時(GET時)に"入力されていません。"が表示されてしまうので、このように分岐する必要がある。

クエリパラメータ

http://[ドメイン]/[アプリケーション]/[アクション]?id=***&name=*** みたいなよくある奴。
これを取り出すには $this->request->query() を呼び出せばいい。

// TestController.php
		$id = $this->request->query('id');
		$name = $this->request->query('name');
		if (! empty($id)) {
			$this->set('id', $id);
		} else {
			$this->set('id', '入力されていません。');
		}
		if (! empty($name)) {
			$this->set('name', $name);
		} else {
			$this->set('name', '入力されていません。');
		}
// index.ctp
    <div>
        クエリパラメータ<br>
        id: <?= $id ?><br>
        name: <?= $name ?><br>
    </div>

(別にCakePHPじゃなくても楽に書けるところだけど)

フォームヘルパー

Controllerが持つFormインスタンスがいろいろと補助関数を持ってるんでそれを呼び出します。
普通にformを記述するよりも直感的に分かりやすいものが書けるのが利点

  • $this->Form->create(): formの開始タグを生成
    • 第1引数: DB等のモデルを指定
    • 第2引数: オプションを連想配列で指定
  • $this->Form->text(): 入力フィールドを生成
    • 引数: name属性を指定
  • $this->Form->submit(): 送信ボタンを生成
    • 引数: 送信ボタンに表示するテキスト
  • $this->Form->end(): formの終了タグを生成
// TestController.php
		$str = $this->request->data('text1');
		if ($this->request->is('get')) {
			$this->set('input', '');
		} else if (! empty($str)) {
			$this->set('input', $str);
		} else {
			$this->set('input', '入力されていません。');
		}
// index.ctp
        <?= $this->Form->create(null, [
                'type' => 'post',
                'url' => [
                    'controller' => 'test',
                    'action' => 'index',
                ]
            ]);
        ?>
        <?= $this->Form->text('text1') ?>
        <?= $this->Form->submit('送信') ?>
        <?= $this->Form->end() ?>
        <?= $input ?>

Controllerの方は前回のformからまったくいじってません。
これで無事フォームが作成できました。

問題がありまして、デリミタの中にcreate()からend()までセミコロンで改行して、命令を複数行記述してみたのですが、いまいち上手く動作しませんでした。
いちいちデリミタで括らないといけないみたいで面倒, 複数行記述するための文法があるのかもしれない
(bakeで生成されたコードを見ていたら複数行書きたいところは<?php ?>が使われてました)

以下は他に追加できるフィールド

// パスワード入力フィールド: 入力内容が隠蔽されるやつ
<?= $this->Form->password('pass1') ?>

// 非表示フィールド
// ユーザには入力させたくない特定の値を送信したいときとか, 使い方次第
<?= $this->Form->hidden('hidden1', ['value' => 'hid']) ?>

// テキストエリア: 複数行入力できる入力フィールド
<?= $this->Form->textarea('area1') ?>

// チェックボックス: ラベルはlabel()で追加する必要がある
<?= $this->Form->checkbox('cb1', ['value' => 'sad']) ?>
<?= $this->Form->label('sadLabel', 'サド') ?>

// ラジオボタン
<?= $this->Form->radio('radio1', [
    ['value' => 'sadone', 'text' => 'サドネ'],
    ['value' => 'kanon', 'text' => '花音']
    ])
?>

// セレクトボックス
// 第3引数: multipleをtrueにすると複数行選択可能のリストになる, falseならプルダウンメニュー
<?= $this->Form->select('select1', [
    ['value' => 'sadone', 'text' => 'サドネ'],
    ['value' => 'kanon', 'text' => '花音']
    ],
    ['multiple' => true])
?>

// 年月日の入力ボックス
// 年単体(year())とか時間(time())とかも存在する
<?= $this->Form->date('date1', [
    'minYear' => date('Y') - 10,
    'maxYear' => date('Y'),
    ])
?>

バリデーションの方も手を出してみたかったのですが、公式ドキュメントではModelの方で制御してるみたいなので、ひとまずモデルの仕様の方を見て行きたいと思います。

Modelを編集する

CakePHPのインストールフォルダを見ると src/Model というディレクトリがあり、その中にいくつかのディレクトリが存在する。

  • Table: テーブルの情報を保持し、規約を定めるもの
    • バリデーションとかはここ
    • DBの情報を直接持つことになるので位置的にはDBに近い存在
  • Entity: 問い合わせ結果のレコード単位のインスタンス, 取り出した値の管理
    • Tableから得た情報を保持するって感じでしょうか
  • Behavior: Tableクラスに記述するロジックを抽出したもの
    • 別のTableクラスにも同じ操作を提供することが出来る
      • 直接ロジックを記述しなくなるので、Strategyパターンみたいにロジックそのものの動的切り替えも楽にできるかも
    • つまりは資産の再利用ですね

位置的には DB - [Table(+Behavior) - Entity] - PHP になるのかな
ドキュメントを見た感じではTableクラスに特に記述する印象だ

デフォルトのバリデーション

usersテーブルの仕様は#2に記載したとおり。

// UsersTable.php
    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmpty('id', 'create');

        $validator
            ->integer('age')
            ->requirePresence('age', 'create')
            ->notEmpty('age');

        $validator
            ->email('email')
            ->requirePresence('email', 'create')
            ->notEmpty('email');

        return $validator;
    }
  • $valiatorに対して
    • カラムごとの型に対応する関数の呼び出し
      • 各バリデーション
  • requirePresence(): フィールドが存在することをチェックするもの
    • 第1引数: カラム名
    • 第2引数: チェックするタイミングの指定, createならcreate実行時
  • allowEmpty(): フィールドの値が空であることを許す
  • notEmpty(): フィールドの値が空であることを許さない

idに主キー設定してたはずなんだけど、bakeはallowEmpty()を付与している。
(ちょっとよくわからない)

メール用のバリデーションがあるような書き方がされてるけどsublimeがemail()定義元に飛んでくれない。

DBへレコードを追加するフォーム

新規にDBを作成してbakeします。

[root@localhost ~]# mysql -u root -p
mysql> use cake_test
mysql> CREATE TABLE testusers (
    -> id INT AUTO_INCREMENT PRIMARY KEY,
    -> age INT(3) NOT NULL,
    -> email VARCHAR(255) NOT NULL,
    -> created DATETIME,
    -> modified DATETIME);
[root@localhost ~]# cd /var/www/html/Cake
[root@localhost Cake]# bin/cake bake model testusers
// src/Template/Testusers/add.ctp を作成する
<div>
    <h3>ユーザ追加</h3>
    <fieldset>
        <?= $this->Form->create(null, [
                'type' => 'post',
                'url' => [
                    'controller' => 'testusers',
                    'action' => 'add',
                ]
            ]);
        ?>
        <?php
            echo $this->Form->control('age');
            echo $this->Form->control('email');
        ?>
        <?= $this->Form->submit('送信') ?>
        <?= $this->Form->end() ?>
    </fieldset>
</div>
// TestusersController.php
class TestController extends AppController
{	
	public function index(){
	}
	public function add() {
		if ($this->request->is('post')) {
			$user = $this->Testusers->newEntity();
			$user = $this->Testusers->patchEntity($user, $this->request->data);
			if ($this->Testusers->save($user)) {
				// indexにリダイレクト
				return $this->redirect(['action' => 'index']);
			}
		}
	}

ここまで書いて http://[ドメイン]/Cake/testusers/add にアクセスしてフォームに入力して送信。

mysql> select * from testusers;
+----+-----+--------------+---------------------+---------------------+
| id | age | email        | created             | modified            |
+----+-----+--------------+---------------------+---------------------+
|  1 |  12 | test@test.jp | 2017-09-05 22:51:33 | 2017-09-05 22:51:33 |
+----+-----+--------------+---------------------+---------------------+

レコードを追加してみると確かに追加されている。

既にデフォルトバリデーションが効いてるので、notEmptyが効いてる項目を空欄で送信してみると画面遷移しない。
(リダイレクトが働かない = $this->Testusers->save($user)がfalse)

DBから値を全て取り出す

モデルに対してfind()を呼び出すことで、特定のエンティティを取り出すことが出来る。
とりあえずfind('all')で全部のエンティティを取り出す。

// TestusersController.php
	public function index(){
        $this->set('testusers', $this->testusers->find('all'));
	}
// index.ctp
<?= $testusers ?>
SELECT Testusers.id AS `Testusers__id`, Testusers.age AS `Testusers__age`, Testusers.email AS `Testusers__email`, Testusers.created AS `Testusers__created`, Testusers.modified AS `Testusers__modified` FROM testusers Testusers 

この状態で雑に出力すると以上のようになる。

$testusersをvar_dump()してみるとCake\ORM\Queryオブジェクトであることが分かった。
このQueryを使用した仕組みがいわゆるQuery Builderで、C#LINQのようにメソッドチェーンでSQLを組み立てることができるんだとか。
とりあえずここではQueryオブジェクトをforeachで全件表示してみます。

// index.ctp
        <table>
            <thead>
                <tr>
                    <th>id</th>
                    <th>age</th>
                    <th>email</th>
                    <th>created</th>
                    <th>modified</th>
                </tr>
            </thead>
            <tbody>
                <?php
                    foreach ($testusers as $user) {
                        echo '<tr>';
                        echo '<td>' . h($user->id) . '</td>';
                        echo '<td>' . h($user->age) . '</td>';
                        echo '<td>' . h($user->email) . '</td>';
                        echo '<td>' . h($user->created) . '</td>';
                        echo '<td>' . h($user->modified) . '</td>';
                        echo '</tr>';
                    }
                ?>
            </tbody>
        </table>


cssが効いてるからきれいに表示された。

DBの値を書き換える

レコードの作成時はnewEntity()で新しくエンティティを作成したが、更新時はエンティティをget()で取得する。
フォームの入力情報をエンティティにpatchEntity()でマージしてsave()するのは変わらない。

create()の引数にエンティティを投げると、エンティティの値が入力フィールドに既定値として設定される模様。
また、引数にエンティティを投げた場合はPUTメソッドによる送信となるので、コントローラを記述する時は若干注意。

// edit.ctpを作成
<div>
	<h3>ユーザ情報編集</h3>
	<?= $this->Form->create($user) ?>
	<fieldset>
		<?php
			echo $this->Form->control('id');
			echo $this->Form->control('age');
			echo $this->Form->control('email');
			echo $this->Form->control('created');
			echo $this->Form->control('modified');
		?>
	</fieldset>
	<?= $this->Form->submit('送信') ?>
	<?= $this->Form->end() ?>
</div>
// TestusersController.php
    public function edit($id = null) {
        $user = $this->Testusers->get($id);
        if ($this->request->is(['post', 'put'])) {
            $user = $this->Testusers->patchEntity($user, $this->request->data);
            if ($this->Testusers->save($user)) {
                return $this->redirect(['action' => 'index']);
            }
        } else {
            $this->set('user', $user);
        }
    }

editアクションの引数としてidを渡すことにした。
http://[ドメイン]/Cake/testusers/edit/1 ならid=1のエンティティの編集画面に移る。

indexは全エンティティを一覧表示するようになっているので、editへのリンクを作ることにする。

サイト内リンク

bakeの自動生成部分を参照

<?= $this->Html->link(__('New User'), ['action' => 'add']) ?>
  • link()
    • 第1引数: __() でリンクを貼るテキストを指定
    • 第2引数: どのアクションに遷移するかを指定

公式ドキュメントでは以下のような書き方もしている。
こちらの方が理解しやすいかもしれない

echo $this->Html->link(
    'Enter',
    '/pages/home',
    ['class' => 'button', 'target' => '_blank']
);

本題である編集画面へのリンクだが、foreach内でeditへのリンクを貼る。

echo '<td>' . $this->Html->link('edit', ['action' => 'edit/' . $user->id]) . '</td>';

DB内の値を削除する

DBからget()でエンティティを取り出してdelete()するだけ。
edit.ctpの時とあまり変わらないが、今回は削除なので入力フィールドを設ける必要がない。
また、データをマージする操作も不要である。

// delete.ctp を作成
<div>
	<h3>ユーザ情報削除</h3>
	<?= $this->Form->create($user) ?>
	<fieldset>
		<p>id: <?= h($user->id) ?></p>
		<p>age: <?= h($user->age) ?></p>
		<p>email: <?= h($user->email) ?></p>
		<p>created: <?= h($user->created) ?></p>
		<p>modified: <?= h($user->modified) ?></p>
	</fieldset>
	<?= $this->Form->submit('送信') ?>
	<?= $this->Form->end() ?>
</div>
// TestusersController.php
    public function delete($id = null) {
        $user = $this->Testusers->get($id);
        if ($this->request->is(['post', 'put'])) {
            if ($this->Testusers->delete($user)) {
                return $this->redirect(['action' => 'index']);
            }
        } else {
            $this->set('user', $user);
        }
    }

editの時と同様にindexに削除画面へのリンクを貼る。

Modelを介したDBのCRUD操作をざっとやったので今回はここまで。