CakePHP3のアンチパターン
CakePHPで開発されたいくつかのプロジェクトのソースコードを実際に確認したところ、共通する問題点がいくつか見つかったので、それらの事例についてアンチパターンとして紹介していきたいと思います。
不要なローディング
namespace App\Controller;
use App\Controller\AppController;
use Cake\Routing\Router;
require_once(ROOT . DS . 'vendor' . DS . "tcpdf" . DS . "tcpdf.php");
require_once(ROOT . DS . 'vendor' . DS . "tcpdf" . DS . "fpdi.php");
use fpdi;
use tcpdf_fonts;
class AmentsController extends AppController {
素直にautoloadを使いましょう。 Cakeアプリ全体でautoload機構によるファイル配置が前提になっているのに、部分的に明示的なロードを混ぜるのは混乱しか招きません。
Controllerに何でも書きすぎ
各ControllerのActionに入力チェックやORMの入出力まで全部書かれている実装が頻繁に見受けられる、というかむしろそういう実装しか見かけないのですが、入力確認についてはValidation系クラス、RDBのレコード処理についてはTableクラス側に移譲しましょう。
1つのActionが1000行以上になっているようなケースなどは、Controllerに書くべきでないコードが含まれていると断言できるでしょう。
アソシエーションを使っていない
CakePHPのActiveRecord Tableはなかなか賢くて、ある程度の複数テーブルのリレーションをサポートしています。
アソシエーション - モデル同士を繋ぐ(CakePHP 3.7 Red Velvet Cookbook)
https://book.cakephp.org/3.0/ja/orm/associations.html
CakePHP3ではアソシエーションと呼ばれ、Tableクラスからfind()してきたEntity系クラスの指定したプロパティに別のTableクラスのEntityがぶら下がった状態で取得できるようになります。この親子関係(または兄弟・親親など)の定義については以下の4種類が定義されています。
- hasOne 例:ユーザーは1つのプロフィールを持っている。
- hasMany 例:ユーザーは複数の記事を持つことができる。
- belongsTo 例:多くの記事がユーザーに属している。
- belongsToMany 例:タグは多くの記事に属している。
例を挙げましょう。
(Controller/BookingController.php内)
$performance_list = $this->PerformanceInfoTable->getPerformanceInfoList($search_param,$now);
for($i=0; $i<count($performance_list); $i++){
$performance_opt_info = $this->PerformanceOptInfoTable
->getPerformanceInfo($performance_list[$i]['manage_no']);
...
}
子のテーブルを含む取得処理でありがちな実装ですが、上記の例の場合、子のテーブルをSELECTする回数が少なくとも親のレコード数だけ発生し、パフォーマンスが著しく低下します。これをN+1問題といいますが、アソシエーションを使えば簡単に解決できます。
まず、PerformanceInfoTableのコンストラクタでPerformanceOptInfoTableをhasManyで宣言します。
(Model/Table/PerformanceInfoTable.php内)
$this->hasMany('PerformanceOptInfo', [
'bindingKey' => 'performance_id',
'foreignKey' => 'performance_id',
'conditions' => ['PerformanceOptInfo.delete_flg' => 0],
'propertyName' => 'Options'
]);
あとはfind()するときにcontain()メソッドで指定することで、2つ以上のテーブル読み込みに対応したクエリが予め実行され、この場合はOptionsプロパティとしてアクセスできるようになります。
(Model/Table/PerformanceInfoTable.php内)
$performances = $this->find();
$performances = $performances->contain([
'PerformanceOptInfo' => [
'sort' => [
'performance_id' => 'ASC',
]
]
]);
(Controller/BookingController.php内)
$performance_list = $this->PerformanceInfoTable->getPerformanceInfoList($search_param,$now);
for($i=0; $i<count($performance_list); $i++){
$performance_opt_info = $performance_list[$i].Options;
...
}
実装がダブっている
これはAction側、Template側両方で言えることですが、同じような機能を実現するのにモジュール化せず、別々に実装され、コードがコピペの要領で増やされていることがあります。共通化できるような内容であれば共通化してしまいましょう。
(複数のController内)
public function printTicket() {
$this->TicketPrint = $this->loadComponent('TicketPrint');
$booking_id = $this->request->data['booking_id'];
$print_ticket_info = $this->TicketPrint->printTickets([$booking_id]);
$this->session->write('detail_print', $print_ticket_info);
$res = [
'data' => $print_ticket_info,
'status' => true
];
$this->response->body(json_encode($res));
$this->viewBuilder()->autoLayout(false);
$this->autoRender = false;
}
この例ですと、printTicket()というActionがシステム中6ヶ所存在していたのですが、ほとんど同じ内容だったので、Controller/Componentディレクトリ内、例えば
Controller/Component/TicketPrintComponent.php という形で一つにまとめ、実質的な処理を移譲しています。
もちろんRDBに関する処理ならばTable側に移譲すべきというところは変わりません。
同様に、Templateに関してはElement化すれば共通化できるでしょう。
(Booking/printTicket.ctp)
<?= $this->element('ticket_print');?>
Template側は空っぽの
各印刷ページのテンプレートに上記のコードを記述し、 Template/Elementディレクトリ内、例えば Template/Element/ticket_print.ctp に共通化した処理を記述すればOKです。1
追記の予定
他の事例が見つかれば追記していきたいと思います。
以上。
補遺:地味に困っていること
CakePHP3系は、Modelに相当するのがModel/TableとModel/Entityしかなく、RDBの特定のテーブルに依存しないModelコード(ビジネスロジック)をどこに書けばいいのかわかりません。Model/Behaviorは非常に限定したシチュエーションが想定されているようです。CakePHP2系ではできるようです。
理論上はModel/TableにTableでないクラスを追加すれば参照可能なのですが、特定のTableの実装でないのに ~~Table.php という名前にするのも変な感じがするし、RDBのテーブル一覧と1対1対応しなくなるので見づらくなります。
Modelディレクトリ直下にソースファイルを設置してもloadModel()等で読み込むことができません。ローダー含め自作すれば一応可能である見通しは立っているのですが、ここまでやらないとオリジナルのModelは運用できないものなんでしょうかね?2