サービス
MVCの問題点
MVCというアーキテクチャは非常によくできていますが、問題点もあります。
簡単に言うとモデルとコントローラーの守備範囲の境界が曖昧であることです。モデルを置かずにロジックを全てコントローラーに書くような実装は論外ですが、コントローラーに記述すべきかモデルに記述すべきかを迷う場面があります。
メインロジックの扱い
一般的な認識は、モデルはデータ操作を含めたロジックを担当するというものでしょう。しかしCurryでも推奨しているようにモデルをテーブルオブジェクトとしての位置づけにした場合、モデル=データという構図が出来てしまい、メインロジックの行き場がなくなります。そのロジックというのがテーブルより取得したデータを加工する意味合いのロジックであればモデルに含めてしまえばよいですが、データに一切かかわらない処理ロジックがある場合に困ります。そうするとロジックの行き場としてコントローラーという、MVCの本来の有るべき姿から逸脱してしまう事になってしまいがちです。
トランザクション処理
複数のテーブルの更新を一連の処理として整合を保つ「トランザクション」という考え方が存在しますが、普通に考えればテーブル一つ一つに対する処理を統括するのはモデルより一層上となるはずです。しかしモデルクラスとテーブルを1:1とした場合、それを統括するロジックはどこに書くかという問題が出てきます。そうすると最もメインとなりそうなモデルを選んでその中に記述するのか、あるいはコントローラーで統括するのか、という妥協を含んだ判断になってきます。これはどちらも得策とは言えません。
コントローラーとモデルの中間に位置する層
モデルクラスとテーブルを1:1とし、モデル=データと位置づけてしまうと、そもそもモデルとコントローラーという2層しかないことに無理がある事は否めません。
そこで、モデルとコントローラーの中間に位置する層を置くことで対応するという事が考えられます。
Curryではこれを「サービス」と呼び、その仕組みを組み込んでいます。
前提としてモデルはテーブルオブジェクトとし、モデル=データという位置づけに固定化します。
その上でのサービスはデータのトランザクション管理とロジックという2つの役割を持たせます。
Curryではサービスもクラスで表現します。
サービスの定義方法
まずサービスクラスはapp/servicesの中に定義します。
そしてサービスクラスは"Service"クラスを継承することを推奨します。サービスはあくまで概念的なものなので、必ずしも継承する必要はありませんが、継承することでサービスとしての役割を果たす為の便利な機能の恩恵が受けられます。
class CartService extends Service { public function buy() { } }
クラス名の命名規則も特にありませんが、モデルとの区別のため、末尾に"Service"などを付加することをお勧めします。
サービスではモデルを操作するのがひとつの役割のため、ServiceクラスにはControllerと同じ働きをするmodelメソッドが用意されています。
class CartService extends Service { public function buy() { $cart = $this->model('Cart'); } }
複数モデルを対象にした処理
複数モデルが絡み合ったデータ取得はサービスに記述するのがよいでしょう。
class CartService extends Service { public function getCartInfo($userId) { $cart = $this->model('Cart', 'C'); $cartPrd = $this->model('CartProduct', 'CP'); $sel = $cart->select(); $sel->joinInner($cartPrd, array( 'CP.cart_id = C.cart_id' )); $sel->where('C.user_id', userId); $sel->order('CP.add_date DESC'); $rows = $sel->fetchAll(); return $rows; } }
また、複数のモデルを統括してトランザクションを統括することもサービスの役割の一つです。
次の例はショッピングサイトにおいてユーザーがカートに入れた商品を購入する処理を想定しています。ここでは一連の処理として購入履歴への記録と、カートのクリアというトランザクション処理を行っています。
class CartService extends Service { public function buy($cartId) { $cart = $this->model('Cart'); $cartPrd = $this->model('CartProduct'); $buyHist = $this->model('BuyHistory'); $this->begin(); try { $userId = $cart->getUserId($cartId); $products = $cartPrd->getProducts($cartId); $buyHist->write($userId, $products); $cartPrd->remove($cartId); $cart->remove($cartId); $this->commit(); return true; } catch (Exception $e) { $this->rollback(); throw $e; } } }
Serviceクラスにはトランザクション関連メソッドが存在します。
beginメソッドでトランザクションを開始し、commitでコミット。エラー発生時にはrollbackによってロールバックします。
コントローラーよりの実行
サービスクラスはコントローラークラスより利用します。コントローラーでのサービスクラスを呼び出すにはいくつかの方法がありますが、基本的にはモデルクラスと同様です。
serviceメソッドを利用
Controllerクラスにはserviceメソッドが存在し、指定したクラス名のサービスクラスのインスタンスを返します。これが最も効率的な利用方法と言えます。
class CartController extends Controller { public function index() { $svc = $this->service('CartService'); $cartInfo = $svc->getCartInfo($this->params['user_id']); $this->view->cart_info = $cartInfo; } }
インクルードした上でnew
require_onceなどにより事前にクラスファイルを読み込んだ上でnewによるインスタンス生成という、PHPとして最も標準的な手順を踏むことは当然可能です。
require_once APP_PATH . 'services/cart_service.php'; class CartController extends Controller { public function index() { $svc = new CartService(); } }