ZendFramework2ガイド

入門編

モデル

モデルの役割

Zend2は自由度が売りです。CakePHPなどに代表されるような規約重視のフレームワークの逆を行っています。
このようなフレームワークの場合、モデルの扱いというのが開発者によりバラつきが出やすくなると考えられます。

しかし、例えばCakePHPなどは誤ったモデルの認識が横行しているのも事実です。それは、データを保持し、コントローラーよりの要求に応じて必要なデータを抽出し、返すというデータストアという役割を担っていると考えられがちなことです。しかしこれは少し誤っています。

モデルはアプリケーションに関連する処理プロセス全般を担うべきです。トランザクションの責任を受け持つのはモデルとも言えます。CakePHPで多く見られるのは、ActiveRecordという考え方のもと、モデルの1クラスをデータベーステーブル(正確にはレコード)に紐づける手法です。これ自体は間違っていません。しかし、例えばデータ更新処理などで復数のテーブルをトランザクションで整合を保つような処理が必要な場合、モデル=テーブルという考え方で行くと、トランザクションを管理するのはコントローラーということになってしまいます。それは正しくはモデルの役割です。これは、CakePHPの公式マニュアルにも明記されています。

この問題はCakePHPに罪はありません。ActiveRecord=モデルという、世間の誤った認識によるものです。つまり、多くの実装者はMVCフレームワークの本質を理解しないままに、形だけで実装しているパターンが多いと考えられます。

何が言いたいかというと、Zend2の場合、モデルの考え方は完全に開発者に委ねられています。モデルという概念についてはマニュアルにも明確には触れていません。スケルトンアプリケーションの初期状態を見てみても、srcディレクトリ以下にControllerは存在しますが、Modelは存在していません。なのでどういう形でモデルの実装をしていくか、悩んでしまうところです。

ここで一つの提案としては、上記のようなデータストアとしての役割のモデルと、誤った認識によりコントローラーに流入していたようなトランザクションだとかアプリケーションロジックを管理するモデルの2段階にするということが挙げられます。幸いなことにZend2ではディレクトリ階層はフレームワークとしては規定がなく、全ては名前空間との関連であるため、どのようにもすることが出来ます。例えばModel/DataとModel/Entityなどのように明確にディレクトリを分けてしまってもいいし、クラス名で判別出来るようにしておけばModel直下に混在させてしまってもいい。Zend2におけるモデルはディレクトリ階層もクラス名も規則が無いため、好きにできることが特徴の一つといえるわけですから。

具体的に、例えば通販サイトを例としてみましょう。
受注データを考えた場合、受注ヘッダと受注明細という2つのテーブル設計になる事が多いと思います。まずはこれをそれぞれクラス化する。これはいわゆる物理モデルです。しかしこれはリレーショナルデータベースというシステム環境に依存したもので、アプリケーションのエンティティとしては「受注」というひとつの事象になります。このエンティティをそのままクラスかしたものを論理モデルとして作成し、「受注ヘッダ」と「受注明細」という2つのデータストアを利用して「受注」に関わる処理やトランザクション全般を請け負う。このような設計が考えられます。
仮に、これをディレクトリ構造として考えたら以下のようになるでしょうか。

src/
 ∟Ec/
  ∟Controller/
  ∟Model/
   ∟Entity/
    ∟Order.php
   ∟Data/
    ∟OrderHeader.php
    ∟OrderDetail.php

または、モデル→データという階層になっていることをディレクトリ構造で明確にするなら、エンティティ系はModel直下に、データ系はModelの下にDataディレクトリをつくり、その中に入れるというのもいいと思います。

src/
 ∟Ec/
  ∟Controller/
  ∟Model/
   ∟Order.php
   ∟Data/
    ∟OrderHeader.php
    ∟OrderDetail.php

一つのモデル設計例としてみてください。

モデル環境の作成

ディレクトリの作成

スケルトンアプリケーションにはModelクラスを入れるディレクトリが存在しないため、これを作成します。

module/
 ∟Ec/
  ∟src/
   ∟Ec/
    ∟Model/

ここから先は自由ですが、「モデルの役割」のところで紹介したとおり、エンティティとデータの2層構造とするためにModel内にDataディレクトリを作成します。

module/
 ∟Ec/
  ∟src/
   ∟Ec/
    ∟Model/
     ∟Data/

データベース接続設定

データベースへの接続情報の設定を行います。ここは公式マニュアルにならいます。

データベース接続情報を設定するファイルは
config/autoload/global.php
config/autoload/local.php
の2つです。

local.phpはデフォルト状態では存在していませんが、作成します。

config/autoload/global.php
<?php

return array(
     'db' => array(
         'driver'         => 'Pdo',
         'dsn'            => 'mysql:dbname=ec;host=localhost',
         'driver_options' => array(
             PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
         ),
     ),
);
config/autoload/local.php
<?php

return array(
     'db' => array(
         'username' => 'ec_user',
         'password' => 'dke2HdxE',
     ),
);

