2013年6月5日水曜日

Zend Framework 2とDependency Injection

高瀬です。

PHP歴約1年、まだYii Frameworkでしか開発出来ないので、少しは他の開発手法もやってみようと思う。そこで、ユニークビジョンでは馴染みがあるようなないようなZend Frameworkに注目。

Zend Frameworkについて


米Zend、PHPアプリフレームワーク「Zend Framework 2.0」をリリース (2012年9月7日)

バージョン1.0のリリースは2007年、2012年9月にバージョン2.0がリリースとのこと。2013年6月時点のバージョンは2.2。

数あるPHPフレームワークの中で、Zemd Frameworkの人気はどうだろうか。

PHPの4大フレームワークの人気を比較 (2013年1月21日)

4大フレームワークと呼ばれるものの中では、Zend Frameworkはいまひとつの様子。

さて、バージョンアップでどのように変わったのか。まずはZend Framework開発メンバーの方が作成されたこちらを参照。

Creating Re-usable Modules for Zend Framework 2 (2012年6月?)

これによると、3つのコンセプトがあるという。

Three core concepts
  • Decoupled and DI-driven (ServiceManager, Di)
  • Event-driven (EventManager)
  • HTTP handlers (DispatchableInterface)

英語で書かれているからということを差し引いても、よくわからない。では、少し観点が違うけれども、日本の方が書かれたこちらも参照。


結局よくわからないが、先ほどの資料と共通しているのは依存性の扱いが変わったことと、イベントという機能が追加されたことか。これは本家のFAQにも書かれている。


ここでは依存性の扱いについて見ていくことにする。

チュートリアルの実施


では、Zend Framework 2を使ってみよう。インストールは、スケルトンアプリケーションをダウンロードし、composerで関連モジュールを取り込む、という手順になる。チュートリアルのページを参照。

Getting started: A skeleton application

上記のページに、php composer.phar create-project --repository-url=... のコマンドでインストールするとあるが、LinuxとWindowsのどちらで試してもエラーが発生し、インストールができなかった。

しかたがないので、GithubからZendSkeletonApplication-master.zipをダウンロードし、解凍する。

解凍したら、composerで関連モジュールを取り込む。これにより、Zend Framework 2本体もインストールされる。

php composer.phar self-update
php composer.phar install

nginxの設定は以下のとおり。

server {
        root /usr/share/nginx/www/workspace/zend/public;
        index index.html index.htm index.php;
        server_name localhost;

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.(php|phtml)?$ {
                fastcgi_param  PATH_INFO        $fastcgi_path_info;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param APPLICATION_ENV development;
        }
}

設定できたら、http://lcoalhost/ でアクセスする。スケルトンアプリケーションの画面が表示されたら成功。なお、nginxの設定の問題ではないかと思うが、http://localhost/index.php でアクセスすると、「Page not found.」のエラーとなった。

チュートリアル自体は、ソースコードをコピー&ペーストしながら読み進めていくと、データベースへのデータの登録、更新、取得、削除が一通りできる内容になっている。

「Using ServiceManager to configure the table gateway and inject into the AlbumTable」の手順で、データベースへのアクセスを行うTableGatewayやAdapterのインスタンスを用意しているあたりがDependency Injectionの書き方になっている。

チュートリアルで作成したプログラムが動作したら、Dependency Injectionについて調べてみよう。


Dependency Injection


自分がDependency Injection(以下、DI)という言葉を知ったのは最近だが、これは別に新しい技術ではなく、検索すると2005年ころの記事が多く見つかる。

Java開発を変える最新の設計思想「Dependency Injection(DI)」とは (2005年2月18日)

上記のページから一部抜粋。
----------
DIを実現するメカニズムの概要は、オブジェクト相互の依存性をプログラム外部に記述して、実行時に結合することである。これにより、オブジェクト間の独立性が高まる。

----------


さて、何のことだかわからないが、自分の認識では以下のような内容。


「ソースコードの中にクラス名を直接書いていると、そのクラスを拡張した派生クラスに差し替えようと思うとあちこち修正しなければならなかったり、開発環境と本番環境でクラスを切り替えたり、というようなことがやりにくい。そこで、ソースコード中には仮のクラス名を書いておき、実際にインスタンスを作るクラスの名前は設定ファイルに書くことにする。」



Zend Framework 2ではこれを以下のページの説明のように実現している。


簡単な例が最初に示されている。A、Bという2つのクラスがあったとして、Bはコンストラクタの引数にAのインスタンスを受け取る。すなわち、Bのインスタンスを生成する構文は以下のようになる。

$b = new B(new A());

newが2回も登場するので、Aを差し替えるときも、Bを差し替えるときもソースコードの修正が面倒そうだ。そこで、newを使わずにそれぞれのインスタンスを生成させてみよう。

クラスBのコンストラクタの定義部分で、引数に型を書いておく。

