私は CakePHP3 系のリリースの頃から CakePHP3 を使い続けていますが、友人から質問を受けたり MeetUpイベントでお話しすることが多くなってきました。そこで感じたことですが、CakePHP3 系の便利機能を使っておらず、場合によっては CakePHP2 系の頃のようなコードを書いて苦労されている印象に思いました。
本投稿では、便利な機能を利用してコードの再利用性・可読性をあげて、少しでも幸せな CakePHP ライフを送れるように願いを込めて色々と書きます。
Helper 編
まずは気軽に試せれる便利な Template 側の機能になります。CakePHP 3 系では、sec/Template ディレクトリ内に Html に関するファイルを作成します。そのファイル内で CakePHP 標準の便利なヘルパーを利用します。
POST ボタン、POST リンク
見た目はただのボタンやリンクなんだけど、実際には form の POST で値を送りたいなんて要求に応えた機能です。標準の Form ヘルパーには 1 行で POST 送信させるボタンやリンクを書くことができます。
POST ボタンの場合
下記の一行を書くだけで以下の見た目のボタンが作成できます。
<?= $this->Form->postButton('削除する', ['controller' => 'Articles', 'action' => 'delete', 'id' => 100]) ?>
このような見た目のボタンが自動で作成されます。
なお、上記ボタンは以下の Html タグになります。
<form accept-charset="utf-8" action="/articles/delete?id=100" method="post">
<div style="display: none;">
<input name="_method" type="hidden" value="POST">
<input autocomplete="off" name="_csrfToken" type="hidden" value="5028af3ea807b812214ec78e0443d883caf0e0a544ee1d56413e2f420afd33ffeb775109b3dbf1833ad3398b7309d240004c911be66cebc0cc8fc86b81a80406">
</div>
<p>
<input name="delete" type="hidden" value="ok">
<input name="someParams" type="hidden" value="abcdef">
<button class="button" type="submit">削除する</button>
</p>
<div style="display: none;">
<input autocomplete="off" name="_Token[fields]" type="hidden" value="e8c670f10ba0e2f889c20839e890a9faad697344%3Adelete%7CsomeParams">
<input autocomplete="off" name="_Token[unlocked]" type="hidden" value="">
<input autocomplete="off" name="_Token[debug]" type="hidden" value="%5B%22%5C%2Farticles%5C%2Fdelete%3Fid%3D100%22%2C%7B%22delete%22%3A%22ok%22%2C%22someParams%22%3A%22abcdef%22%7D%2C%5B%5D%5D">
</div>
</form>
POST リンクの場合
下記の一行を書くだけで以下の見た目のボタンが作成できます。
<?= $this->Form->postLink('削除する', ['controller' => 'Articles', 'action' => 'delete', 'id' => 100]) ?>
このような見た目のリンクが自動で作成されます。
削除する書き出された html は以下の通りです。
<form name="post_5e8af0e62fcd0747886673" style="display:none;" method="post" action="/articles/delete?id=100">
<input type="hidden" name="_method" value="POST" />
<input type="hidden" name="_csrfToken" autocomplete="off" value="79b9b77ff9fa9cc30efdc833e4d219888c54d214309fbb1f091bfd930bf4b62d65659219a1de6fe54949e5e3bdccfd454d0e5c299d65e7d7ff847653a6ef2a0c" />
<div style="display:none;">
<input type="hidden" name="_Token[fields]" autocomplete="off" value="a4c1e5e57dc1c27d57dbe9cf93b45e3a7e9402c6%3A" />
<input type="hidden" name="_Token[unlocked]" autocomplete="off" value="" />
<input type="hidden" name="_Token[debug]" autocomplete="off" value="%5B%22%5C%2Farticles%5C%2Fdelete%3Fid%3D100%22%2C%5B%5D%2C%5B%5D%5D" />
</div>
</form>
<a href="#" onclick="document.post_5e8af0e62fcd0747886673.submit(); event.returnValue = false; return false;">削除する</a>
リンククリック時の確認ダイアログ表示
地味にありがたい機能で、見た目を気にせずに確認ダイアログを表示させたい場合、リンクのヘルパーに引数を渡すことで実現できます。これを知らずに独自で js の confirm() を実装されている方は是非一度お試しください。
// この confirm オプションを渡す
<?= $this->Html->link('記事を削除する',
['controller' => 'Articles', 'action' => 'delete', 'id' => 100],
['confirm' => '削除してよろしいですか?']
) ?>
このような見た目のリンクが自動で作成されます。
記事を削除する書き出された html は以下の通りです。
<a href="/articles/delete?id=100" onclick="if (confirm("\u524a\u9664\u3057\u3066\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f")) { return true; } return false;">記事を削除する</a>
自動リンク化
限定的な利用範囲に限った方がいいかと思いますが、文章内のテキストから自動でリンクを作成させる諸刃の剣のような機能になります。テキストを全部渡すと、その中から email アドレスと url を自動でリンクに変換してくれます。文字列の区切りを伝えるために私は半角スペースを利用しました。私は管理画面からニュースを登録する場合に利用しています。
// リンク化される該当の文字列の前後にスペースを入れると確実です
<?= $this->Text->autoLink('ダミーテキスト内部に [email protected] という文字列や http://example.com などのURLを含めると勝手にリンク化されます。') ?>
このような見た目の Html が書き出されます。
ダミーテキスト内部に [email protected] という文字列や http://example.com などのURLを含めると勝手にリンク化されます。書き出された Html は以下の通りです。
ダミーテキスト内部に <a href="mailto:[email protected]">[email protected]</a> という文字列や <a href="http://example.com">http://example.com</a> などのURLを含めると勝手にリンク化されます。
テキストの段落化
こちらも管理画面からのニュースなどに利用していますが、1 行の改行は br タグ、2 行の改行は p タグでくくられます。以前は CKEditor 等を利用していましたが、サイトのお知らせやニュース等では改行とパラグラフとリンクくらいしか利用しなくなってきたため、こちらで十分になりました。DB に値を投入する画面では単純なテキストボックスにしておき、DB から内容を取得してきた時に表示側で改行や段落化させています。
<?= $this->Text->autoParagraph("ダミーテキスト内部に改行を見つけ、
1 行の場合は br タグで改行を表現し、
2 行の場合は p タグでくくられます。") ?>
このような見た目の Html が書き出されます。
ダミーテキスト内部に改行を見つけ、
1 行の場合は br タグで改行を表現し、
2 行の場合は p タグでくくられます。
書き出された Html は以下になります。
<p>ダミーテキスト内部に改行を見つけ、<br>
1 行の場合は br タグで改行を表現し、</p>
<p>2 行の場合は p タグでくくられます。</p>
テキストの切り詰め
この機能はいろんなところでよく使っています。「…続きを読む」みたいな表現は色々な場面で登場するので必須です。
<?php $text = '山路やまみちを登りながら、こう考えた。智ちに働けば角かどが立つ。情じょうに棹さおさせば流される。意地を通とおせば窮屈きゅうくつだ。とかくに人の世は住みにくい。住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。'; ?>
<?= $this->Text->truncate($text, 60, ['ellipsis' => '...続きを読む']) ?>
このような見た目のテキストが書き出されます。
山路やまみちを登りながら、こう考えた。智ちに働けば角かどが立つ。情じょうに棹さおさせば流される。意地を通…続きを読む Table (Model) 編
CakePHP2 系ではモデルクラスにテータ取得処理やバリデーション、また、取得したデータの加工などを記述して、これをビジネスロジックと呼んでコントローラーから呼び出していましたが、CakePHP3 系では、モデルはテーブルとエンティティに分かれました。テーブルでは DB 接続に関することや取得条件、値のバリデーション、スキーマに関するルールなどを実装します。エンティティは後述します。
カスタムファインダー
会員制サイトを構築する際、例えばユーザに関して以下の条件が加わることがあります。
- メールや電話番号などの疎通確認済みかどうか
- 有料プランユーザかどうか
こういった場合、カスタムファインダーで以下のように実装するとコードの再利用性が高まります。カスタムファインダはテーブルクラス内、findXXXXから始まる関数を実装することで利用が可能になります。
// テーブル内、下記のようにカスタムファインダを実装
class UsersTable extends Table
{
// 疎通確認済みの条件
public function findActive(Query $query, array $options)
{
return $query->where(['Users.status' => 1]);
}
// 有料プランの条件
public function findPaid(Query $query, array $options)
{
return $query->where(['Users.premium' => 1]);
}
}
上記のように記述することで、カスタムファインダの条件を記述しました。すると以下のように例えばコントローラ側で利用する時に疎通確認済みユーザ一覧や有料プランユーザ一覧、また疎通確認済み&有料プランユーザ等の条件で find が簡単に利用できます。
// コントローラ内、下記のように利用
// (この時点では SQL 文の作成しか行われず実行されませんが便宜上取得と書きました)
class UsersController extends AppController
{
// 疎通確認済みのユーザの取得
$active = $this->Users->find('active');
// 有料プランのユーザの取得
$paid = $this->Users->find('paid');
// 疎通確認済み & 有料プランのユーザの取得
$premium = $this->Users->find('active')->find('paid');
}
このようにいろんな条件を繋ぐことができるので、ある条件に追加で別の条件を加えたい場合は、簡単に実装ができます。また、コードのメンテナンス性が上がるので、システムの仕様変更に合わせて柔軟に組み合わせることが可能です。
validationDefault(バリデーション)と buildRules(ルール)の使い分け
ちょっと横道にそれますが、よく聞かれることなのでこのタイミングで使い分けについて記述します。
バリデーションには DB 保存時の値に関する条件を記述し、ルールには schema に関する条件を記述します。例えば nickname カラムを varchar(255) とした場合、バリデーションには nickname の文字数や半角英数のみなど、値に関する条件を記述します。ここで、nickname にユニーク制限をかけたい場合、ルールにユニーク制限を記述します。こうすることで、DB アクセスが必要なエラーチェックと不要なエラーチェックを分けることができ、DB へアクセスする回数を減らすことが可能になります。
Entity (Model) 編
こちらは DB から取得した値(1レコード)に関する処理を主に書きます。仮想プロパティや自動オブジェクト変換機能を使うと、値に関する処理が非常に簡単になります。
仮想プロパティ
短縮した値の受け取りや初期値のセット
例えば以下のようにコードを書くと、UUIDの短縮表示などを表現できます。管理画面でのお問い合わせの一覧表示など、値を丸めて視認性をあげたい時に私は使ったりしてます。(表示上ユニークではなくなる可能性がありますが、値を上書きするわけではないです)
また、null を許可しているけど明確に null だった場合は「–」と表示したい場合にも便利です。こうすることでテンプレート内に条件分岐を書く必要がなくなります。DB から取得した値に関する処理はエンティティに書くことでコードの可読性が上がります。
// エンティティファイル内で記述
class Contact extends Entity
{
protected function _getIdShort()
{
return Text::truncate($this->_properties['id'], 13, ['ellipsis' => null]);
}
protected function _getViaCodeDp()
{
return $this->_properties['via_code']? $this->_properties['via_code'] : '--';
}
}
protected function _getXXXXX と書くことで仮想プロパティを設定することができます。私は非常によく使いますが、意外とテンプレートファイル側で同じ条件分岐の処理を繰り返し書いているケースを見たので、こちらを利用した方がいいです。
テンプレート側ではこのように利用します。
<table>
<thead>
<tr>
<th><?= $this->Paginator->sort('id', 'お問い合わせID') ?></th>
<th><?= $this->Paginator->sort('created', '受信日時') ?></th>
<th><?= $this->Paginator->sort('title', 'タイトル') ?></th>
<th><?= $this->Paginator->sort('via_code', '経路コード') ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><?= $contact->id_short ?></td>
<td><?= $contact->created->format('Y年m月d日') ?></td>
<td><?= h($contact->title) ?></td>
<td><?= h($contact->via_code_dp) ?></td>
</tr>
</tbody>
</table>
enum っぽい利用方法
また、ちょっとこんな感じで実装するのは良くないかもしれませんが、あり・なしなどのフラグやマスタデータ化するほどではないなと思うものには enum っぽい使い方をしています。
# エンティティファイル内で記述
class Gim extends Entity
{
protected function _getLocker()
{
return $this->_properties['has_locker']? 'あり' : 'なし';
}
protected function _getShower()
{
return $this->_properties['has_shower']? 'あり' : 'なし';
}
protected function _getScale()
{
if($this->_properties['size'] == 1){
return '小規模';
}elseif($this->_properties['size'] == 2){
return '中規模';
}else{
return '大規模';
}
}
}
テンプレート側ではこのように利用します。
<dl>
<dt>ロッカーの有無</dt>
<dd><?= $gim->locker ?></dd>
<dt>シャワーの有無</dt>
<dd><?= $gim->shower ?></dd>
<dt>施設の規模</dt>
<dd><?= $gim->scale ?></dd>
</dl>
日付オブジェクトの利用
仮想プロパティのコード内にさらっと書きましたが、created や modified など datetime 型にセットしたものは自動で Timeオブジェクトに変換されます。したがって、好きなフォーマットに表示させることができます。
// 好きなフォーマットで表示できる
<?= $contact->created->format('Y年m月d日') ?>
// こんなことも可能
$limit = Time::createFromDate(2018, 12, 31);
無料期間はあと<?= $contact->created->diff($limit)->days ?>日です。
ざっくりと言ってしまうと CakePHP3 からは DB からデータを取得してきた時、エンティティに変換され、かつそのエンティティの各プロパティの属性によっては自動的にオブジェクトに変換されるようになりました。単純な値でないことで便利になったことが多いので、こちらに早く慣れておいた方がいいと思います。
Controller 編
利用方法は上記と合わせて書いたので、特記することは少なくなりますが、CakePHP2 系を利用されていた方からよく「ビジネスロジックはどこに書きますか?」と聞かれます。上記 Table クラスと Entity クラスを適切に利用し、controller で条件分岐させることで、そもそも以前行っていたモデルファイルにビジネスロジックを書くといったことをしなくなりました。DB アクセス(スキーマ関連)に関するものが Table クラス、値(取得したレコード 1 行)に対する処理が Entity クラスと認識すればわかりやすいかもしれません。
異なるハッシュ化方法のパスワードのシームレスな移行は私が実に便利と感じたものなので再度紹介します。
シームレスなパスワードハッシュ化の移行
終わりに
最初は Tips 集をかく予定でしたが、後半では CakePHP3 系を使うと便利だよというただの紹介文になってしまったかもしれません。書き忘れたことや MeetUpイベントでよく聞かれることなど、また何かあれば随時情報を公開していきます。みなさま楽しい CakePHP ライフを送り下さいませ。
そして来年は CakePHP の国際イベントが日本で開催されます。ぜひ会場でお会いしましょう〜。
Enjoy & Happy New Year!!