CakePHP3のアンチパターン

# CakePHP3のアンチパターン

CakePHPで開発されたいくつかのプロジェクトのソースコードを実際に確認したところ、共通する問題点がいくつか見つかったので、それらの事例についてアンチパターンとして紹介していきたいと思います。


## 不要なローディング

```php
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 例:ユーザーは１つのプロフィールを持っている。
- hasMany 例:ユーザーは複数の記事を持つことができる。
- belongsTo 例:多くの記事がユーザーに属している。
- belongsToMany 例:タグは多くの記事に属している。

例を挙げましょう。

(Controller/BookingController.php内)

```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内)

```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内)

```PHP
$performances = $this->find();
$performances = $performances->contain([
    'PerformanceOptInfo' => [
        'sort' => [
            'performance_id' => 'ASC',
        ]
    ]
]);
```

(Controller/BookingController.php内)

```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内)

```PHP
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)

```PHP
<?= $this->element('ticket_print');?>
```

Template側は空っぽの <div> タグにJavaScriptでDOMを流し込む処理方式になっていたので、共通化はJavaScriptのみで済む形となりました。
各印刷ページのテンプレートに上記のコードを記述し、 Template/Elementディレクトリ内、例えば **Template/Element/ticket_print.ctp** に共通化した処理を記述すればOKです。[^1]

[^1]: 純粋なJavaScriptなら固定のjsファイルに保存してしまうという考え方はもちろんアリでしょう。

## 追記の予定

他の事例が見つかれば追記していきたいと思います。
以上。

## 補遺：地味に困っていること

CakePHP3系は、Modelに相当するのがModel/TableとModel/Entityしかなく、RDBの特定のテーブルに依存しないModelコード(ビジネスロジック)をどこに書けばいいのかわかりません。Model/Behaviorは非常に限定したシチュエーションが想定されているようです。CakePHP2系ではできるようです。

理論上はModel/TableにTableでないクラスを追加すれば参照可能なのですが、特定のTableの実装でないのに **～～Table.php** という名前にするのも変な感じがするし、RDBのテーブル一覧と1対1対応しなくなるので見づらくなります。

Modelディレクトリ直下にソースファイルを設置してもloadModel()等で読み込むことができません。ローダー含め自作すれば一応可能である見通しは立っているのですが、ここまでやらないとオリジナルのModelは運用できないものなんでしょうかね？[^2]

[^2]: 『Serviceを追加する(非標準)』という解決策をコメント欄で @nojimage さんに教えていただきました。ありがとうございます。