class B {
        public function __construct(A $a) { ...

そのうえで、以下のように記述する。

$di = new Zend\Di\Di;
$b = $di->get('B');

こうすると、$bにはBのインスタンスが代入されるが、その際に自動的にAのインスタンスも生成される。Zend\Di\Diクラスが必要なインスタンスの準備を肩代わりしてくれるのだ。少なくともこれで、クラスAの登場場面が減ったので、Aの差し替えが楽になる。

newが消えたとはいえ、Bの方は相変わらず直接名前が指定されている。$bに代入されるインスタンスの型を設定ファイルへ追い出すことができるかどうかはまだ勉強中。


実行環境による型の切り替え


前述の例では設定ファイルが登場しなかったが、生成されるインスタンスの型を設定ファイルに追い出してみよう。

BookStoreというモジュールに、StoreControllerというコントローラがあるとする。本屋さんが仕入先の出版社から本を入荷する、と考えてみる。

<?php
namespace BookStore\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Di\Di;
use Zend\Di\Config;

class StoreController extends AbstractActionController
{
    public function indexAction()
    {
        $di = $this->getDi();
        $purchase = $di->get('BookStore\Model\Purchase');

        return new ViewModel(array(
            'purchase' => $purchase,
        ));
    }

    protected function getDi()
    {
        // http://stackoverflow.com/questions/8957274/access-to-module-config-in-zend-framework-2
        $config = $this->getServiceLocator()->get('Config');

        // http://www.eschrade.com/page/zf2-dependency-injection-managing-configuration/
        $diConfig = new Config($config['di']);

        $di = new Di;
        $di->configure($diConfig);
        return $di;
    }
}

Purchaseクラスは仕入先を表し、以下のように定義する。

<?php
namespace BookStore\Model;

class Purchase
{
    protected $publisher;

    public function setPublisher($publisher)
    {
        $this->publisher = $publisher;
    }

    public function getPublisher()
    {
        return $this->publisher->getName();
    }
}

コンストラクタの引数にPublisherクラスのインスタンスを受け取る。Publisherクラスは以下のとおり。ついでにほぼ同様の内容でPublisherMockクラスも作っておく。

<?php
namespace BookStore\Model;

class Publisher
{
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}


ここで、このプログラムを本番環境で動作させる時はPublisherクラスを使用し、開発環境の場合はPublisherMockクラスを使用したいとする。nginxから受け取っている環境変数APPLICATION_ENVで実行環境は判定。設定は config/module.config.php と config/module.config.develop.php に書くことにする。

まず環境変数APPLICATION_ENVの値により読み取る設定ファイルを切り替えるようにしてみる。というか、マージさせてみる。

Module.phpに、設定ファイルのパスを記述している部分がある。

class Module
{
    public function getAutoloaderConfig()
    {
        省略
    }

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

これを、以下のように変更。config/module.config.phpを読み取ったうえで、環境変数の値に対応するファイルがあったらそれのファイルの内容をマージする。

    public function getConfig()
    {
        $basePath = __DIR__ . '/config/';
        $config = include $basePath . '/module.config.php';
      $mode = $_ENV['APPLICATION_ENV'];

        if (!empty($mode)) {
            $filePath = $basePath . "/module.config.{$mode}.php";

            if (file_exists($filePath)) {
                $overwrite = include $filePath;
                $config = array_merge($config, $overwrite);
            }
        }
        return $config;
    }


それから、config/module.config.phpに以下の内容を追加する。

<?php
return array(
    'di' => array(
        'instance' => array(
            'BookStore\Model\Purchase' => array(
                'parameters' => array(
                    'publisher' => 'BookStore\Model\Publisher',
                ),
            ),
            'BookStore\Model\Publisher' => array(
                'parameters' => array(
                    'name' => 'Amazon',
                ),
            ),
        ),
    ),
    :

さらに、config/module.config.development.php には以下の内容を書いておく。

<?php
return array(
   'di' => array(
        'instance' => array(
            'BookStore\Model\Purchase' => array(
                'parameters' => array(
                    'publisher' => 'BookStore\Model\PublisherMock',
                ),
            ),
            'BookStore\Model\PublisherMock' => array(
                'parameters' => array(
                    'name' => 'Amazon',
                ),
            ),
        ),
    ),
);


以上のコードで、Purchaseクラスのインスタンスが作成されるとき、環境変数APPLICATION_ENVの値がdevelopmentでないときはコンストラクタの引数$publisherにPublisherクラスのインスタンスが注入され、developmentの場合はPublisherMockクラスのインスタンスが注入される。

ソースコードを1行も書き換えずに、環境変数の値だけで使用されるクラスを切り替えることができるようになった。

Zend Frameworkのサイトの説明だけでは分かりにくかったが、以下のページが参考になった。



終わり


正直なところ、DIがどのような場面で活躍するのか今ひとつ想像できていない。逆にソースコードの流れが読み取りにくくなりそうで少々心配だが、それは自分が大規模なシステム開発をしたことがないためか。

Zend Frameworkはまだチュートリアルくらいしか触っていないのでなんとも言えないが、DIやREST用コントローラ、コントローラの単体テストなどYiiでは提供されていなかったり、標準機能ではなかったりするものがあって、いろいろ勉強できそう。

またPHPの4大フレームワークと呼ばれるものを一つも知らないし、最近はFuelPHPやLaravelなどの方が有名らしいので、順番に見ていき、比較ができるようになれればよいと思う。

0 件のコメント:

コメントを投稿