local.phpの内容はglobal.phpに書いてしまっても実は動作します。最終的にはこのふたつはマージされます。
ではなぜ分かれているかというと、ソース管理ツールなどを利用し、ソースをWebで配布している場合など、ローカルで行った変更をそのまま公開上に反映される様な状態の場合、データベース接続情報のうちのユーザー名やパスワードなどが含まれていると、自分の環境のパスワードがバレてしまいます。なので、その部分だけはソース管理上から外しましょうといわけです。上記の状態であれば、local.phpだけをソース管理から外し、それだけはユーザー各々で作成してください、とするわけです。

そこまで厳格にやる必要が無いと言える環境であれば、global.phpに全て書いてしまってもかまいません。

データクラスの作成

Zend2のデータベーステーブル操作用のクラスとしてはTableGatewayがあります。これを利用することにより、SELECT、INSERT、UPDATE、DELETEというような処理をメソッドレベルで実行できる用になっています。公式マニュアルでは、テーブルクラスを作成し、TableGatewayのインスタンスをクラス内部に保持するような例が紹介されていますが、ここではTableGatewayを継承してテーブルクラスを作成するという手法を紹介したいと思います。

まずはTableGatewayを継承してクラスを作成してみましょう。

module/Ec/src/Ec/Model/Data/OrderHeader.php
namespace Ec\Model\Data;

use Zend\Db\TableGateway\TableGateway;

class OrderHeader extends TableGateway
{
    public function __construct($adapter)
    {
        parent::__construct('order_header', $adapter);
    }
}

注文ヘッダ情報を保持するOrderHeaderクラスです。

クラスファイルを作成したディレクトリがsrc以下、Ec/Model/Dataなので、クラスの属する名前空間をEc\Model\Dataとします。
TableGatewayのコンストラクタとして、実際のテーブル名と、データベース接続状態を保持しているアダプタークラスのインスタンスを要求しているため、それらを指定します。

なので、まずは環境の作成のところで出てきた、global.php、local.phpの設定情報をもとにしたデータベース接続アダプターの生成が必要になります。

まず、global.php、local.phpの設定は、ApplicationクラスのgetConfigメソッドで取得することが出来ます。
public/index.phpを見ていただければわかりますが、ここでApplicationクラスのインスタンスが生成されています。
これにglobal.php、local.phpの情報が読み込まれ、保持されています。

ではApplicationのインスタンスはどうやって取得するのか。
これはモデルのクラスなどから取得は出来ません。コントローラークラスまでは取得する仕組みが備わっていますが、そのままでは、コントローラーからモデルクラスのインスタンスを生成するたびにパラメーターとして渡してやる必要が出てきます。これはコーディングとしては非効率的で、コントローラーとモデルの結合性を下げる意味でもよろしくありません。

公式マニュアルでは、モデルクラスのインスタンスを生成するメソッドをコントローラーに作成し、モデルクラスの生成はこれを介して行う手法が挙げられていますが、
テーブルの数だけこれを行うのは正直、あまりおすすめ出来る方法ではないように思います。

Module.phpを活用する

Module.phpはmodule/{モジュール名}/の直下にあり、モジュール全体に関わる処理を行うクラスです。
最初からonBootstrapというメソッドが存在していますが、アプリケーションが起動した(リクエストを受けた)直後のかなり早いタイミングでこのメソッドが実行されます。
ここで、データベース接続アダプターを生成します。そして、GlobalAdapterFeatureというクラスにスタティック変数としてデータベース接続アダプターを設定出来るようになっているので、ここにセットすればどこからでも取得が出来るというわけです。

module/Ec/Module.php
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

use Zend\Db\Adapter\Adapter;    // ←追加
use Zend\Db\TableGateway\Feature\GlobalAdapterFeature;    // ←追加

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        // 追加
        $this->createDbAdapter($e);
    }
    
    // 追加
    protected function createDbAdapter(MvcEvent $e)
    {
        $config = $e->getApplication()->getConfig();
        $adapter = new Adapter($config['db']);
        GlobalAdapterFeature::setStaticAdapter($adapter);		
    }

~~~(中略)~~~

}

その上で、データクラスを以下のようにします。

module/Ec/src/Ec/Model/Data/OrderHeader.php
namespace Ec\Model\Data;

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\TableGateway\Feature\GlobalAdapterFeature;

class OrderHeader extends TableGateway
{
    public function __construct($adapter = null)
    {
        if ($adapter == null) {
            $adapter = GlobalAdapterFeature::getStaticAdapter();
        }
        parent::__construct('order_header', $adapter);
    }
}

これで、単に new OrderHeader() とするだけでモデルのインスタンスが得られるわけですが、このようなテーブルモデルをたくさん作成することを考えると、コンストラクタの部分やuseをそれら全てに、ほとんどコピーのような状態で作成する必要があります。なので、これらを実装した基底クラスを作成し、それを継承するような形にします。

module/Ec/src/Ec/Model/Data/TableModel.php
namespace Ec\Model\Data;

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\TableGateway\Feature\GlobalAdapterFeature;

class TableModel extends TableGateway
{
    // テーブル名
    protected $name;

