MVCフレームワーク

モデル

モデルの概念

システムにはデータがつき物です。
Webアプリにおいてはデータはほとんどの場合、データベースを指します。
テキストファイルを利用したりもしますが。

そもそもMVCフレームワークはRuby on Railsが発祥と言われており、
そのRuby on Railsを見てみると、Modelはデータベーステーブルをそのままオブジェクトした概念として扱われます。

PHPの世界では、代表的なMVCフレームワークであるCakePHPがRuby on RailsのPHP版と呼ばれており、モデルの考え方もほぼRailsと同じような感じになっています。
PHPの開発元であるZend社が開発したZendFrameworkではモデルはもっと緩い感じで、モデルに関して規定された概念はありません。
フレームワークによってモデルの考え方はさまざまといえます。

フレームワークを自作する以上は、モデルの考え方も当然自由です。
自分に合った形に作ればよいのです。

メインロジックはモデルに

一つ確実に言える事は、メインの処理ロジックはモデルの役割です。
コントローラーにゴリゴリ書くような実装はすべきではないです。

コントローラーは極力シンプルに、
モデルのメソッドを呼び出すだけのような形にするように意識すればよいと思います。

あまり何も考えないで作っていくと、コントローラーとモデルの境目がボケた作りになってしまいがちなので注意が必要です。

モデルをテーブルオブジェクトと捉える

Railsのような、モデル=データベーステーブルという考え方はモデルの利用方法の有力候補の一つです。
この場合、テーブル名と同一か、またはある一定の規則に従ってテーブル名と関連のあるクラス名でクラスを作ります。
つまり、そのアプリ上で利用するテーブルの数だけモデルクラスも存在することになります。

Railsの標準の規定では、テーブル名は全て小文字で、単語の区切りは"_"を使用します。
そのテーブルに対するモデルクラスは各単語の区切りの"_"を無くして、代わりに単語の先頭を大文字にし、更に末尾に"s"を付けることになっています。
例えばテーブル名が"cart_detail"だとしたら、モデルクラスはCartDetailsです。
テーブル名とクラス名を規則性をもって関連付けているわけです。

ここまでガチガチに名前付け規則によって関連付けるかどうかは好き好きですが、
単純にテーブル名をフィールドとして持たせておくという方法もあります。

Cart.php
<?php

class Cart
{
    private $db;
    private $name = 'cart';    

    public function __construct($user, $pass)
    {
        $this->db = new PDO($user, $pass);
    }

    // カート基本情報取得
    public function getUserCart($userId)
    {
        $sql = sprintf('SELECT * FROM %s where user_id = :user_id' , $this->name);
        $stmt = $this->db->query($sql);
        $stmt->bindValue(':user_id', $userId);
        $rows = $stmt->fetchAll();
        return $rows[0];
    }

    // 新規カート作成
    public function create($userId)
    {
        $sql = sprintf('INSERT INTO %s ・・・・・・・・', $this->name);
        $res = $this->db->query($sql);
        return $res;
    }

}

?>

カートの基本情報を格納するcartというテーブルに対するモデルクラスです。
このテーブルのデータ操作に直接関わる処理はこのクラスにまとめるわけです。

カートには当然、商品を入れるわけなので、カートの基本情報を格納するテーブルとは別に、
カート内の商品リスト情報を格納するテーブルが必要になるはずです。

CartProduct.php
<?php

class CartProduct
{
    private $db;
    private $name = 'cart_product';    

    public function __construct($user, $pass)
    {
        $this->db = new PDO($user, $pass);
    }

    // 商品リスト取得
    public function getList($cartId)
    {
        $sql = sprintf('SELECT * FROM %s where cart_id = :cart_id', $this->name);
        $stmt = $this->db->query($sql);
        $stmt->bindValue(':cart_id', $cartId);
        $rows = $stmt->fetchAll();
        return $rows;
    }
    
    // 商品追加
    public function add($data)
    {
        $sql = sprintf('INSERT INTO %s ・・・・・・・・', $this->name);
        $res = $this->db->query($sql);
        return $res;
    }
    
    // 商品削除
    public function remove($productId)
    {
        $sql = sprintf('DELETE FROM %s WHERE ・・・・・・・・', $this->name);
        $res = $db->query($sql);
        return $res;
    }
}

?>

今度はcart_productテーブルの処理用のCartProductというモデルクラスを作りました。

コントローラーでこれらのモデルを利用してみます。

CartController.php
<?php

