LoginSignup
5

More than 3 years have passed since last update.

CakePHP3のアンチパターン

Last updated at Posted at 2019-06-18

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側は空っぽの

タグにJavaScriptでDOMを流し込む処理方式になっていたので、共通化はJavaScriptのみで済む形となりました。
各印刷ページのテンプレートに上記のコードを記述し、 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


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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5