この記事は、CakePHP3 Advent Calendar 2016 25日目の記事ではありません。
まとめ
- Controller/Componentにビジネスロジックを書かない
- カスタムfindを活用しよう
- 1テーブル1モデルではなく、場面によってモデルクラスを増やそう
- Modelless Form / Tableless Modelを活用しよう
- beforeSave/afterSaveは諸刃の刃
- Behaviorにビジネスロジック書くのもあり
- View層ではView Cell/View Blockを活用しよう
- CRUDプラグインお勧め
この記事を書いた動機
CakePHPにおいてビジネスロジックに該当するものをComponentに書くという記事を見かけるたびに、それはアンチパターンじゃないのと感じている日々。
ビジネスロジックをどこに書くべきか、共通処理をどこに書くべきかというのを、自分の考えを整理する意味でも記事として残しておきたいと思います。
なぜ、コンポーネントにビジネスロジックを書いてはいけないのか
コントローラやリクエストに依存するためテストを書きづらくなるのと、そのビジネスロジックをシェルなどから実行したい場合にそのまま呼び出せないという問題があります。
ビジネスロジックはコントローラに依存しないように書くべきです。
Controller/Componentに書くべき処理
HTTP リクエスト/レスポンス、セッション状態を対象とする処理
特定のページからの遷移しか許可しない、ログインユーザによってアクセスを制限する、といったような処理は、Controller/Componentに書くべき処理です。
なお、HTTPリクエストに関する処理は、DispacherFilter
やMiddleware
という選択肢もあります。
ログインユーザによってアクセスを制限するような処理については、CookBookにあるAuthComponentの
認可 の部分も一読すると良いでしょう。
リクエスト、セッション状態をModel層に渡す処理、Model層からの結果をViewへ渡す処理
Contollerの本来の領分ですね。
add/editのアクションで同じようなデータを取得しておきたいなど、複数のactionから使用されるような処理は、Controller内にprivateなメソッドを作成するか、Controllerをまたぐようであれば、Component化するのがよいと思います。
Contoller/Componentでconditionsなどのパラメータを組立ててfindメソッドを呼び出すのはお勧めしません。カスタムファインドや取得用のメソッドをModel層に作成して取得するようにすべきです。
保存処理などにおいて入力パラメータの変換などをContoller/Componentに書いてしまいがちですが、モデルのメソッドへ$this->request->data()
などを直接渡して、パラメータの変換などもモデル層で行うのがよいでしょう。
なお、CakePHP 3.x以降では、ビューセルという概念が追加されています。
画面へ共通パーツを表示したいという目的であれば、ビューセルを使うのが望ましいでしょう。
コントローラのメソッドに10行以上の記述がある場合は、危ない兆候だと思いましょう。
CRUDプラグインによる矯正
CRUDプラグインを利用することでコントローラ層を簡潔に記述することができます。
CRUDプラグインはマスタデータの管理やAPIを作成する場合に有用なのですが、独自ロジックを組み込む場合はイベントを利用するため、処理ごとにロジックを分離しやすくなります。
- Events — Crud v4
- CakePHP3でREST APIをちゃちゃっと作る方法 – Qiita
- #CakePHP 爆速でAPIを実装するチュートリアル – 忍び歩く男 – SLYWALKER
ControllerでのTraitの使用は危険
トレイトを使用した場合、意図せずにメソッドが公開されてしまう可能性があります。
Model (Table/Entity/Behavior) に書くべき処理
cakephp – MVCモデルにおけるサービスの役割について教えて下さい – スタック・オーバーフロー にある、Hidenori GOTOさんの回答が素晴らしいので以下に引用します。
モデルとは?
一般的に「モデル」というのは、仕組みや構造を分かりやすく表したものを指します。業務のためのシステムであれば、その業務で扱っている「情報の構造」だったり、「情報の作り方のルール」「手順」だったりします。
CakePHPのModelは?
CakePHPのModelの場合、データベースのスキーマを自動的に読み取って、そのテーブルのレコードを簡単に扱えるようになっているため、一見Modelの役割が「DB入出力のためのコード置き場」のように見えてしまいます。しかし、このようなActiveRecordの本来の目的は、アプリケーションコードから、情報の形 を統一した形式で扱えるようにする点にあります。
他のMVC系のフレームワークでも、Modelと名付けられている部分には、このように「情報の形」を表現するという目的が与えられています。この情報の形に付随する処理(情報の入出力や、形の整合性のチェック、形の多少の変形・合成)は、Modelに記述するということになるでしょう。
CakePHP 3.xにおいては、Repository/Entityパターンを採用していますので、情報の取得に関すること、情報の整合性のチェックに関することについてはTable
に、個々の情報の整形についてはEntity
の役割になります。
データソースからの取得
データソースから情報を指定の条件で取得したい場合は、モデルクラスのfindメソッドを直接利用するのではなく、取得、整形するためのメソッドを用意した方がよいです。
また、2.xにおいては取得条件が複雑になる場合、条件を返すメソッドをモデルクラスへ作成するのも良い手法です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function getPublishedConditions($now) { $conditions = [ 'Post.publish' => true, 'Post.release_date >=' => $now, 'Post.expired_date <=' => $now, ]; // ... return $conditions; } // 呼び出し側 $Post->find('all', ['conditions' => $Post->getPublishedConditions($now)]); |
取得したデータの整形
モデルに取得用の専用メソッドを作成するか、カスタムfindメソッドを利用して書きます。
個々のフィールドの整形については、Entityに記述します。
eg. nameフィールドを全角変換する
1 2 3 4 5 |
protected funciton _getName($value) { return mb_convert_kana($value, 'KVAS'); } |
ただし、アクセサメソッド(_get)はDBへの保存時にも適用されてしまいます。
取得したデータを呼び出すときのみフィルタを掛けたければ、仮想プロパティを作成して、そちらを利用するようにするとよいでしょう。
また、text型のフィールドにシリアライズして格納したデータを呼び戻したい場合などは、カスタムデータタイプを定義します。
CakePHP 2.xではエンティティの概念がないため、一般的にはafterFindにて処理することになります。
ただし、CakePHP Entity Plugin を入れることでエンティティへの変換が可能になります。
場面によって表示形式を変えたい場合は、Entityクラスを変更する方法がよいと考えます。
データの整形についてHelperなどを使用してView層でやると、規模が大きくなってきた場合に破綻しやすいのであまりお勧めしません。
データの保存
同じテーブル/データソースに対してでも、状況によって保存するフィールドやバリデーションを変えたいということは往々にしてあります。CakePHPのよくある誤解として、1つのテーブルに対してモデルクラスは1つというのがありますが、そんなことはありません。
上記のモデルの定義の通り、1つのテーブルに対してでも場面ごとにモデルクラスを作成して、バリデーションや保存処理を書いていくとよいでしょう。
理想としては、コントローラではモデルの保存処理メソッドを呼び出すだけの記述になるのがよいです。
1 2 3 4 5 6 7 8 |
public function placeOrder() { $table = TableRegistry::get('Orders'); if ($order = $table->placeOrder($this->request->data)) { // ... } } |
また、CakePHP 3.xでは、バリデーションの変更が容易になっています。こちらを利用してバリデーションを切り替える方法もありです。
- CakePHP 3.x Cookbook データの保存
- CakePHP 3.x Cookbook データの検証
- CakePHP 2.x Cookbook データを保存する
- CakePHP 2.x Cookbook データバリデーション
なお、バリデーションについては、CakePHP 3.xからエンティティ構築前のデータ検証と、保存時に適用されるアプリケーションルール に分かれていることを覚えておいてください。
beforeSave/afterSaveを避ける
保存処理の前後にメール送信や決済処理など連動する処理がある場合、beforeSave/afterSaveなどに書いてしまいがちですが、処理が複雑になってくるとイベントの依存関係の把握に苦労するようになります。
そのため、保存の前後に併せて処理を行う場合は一連の処理専用メソッドを作成するようにします。
eg. カートでの注文処理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public function placeOrder($entity) { // ... 前処理 if (!$this->paymentAuthorization($entity)) { // ... 与信失敗時の処理 } if (!$this->save($entity)) { // ... 保存失敗時の処理 } // 後処理 if (!$this->paymentConfirm($entity)) { // ... 決済失敗時の処理 } $this->sendPlaceOrderMail($entity); return $entity; } |
モデルのないフォーム/テーブルのないモデルを活用する
入力フィールド名とテーブルのフィールド名が異なる場合
APIなどで入力フィールド名とテーブルのフィールド名が異なる場合は、 Modelless From(3.x) / Tableless Model(2.x) を利用して入力に対してのバリデーション定義と実テーブルへのフィールド変換を行うとよいでしょう。
CakePHP 2.xでは、$useTable = false
とすることで、テーブルに紐付かないモデルを作成できます。
$_schema
プロパティを定義することで、FormHelprとの連携も可能です。
eg. 2.xでのTableless Modelの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
class ChangePasswordForm extends Model { public $useTable = false; public function __construct($id = false, $table = null, $ds = null) { parent::__construct($id, $table, $ds); $this->bindModel(['hasOne' => [ // bindModelを利用して他のモデルを呼び出すことができます 'User' => [ 'className' => 'User', 'foreignKey' => false, ], ]], false); $this->setupSchema(); $this->validator($this->validationDefault()); } protected function setupSchema() { $this->_schema = [ 'current_password' => ['type' => 'string', 'null' => false, 'length' => 255], 'new_password' => ['type' => 'string', 'null' => false, 'length' => 255], 'confirm_password' => ['type' => 'string', 'null' => false, 'length' => 255], ]; } /** * @return ModelValidator */ public function validationDefault() { $validator = $this->validator(); // ... バリデーション定義 return $validator; } /** * @param array $userId AuthCompoent::user('id')で取得 * @param array $data * @return boolean */ public function execute($userId, array $data) { $this->clear(); $this->set($data); if (!$this->validates()) { reutrn false; } $password = $this->data[$this->alias]['new_password']; if (!$this->User->save(['id' => $userId, 'password' => $password])) { // ... 保存失敗時処理 } return true; } } |
複数のテーブルにまたがる処理
一度のリクエストで複数のテーブルにまたがって処理を行いたい場合があります。
アソシエーションで定義されている範囲であれば標準のメソッドを利用してもよいのですが、紐付かない場合は Modelless From / Tableless Model を利用して処理を記述するのがよいでしょう。
ビヘイビアを活用する
ビジネスロジックを記述するのにビヘイビアを利用するのも良い手法です。
※このアイディアはごく最近にCakePHP Slackチャンネルでslywakerさんにお聞きしたのがきっかけです。
1テーブルに複数のロジックが存在する場合、モデルクラスが肥大化しがちです。これを解決する手法としては場面毎にモデルを分ける方法や、Modelless Formを利用するなどの方法がありますが、処理ごとにビヘイビアを作成し切り替えて利用するのも一つのアイディアです。
Traitで処理を切り分ける方法もありますが、処理の有効化/無効化が容易なことと、モデル/ビヘイビアの依存関係を記述しやすいことから、ビジネスロジックを書くのであればビヘイビアをお勧めします。
※デメリットとしてIDEの補完ができない点があります。@methodアノテーションをモデルクラスに付与して我慢しましょう。
特にコア部分で名前空間を使用できない2.x系で処理を分けるのに有用です。
ex. CakePHP 2.xでの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class OrderPlaceOrderBehavior extends ModelBehavior { public function setup(Model $model, $config = array()) { parent::setup($model, $config); // 依存関係を記述してみる if (!is_a($model, 'Order')) { throw new InvalidArgumentException(sprintf('%s は、Orderモデル専用です。', __CLASS__)); } if (!$model->Behaviors->enabled('OrderMail')) { throw new InvalidArgumentException(sprintf('%s に OrderMailBehavior がセットされていません。', get_class($model))); } } public function placeOrder(Order $model, array $data) { // ... } |
View / Template
View Cell
3.xからの新機能で、CMSでサイドバーへの記事ランキングの表示や、カートシステムでの買い物カゴの表示などに利用できるのがビューセルです。
2.xではrequestAction()
メソッドを利用して同じようなことができますが、あまりお勧めしません。
Element
エレメントは繰り返し表示する部品や、共通パーツを表現するのによく使います。
1 2 3 4 |
echo $this->element('helpbox', [ "helptext" => "おお、このテキストはとても役に立つ。" ]); |
View Block
ヘッダーやサイドバーに特定のページのみ何かを表示したい場合などに利用できるのが、ビューブロックという機能です。
利用方法としてはまず挿入先のブロックをレイアウトファイルなどに定義します。
1 2 3 4 5 6 |
// Layout/default.ctp <aside id="sidebar"> <!-- ... 固定のコンテンツ ... --> <?= $this->fetch('sidebar') ?> </aside> |
そして、特定ページのテンプレートから定義したビューブロックへコンテンツを追加します。
1 2 3 4 5 6 7 8 |
// Foo/index.ctp <?php $this->append('sidebar') ?> <div class="sidebar-block"> <!-- サイドバーコンテンツ --> </di> <?php $this->end();?> |
詳しくはマニュアルを確認してください。
View層からModelを直接呼び出す
SELECTタグのoption表示用のデータを取得したい場合など、いちいちコントローラーでセットして取得するというのは面倒ですので、主となるもの以外のデータの取得に限って言えばViewからModelを呼び出してもよいでしょう。
ただし、データの更新など副作用を伴う処理を書いてはいけません。
Helper? 知らない子ですね
標準のHtmlHelperやFormHelperを拡張する以外では、独自に作成するメリットをあまり見いだせていません。