class CartController
{
    public function displayAction()
    {
        $req= new Request();
        $params = $req->getParam();
        $userId = $params['user_id'];

        // カート基本情報を取得
        $cart = new Cart();
        $cartInfo = $cart->getUserCart($userId);
        
        // 商品一覧を取得
        $cartProduct = new CartProduct();        
        $products = $cartProduct->getProductList($cartInfo['cart_id']);
                ・
                ・
                ・
    }

}

?>

このように、一連の処理の中で複数のテーブルに対して処理が発生する場合、
テーブルごとのモデルクラスを用意し、それぞれの処理を呼び出す形になります。

テーブルをオブジェクトとらえて、オブジェクト単位の処理に綺麗に分類できるため、
ロジックの整理がしやすいと思います。
また、こうした設計は本来のオブジェクト指向の考え方が最も反映された手法と言えます。

ただしデメリットもあります。
テーブルとはあまり関わりない部分のロジックははどこに書くかという事で迷うのです。
そうなった時にコントローラーにだらだらとロジックを書いてしまうという過ちを起こしやすくなります。

またアプリ全体の中で、ごく一部でしか使われないようなテーブルで、
処理内容もほとんど無いような場合にも一つのクラスとする必要があり、
無駄にクラスが増えすぎる可能性があるということも言えます。

コントローラーと対にする

一つのアプローチとして、
コントローラーに対して必ず1対にするという手法が考えられます。

たとえばコントローラーとしてCartControllerというのがあったら、CartModelというクラスを必ず作ります。そしてカート画面に関わる処理は全てここに記述するわけです。

CartModel.php
<?php

class CartModel
{
    // 商品リストを含めたカート情報を取得
    public function getCartInfo($userId)    
    {
        $cart = $this->getUserCart($userId);
        $cart['products'] = $this->getProducts($cart['cart_id']);
        return $cart;
    }

    // カート基本情報取得
    public function getUserCart($userId)
    {
        $sql = 'SELECT * FROM cart where user_id = :user_id' ;
        $stmt = $this->db->query($sql);
        $stmt->bindValue(':user_id', $userId);
        $rows = $stmt->fetchAll();
        return $rows[0];
    }

    // 商品リスト取得
    public function getProducts($cartId)
    {
        $sql = 'SELECT * FROM cart_product where cart_id = :cart_id';
        $stmt = $this->db->query($sql);
        $stmt->bindValue(':cart_id', $cartId);
        $rows = $stmt->fetchAll();
        return $rows;
    }

}

?>

このように複数テーブルの処理でも一つのモデルに含めてしまいます。
この場合のコントローラーの処理はこんな感じになります。

CartCotroller.php
<?php

class CartController
{
    private $view;
    private $model;

    public function __construct()
    {
        // ビュー
        $this->view = new Smarty();
        $this->view->template_dir = '../view/templates';
        // モデル
        $this->model = new CartModel();
    }

    public function displayAction()
    { 
        $req = new Request();
        $params = $req->getParam();
        $userId = $params['user_id'];

        // カート情報を取得
        $this->model->getCartInfo($userId);
        $this->view->assign('cart_info', $cartInfo);

        $this->view->display($this->templatePath);
    }

}

?>

とてもすっきりします。
見たとおり、コントローラーに対して必ず対応するモデルを作る為、あらかじめコンストラクタ等でモデルのインスタンスを生成してフィールドに持たせるという使い方もしやすくなります。

このような手法は、オブジェクト単位というよりは、ページ単位にモデルを作るという考え方です。
なので、複数オブジェクト(テーブル)に対する処理が発生する場合でも、一つのモデル内で処理を完結することができ、大きめのシステムで機能数が多くなっても管理がしやすいというメリットがあります。

デメリットとしては、同じオブジェクトに対する同じような処理が別々のモデルで発生する場合、同じロジックをそれぞれに書かなければならなくなってしまうことです。

一貫した思想で

上の例ではテーブル単位にモデル、またはコントローラー単位にモデルを作成しましたが、
もっと緩く、単純にカートという概念に対するモデルとしてCartというモデルクラスを作成し、カートに関わるテーブル処理や付随するロジックは全てここに集約という考え方もできます。
これが最もオブジェクト指向らしいと言えるかもしれません。

なんにせよ、モデルの設計はMVCの中でも自由度が高く、かつ最もポイントとなる部分だと言えるでしょう。
どういう手法をとるにしても、一貫した思想を持ってモデルを設計するという事が重要です。