    public function __construct($adapter = null)
    {
        if ($adapter == null) {
            $adapter = GlobalAdapterFeature::getStaticAdapter();
        }
        parent::__construct($this->name, $adapter);
    }
}
module/Ec/src/Ec/Model/Data/OrderHeader.php
namespace Ec\Model\Data;

class OrderHeader extends TableModel
{
    protected $name = 'order_header';
}

これで、テーブルモデルクラスはかなりスッキリします。

同様にOrderDetailクラスも作成します。
注文明細データを保持するテーブルのクラスです。

module/Ec/src/Ec/Model/Data/OrderDetail.php
namespace Ec\Model\Data;

class OrderDetail extends TableModel
{
    protected $name = 'order_detail';
}

エンティティモデルクラスの作成

作成したOrderHeaderとOrderDetailはテーブルを表す物理モデルクラスです。
それではそもそものアプリケーションに登場するエンティティとしての「注文」を表す論理モデルを作成します。

module/Ec/src/Ec/Model/Order.php
namespace Ec\Model;

use Ec\Model\Data\OrderHeader;
use Ec\Model\Data\OrderDetail;

class Order
{
    public function add($data)
    {        
        $adapter = GlobalAdapterFeature::getStaticAdapter();
        $connection = $adapter->getDriver()->getConnection();

        $header = new OrderHeader();
        $detail= new OrderDetail();

        $connection->beginTransaction();
        try {
            $header->insertNewOrder($data['user_id'], $data['pay_type']);
            $detail->insertOrderItems($data['items']);
            $connection->commit();
        } catch (\Exception $e) {
            $connection->rollback();
            return false;
        }
        return true;
    }
}

受注関連のロジックを受け持つOrderというクラスを作成しました。
まず作成したのはコントローラーから受け取った受注データをもとに受注処理を行うaddというメソッドです。

実際の受注情報を保持するのはOrderHeaderとOrderDetailというは2つのテーブルモデルですので、それぞれのテーブルに必要な情報を引数から取り出して、それぞれの処理メソッドへ引き渡しています。
実際の受注処理では例えば代金の決済を、専門の代行業者を利用する場合が多いので、その連携APIを実行するとか、その他受注時に行うべき処理でテーブルへのINSERT以外にもいろいろと考えられます。テーブルへのINSERTも含め、その辺のロジックのひと通りの流れを管理するのがエンティティとしてのOrderクラスの役割です。多くのMVC実装を見ていると、この部分が全てコントローラーに書かれている事が多くなっていますが、モデルに書くのが正しいです。

Zend2でトランザクション処理をするには、Connectionクラスのインスタンスを取り出す必要があります。
GlobalAdapterFeatureを利用している場合は上の様な実装になります。
ConnectionはAdapterの内部に保持されているため、GlobalAdapterFeatureからAdapterのインスタンスを取得し、取得したAdapterのインスタンスからConnectionインスタンスを取得します。
いろいろなメソッドでいちいちこれを記述するのがうっとおしければ、別メソッドに切り出すといいと思います。

ちなみにGlobalAdapterFeature::getStaticAdapter()を利用した場合、取得されるAdapterのインスタンスはどこでやっても同一のインスタンスです。
上の「テーブルクラスの作成」のところで解説したようにGlobalAdapterFeature::setStaticAdapter($adapter)とやっておけば、そこでセットしたインスタンスがずっと使いまわされる事になり、コネクション数の節約になるため、サーバーへの負荷も軽くすみます。

そうではなく、常にいちいち新しいコネクションを張るような場合、以下の様になります。

class Order
{
    public function add($data)
    {
        $config = $this->getDbConfig();
        $adapter = new Adapter($config);
        $connection = $adapter->getDriver()->getConnection();

        $header = new OrderHeader($adapter);
        $detail= new OrderDetail($adapter);

        $connection->beginTransaction();
        try {
            $header->insertNewOrder($data['user_id'], $data['pay_type']);
            $detail->insertOrderItems($data['items']);
            $connection->commit();
        } catch (\Exception $e) {
            $connection->rollback();
            return false;
        }
        return true;
    }

    protected function getDbConfig()
    {
        $config = ~~ // ←どうにかしてDBのコンフィグを取得

        return $config;
    }
}

ポイントは、OrderHeaderやOrderDetailのコンストラクタに$adapterを渡していることです。
トランザクションは当然、同一コネクション内でしか効かないので、新たなコネクションをその場で確立した場合、INSERT処理自体もそのインスタンスを使いまわさなければなりません。

getDbConfig()内の「どうにかしてDBのコンフィグを取得」と書いた部分については、どうにかします。
global.phpやlocal.phpから取得したいとしたら、上記のままではその手段がないので、コンストラクタなどでコントローラーからなにかパラメーターとして受け取っておく必要があります。そのためには、コントローラーではエンティティモデルのnewの時には必ずglobal.phpやlocal.phpにつながるパラメータを渡さなければならなくなります。

なので、前者の様なGlobalAdapterFeatureを利用した方法をおすすめします。

他に、ファクトリという仕組みを利用した方法も考えられます。