Curry
PHP Framework

モデル

モデルの概念

モデルはMVCの中でシステムのメインロジックを担当するべき部分です。ただしCurryではモデルの利用は必須ではありません。コントローラーから利用する処理ライブラリ的な感覚での利用と考えても構いません。

ただ一般的にモデルはデータ処理、メインロジックを担当する層だと位置付けられます。
基本的にシステムの根幹を成すロジックはコントローラーに記述せず、モデルに記述することを勧めします。

ひとつの考え方として、モデルとデータベーステーブルを密接に関連付け、モデルクラスとテーブルが1:1となるような設計も一般的です。
CurryではModelというクラスを継承することにより、それにならう事ができます。
この章ではModelクラスの継承を前提として説明しています。

モデルの利用

モデルクラスはapp/modelsディレクトリ内に配置します。

∟site/
    ∟app/
        ∟models/
            ∟cart.php

モデルはコントローラーから呼び出して利用しますが、呼び出し方にはいくつかの方法があります。

コントローラーのmodelメソッドを利用する

コントローラーのmodelメソッドの引数にモデルクラス名を指定することで、モデルクラスのインスタンスを得ることができます。モデルクラスのファイルが所定の位置に正しい名前規則で配置されていれば、このメソッドを利用するのが最もシンプルかつ効率的です。
この場合、コンストラクタの引数なしでインスタンス生成するため、引数を要求する場合は利用できません。

app/controllers/cart_controller.php
class CartController extends Controller
{
    public function list()
    {
        $model = $this->model('Cart');
    }
}

インクルードした上でnew

require_onceなどにより事前にクラスファイルを読み込んだ上でnewによるインスタンス生成という、PHPとして最も標準的な手順を踏むことは当然可能です。この場合、モデルクラスのコンストラクタの引数を自由に使えると言うメリットがあります。

app/controllers/cart_controller.php
require_once PathManager::getModelDirectory() . '/cart.php';

class CartController extends Controller
{
    public function list()
    {
        $model = new Cart();
    }
}

オートロードを利用してnew

PHPの特殊関数である__autoload関数を利用し、LoaderクラスのloadModelメソッドを利用する方法です。LoaderクラスのloadModelメソッドは、クラス名をもとにモデルクラスのphpファイルを読み込んでくれます。そしてコントローラーではnewするだけです。いちいちrequire_onceする必要がない上にモデルのコンストラクタの引数を自由に使えるというメリットがあります。

htdocs/index.php
function __autoload($className)
{
    Loader::loadModel($className);
}
app/controllers/cart_controller.php
class CartController extends Controller
{
    public function list()
    {
        $model = new Cart();
    }
}

モデルクラスの定義

モデルクラスをデータベーステーブルと関連付ける場合、一定のルールに従います。
Curryではテーブル名規則はスネークケース、クラス名はパスカルケースと規定されているため、それに従ってテーブルを作成し、クラスも作成します。
そしてモデルクラスは必ずフレームワークの"Model"クラスを継承します。

CREATE TABLE cart
(
    cart_id int primary key auto_increment,
    user_id int not null,
    create_time varchar(14) not null,
    update_time varchar(14) not null
)
app/models/cart.php
class Cart extends Model
{

}

データアクセス

SELECT

Modelを継承したクラスではテーブルへのデータアクセスが簡単に行えます。

class Cart extends Model
{
    public function getCartList()
    {
        $sel = $this->select();
        $sel->order('create_time DESC');
        $rows = $sel->fetchAll();
        return $rows;
    }

    public function getUserCart($userId)
    {
        $sel = $this->select();
        $sel->where('user_id', $userId);
        $row = $sel->fetchRow();
        return $row;
    }
}

SELECT文によるデータ取得の例です。モデルクラスのselectメソッドは、SELECTオブジェクトを返します。このSELECTオブジェクトに対してWHERE条件を設定したり、ORDER BYを設定したりして、最後にfetchAllなどの実行メソッドにより、データを取得します。
fetchAllは複数レコードを取得し、fetchRowは単一レコードを取得します。

そしてモデルクラスのメソッドをコントローラーから利用するような形にします。

class CartController extends Controller
{
    public function index()
    {
        $cart = $this->model('Cart');
        $cartInfo = $cart->getUserCart($this->params['user_id']);
        $this->view->cart_info = $cartInfo;
    }
}

データ取得にはfindというメソッドもあります。selectメソッドが条件を指定していろいろなデータの取得の方法があるのに対し、findはテーブルのキー項目の値を直接引数で指定するだけで単一行を得られるメソッドです。

class CartController extends Controller
{
    public function index()
    {
        $cart= $this->model('Cart');
        $cartInfo = $cart->find($this->params['cart_id']);
        $this->view->cart_info = $cartInfo;
    }
}

先ほどの例では、キー項目ではないuser_idを指定で取得するために、selectメソッドを利用してWHERE条件を指定して、という一連の処理があります。この一連の処理をコントローラーに記述すると多少ごちゃつくので、モデル内に一連の処理をまとめてメソッドとし、コントローラーから呼び出すような形にしています。

それに対してfindメソッドでは引数としてそのテーブルのキー項目を指定するという決まりがあり、findメソッドの戻り値が結果の単一レコードになるため、直接コントローラーから利用するのに向いています。例ではテーブルのキー項目がcart_idという1フィールドでキーとなっている場合はfindの引数も一つになりますが、複数のフィールドでキーをなす複合キーの場合は、その数の分、引数の数も増やしていくということになります。

以下のような2つの列でキーを成す複合キーテーブルがあった場合の例を示します。

CREATE TABLE cart_product
(
    cart_id int not null primary key,
    product_id int not null primary key,
    create_time varchar(14) not null,
    update_time varchar(14) not null
)
class CartController extends Controller
{
    public function index()
    {
        $cartPrd = $this->model('CartProduct');
        $productInfo= $cartPrd->find($this->params['cart_id'], $this->params['product_id']);
        $this->view->product_info = $productInfo;
    }
}

INSERT・UPDATE・DELETE

更新系のクエリも同様に行えます。

class Cart extends Model
{
    public function updateDate($cartId)
    {
        // UPDATE
        $upd = $this->update();
        $upd->values('update_time', date('YmdHis'));
        $upd->where('cart_id', $cartId);
        $res = $upd->execute();
        return $res;
    }

    public function createCart($userId)
    {
        // INSERT
        $ins = $this->insert();
        $ins->values('user_id', $userId);
        $ins->values('create_time', date('YmdHis'));
        $ins->values('update_time', date('YmdHis'));
        $res = $ins->execute();
        return $res;
    }

    public function deleteCart($cartId)
    {
        // DELETE
        $del = $this->delete();
        $del->where('cart_id', $cartId);
        $res = $del->execute();
        return $res;
    }
}

データアクセスについてはもっといろいろな機能が用意されています。詳しくはデータアクセスのところで詳しく解説していますのでそちらを参照してください。

空想モデルクラス

基本的にはコントローラーのmodelメソッドは、引数で指定したモデルクラスのインスタンスを返すものです。当然、モデルディレクトリにそのモデルクラスが存在する前提のメソッドです。しかし実はモデルクラスが存在しなくても、modelメソッドで指定する引数に対して正しい名前規則のテーブルが存在する場合、そのモデルクラスが存在するように見立ててインスタンスを返してくれます。基本的にはちゃんとモデルクラスを定義するべきですが、ごく単純なアクセスしか行わず、独自にメソッドを持たせる必要も無いような場合、敢えてモデルクラスを作らずとも良いようになっています。

∟site/
    ∟app/
        ∟controllers/
            ∟cart_controller.php
        ∟models/
              (※cart.phpは存在しない)
class CartController extends Controller
{
    public function index()
    {
        // Cartクラスは実際には存在しないが、cartテーブルオブジェクトとしてのインスタンスが返る
        $cart = $this->model('Cart');
        $cartInfo = $cart->find($this->params['cart_id']);
        $this->view->cart_info = $cartInfo;
    }
}

この仕組みを利用するとモデルクラスなんて一切必要ないという考えに至るかもしれません。しかしそうすると本来モデルクラスに記述すべきロジックが全てコントローラーに書かれることになるため、好ましくありません。本当に必要ないと思われる場合以外はモデルクラスをちゃんと定義することをお勧めします。

データベースコネクションの考え方

Modelクラスを継承したモデルでデータアクセスを行う場合、データベース接続に関しては裏で行われているため、一切意識しません。
実は、モデルクラスのインスタンスを生成する時にデータベースへの接続が確立され、モデルクラスの裏側でその接続オブジェクトを保持しています。そのため、複数のモデルのインスタンスを生成した場合、それぞれ別々の接続オブジェクトとなり、お互いに別々に接続が確立されていることになります。しかし、例えばトランザクション処理を行う場合など、複数のテーブルに対して同一の接続であって欲しい場合はあります。これを実現するにはいくつかの方法があります。

コネクションを共有する

モデルのインスタンスには接続オブジェクトを取得するメソッドと設定するメソッドがあります。これを利用し、あるモデルから接続を取得し、別のモデルにその接続を設定することで、複数のモデル間で接続を共有できることになります。

class CartController extends Controller
{
    public function index()
    {
        $cart = $this->model('Cart');
        $db = $cart->getConnection();
        $cartPrd = $this->model('CartProduct');
        $cartPrd->setConnection($db);
    }
}

簡単な方法ですが、実は例のCartProductの方は、インスタンス生成時に一旦独自の接続を確立し、それを閉じて別な接続を利用するような形になるため、多少効率は悪くなります。

デフォルトの接続インスタンスを設定する

Modelクラスの静的メソッド、setDefaultConnectionにPDOインスタンスを設定することで、モデルクラスのインスタンス生成時に利用する接続のデフォルトとすることができ、全てのモデルインスタンスで同一の接続を利用することができます。
この仕組を利用する場合、index.phpなどで設定を行うとよいでしょう。

仮に、ある処理だけ敢えて別接続で処理したいというような場合、Db::factoryメソッドで新たにコネクションを確立し、返り値であるPDOインスタンスをモデルのsetConnectionメソッドで設定することで可能です。

app/initializer.php
class Initializer extends InitializerStandard
{
    public function initialize()
    {
        $db = Db::factory();
        Model::setDefaultConnection($db);
    }
}

Dbクラスをシングルトン的に利用する

シングルトンとはクラス設計パターンの一つで、どこからインスタンスを取得しても必ず同一インスタンスを返すような仕組みです。CurryのDB接続を表現するクラスではこのシングルトンに対応しています。これを利用すると、Db::factoryによって得られるインスタンスは必ず同一インスタンスになり、全てのモデルクラスが同一の接続を利用するようになるので、トランザクション処理なども容易に対応できるし、コネクション数の節約となるため、データベースサーバーの負担が軽く出来ます。
シングルトンを利用するにはindex.phpに以下の一行を加えます。

app/initializer.php
class Initializer extends InitializerStandard
{
    public function initialize()
    {
        Db::setIsSingleton(true);
    }
}

またはcurry.iniでの初期設定でも対応可能です。

curry.ini
[database]
;--If you want to use singleton db connection, set this flag "1".
is_singleton = 1