ZendFramework2ガイド

機能編

サービスマネージャー

サービスマネージャーとは

サービスマネージャーは、Zend2の核となる仕組みの一つです。

まずサービスって何って話です。あるオブジェクトから見て、そのオブジェクトの役割を果たすためにはある別のオブジェクトの機能を必要とする場合があります。この場合にその必要な機能を提供してくれるオブジェクトをサービスって呼んでいるわけです。
ここでは仮にあるオブジェクトをAとします。そしてAが必要とする機能を提供するオブジェクトをBとします。

で、普通に考えると、単純にオブジェクトAがオブジェクトBの機能を利用するなら、Aの中でBをnewして利用すればいいわけですが、これがあまり良くないというのが世間の認識なわけです。何がいけないかというと、Aの中に、new Bと書いてあったら、AはBありきのクラスになってしまい、AとBは切っても切れない関係になってしまいます。このようなAがBに依存しているような状態は、仕様変更に弱いとかクラスの単体テストがやりにくいとかいう弊害があるというわけです。

これを解決するために、AとBの間に挟まり、間接的にオブジェクトを得られるような機能を提供するのがサービスマネージャーです。
これは「サービスロケーター」というデザインパターンに則ったクラスなわけですが、そのへんの説明は解析編の「サービスロケーター」のところで詳しく書いていますので、そちらを参照して下さい。

Zend2では、MVCの基盤として必要な機能の多くをServiceManagerというクラスで管理しており、必要に応じてあちこちからこのServiceManagerを通して必要なクラスのインスタンスを取得するような実装になっています。

ServiceManagerへのサービス登録方法の種類

ServiceManagerにサービスを登録する方法にはいくつかの種類があります。
それは以下の4つです。

それぞれ解説します。

Service

Serviceは、ServiceManagerの外側で生成されたインスタンスをそのままサービスとして登録します。
最も単純で基本的なサービス登録と言えますが、依存性の注入的とかシングルトンというような意味合いでしかなく、ServiceManagerのメリットがあまり生かされていないため、使いどころは少ないでしょう。

登録にはsetServiceメソッドを利用します。

namespace Hoge;

class Fuga
{

}
// インスタンスをサービスとして登録
$fuga= new Fuga();
$serviceManager->setService('Fuga', $fuga);

------------------------------------------------------------

// 登録したインスタンスを取得
$fuga = $serviceManager->get('Fuga');

InvokableClass

Serviceでは最初からインスタンスを登録しますが、InvokableClassはクラス名を登録します。
そしてgetメソッドで要求が来た時に初めてインスタンスを生成して返してくれます。
サービスの登録は文字列情報としてのクラス名でしかないため、コンフィグファイルなどで定義したものから登録するような形にすれば、getで返ってくるオブジェクトのクラスを差し替えることも用意にできるわけで、有効と言えます。

ただしInvokableClassで登録されたクラスは、getされた時、ServiceManager内で直接newしており、は"new クラス名()"という感じで、コンストラクタに引数がない前提になってしまっているため、コンストラクタで引数と必要とするクラスには対応しません。

登録にはsetInvokableClassメソッドを利用します。

namespace Hoge;

class Fuga
{

}
// InvokableClassを登録
$serviceManager->setInvokableClass('Fuga', 'Hoge\Fuga');

---------------------------------------------------------------------------

// インスタンスを取得
$fuga = $serviceManager->get('Fuga');

Factory

これもクラス名を登録しますが、getにより取得したいサービスのクラスそのものではなく、それを生成する機能を持つファクトリクラスです。登録するのはファクトリクラスのインスタンスでもかまいませんが、基本的にはクラス名で登録しないとServiceManagerのメリットは生きません。
これによりgetした時に、ファクトリクラスそのもののインスタンスではなく、ファクトリクラスが生成するインスタンスになります。
また登録するファクトリクラスは、Zend\ServiceManager\FactoryInterfaceを実装していなければなりません。

InvokableClassとの違いとして、ServiceManagerで直接newするわけではなく、newする部分は自分で書けるため、コンストラクタで引数を要求するクラスにも対応することが出来ます。

登録にはsetFactoryメソッドを利用します。

namespace Hoge;

class Fuga
{

}
namespace Hoge;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class FugaFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $fuga= new Fuga();
        return $fuga;
    }
}
// Factoryを登録
$serviceManager->setFactory('Fuga', 'Hoge\FugaFactory');

---------------------------------------------------------------------------

// Factoryの生成するインスタンスを取得
$fuga = $serviceManager->get('Fuga');

Alias

これは単純に、既に登録されている別のサービスに別名を付けるだけのものです。

登録にはsetAliasメソッドを利用します。

namespace Hoge;

class Fuga
{

}
// InvokableClassを登録
$serviceManager->setInvokableClass('Fuga', 'Hoge\Fuga');
// エイリアスを設定
$serviceManager->setAlias('Piyo', 'Fuga');

---------------------------------------------------------------------------

// エイリアスを指定してインスタンスを取得
$fuga = $serviceManager->get('Piyo');

同じサービスを複数回要求した場合の動き

ServiceManagerのgetメソッドでサービスを取得した場合、デフォルトでは全て同一のインスタンスが返ってきます。
つまりServiceManagerはサービスをシングルトンとして管理していることになります。

しかし、getの都度、新しいインスタンスを生成させることも可能です。

setShared

setSharedメソッドは、対象のサービス名を指定して、毎回インスタンスを生成するか否かを設定できます。
デフォルトは全てのサービスが同一のインスタンスを返す設定になっていますが、setSharedで特定のサービスをfalseに設定した場合は、そのサービスのgetの場合のみ、都度インスタンスを新しく生成します。

$serviceManager->setInvokableClass('Fuga', 'Hoge\Fuga');
$serviceManager->setShared('Fuga', false);
$fuga1 = $serviceManager->get('Fuga');
$fuga2 = $serviceManager->get('Fuga');
var_dump($fuga1 === $fuga2);
bool(false) 

setShareByDefault

setShareByDefaultでtrueを指定すると、ServiceManagerは全てのサービスの取得において毎回新たなインスタンスを生成するようになります。
ただし、setSharedメソッドでサービス個別にfalseに設定されている場合は、そのサービスに限っては同一のインスタンスを返します。
注意点として、先にsetAllowOverrideメソッドでtrueを指定しておかないと、setShareByDefaultの設定はできません。

$this->getServiceLocator()->setAllowOverride(true);
$this->getServiceLocator()->setShareByDefault(false);
$serviceManager->setInvokableClass('Fuga', 'Hoge\Fuga');
$fuga1 = $serviceManager->get('Fuga');
$fuga2 = $serviceManager->get('Fuga');
var_dump($fuga1 === $fuga2);
bool(false) 

コンフィグにサービスを定義する

スケルトンアプリケーションのmodule.config.phpを見ると、'service_manager'というキーが存在するのに気が付くと思います。
これはまさにServiceManagerにサービスとして登録するクラスの定義です。
ここにサービス名とクラス名のマッピングを設定しておくだけで、ServiceManagerのgetメソッドによりインスタンスが得られるようになります。

module/Hoge/config/module.config.php
    'service_manager' => array(
        'invokables' => array(
            'Fuga' => 'Hoge\Fuga',
        ),

        'factories' => array(

        ),

        'aliases'=> array(

        ),
    ),
$fuga = $serviceManager->get('Fuga');
var_dump(get_class($fuga));
string(9) "Hoge\Fuga" 

'service_manager'の下の'invokables'、'factories', 'aliases'はそれぞれ、setInvokableClassメソッド、setFactoryメソッド、setAliasメソッドで登録するのと同様の意味を持ちます。

module.config.phpに設定しておくと、ここに設定したクラス名を書き換えるだけで、その他のソースコードは一切触ること無く、得られるインスタンスのもととなるクラスを差し替える事が出来ます。これはクラスの単体テストをやりやすくしたり、仕様変更により、クラスが変更になるなどに対して非常に強くなるので、ServiceManagerの利用方法としては最も望ましいといえると思います。

ServiceManagerの使いどころ

クラス同士の依存度を下げる目的で使用

クラス同士の依存度を下げるという、本来の目的を達するためにServiceManagerを利用する場合、基本的には全てのインスタンス生成で直接newは行わずに、ServiceManagerを利用し、インスタンス生成の手続きは隠蔽するのが望ましいということになります。その意味で言うと、例えばコントローラークラスでモデルクラスを利用する場合などにも、モデルクラスをnewするのではなく、ServiceManagerを利用すると良いでしょう。

このような場合、module.config.phpに、モデルクラス全てをinvokablesとして設定を並べておく方法もありますが、ここで威力を発揮するのはfactoriesです。

まずModelクラスのインスタンスを生成するModelLoaderみたいなクラスを作ります。
次に、ModelLoaderのインスタンスを生成するファクトリクラスとしてModelLoaderFactoryを作ります。
そしてこれをmodule.config.phpのfactoriesに設定します。
factoriesで設定したファクトリクラスはFactoryIntafaceを実装している必要が有るため、createServiceというメソッドの実装が必要になります。そしてServiceManagerでインスタンス生成する時にcreateServiceメソッドが呼び出されるわけですが、これの引数としてServiceManagerのインスタンスが渡ってきます。
ModelLoaderFactoryのcreateServiceメソッドでは、ModelLoaderのインスタンスを生成し、このインスタンスにServiceManagerのインスタンスを渡します。
module.config.php自体もサービスとして登録されているため、ServiceManagerのインスタンスを内部に持っているクラスはmodule.config.phpを直接参照できることになります。これを利用し、module.config.phpにオリジナルの'models'というセクションを設け、モデルクラスのクラス名と、それに対する呼び出し名を設定できるようにします。

この状態で、コントローラークラスではServiceManagerを使ってModelLoaderのインスタンスを取得します。
そして、ModelLoaderからmodule.config.phpに設定したモデルクラスの呼び出し名を指定すると、それに対するモデルクラスのインスタンスが得られるようにするわけです。
以下に例を示します。

ModelLoader
namespace Hoge\Model;

use Zend\ServiceManager\ServiceLocatorInterface;

class ModelLoader
{
    protected $serviceLocator;

    public function get($modelName)
    {
        // module.config.phpにモデルのサービス定義が存在したら、それをモデルクラス名として取得
        $moduleConfig = $serviceLocator->get('Config');
        if (isset($moduleConfig['models'][$modelName])) {
            $modelName = $moduleConfig['models'][$modelName];
        }
        $model = new $modelName();
        return $model;
    }

    public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
    {
        $this->serviceLocator = $serviceLocator;
    }
}
ModelLoaderFactory
namespace Hoge\Model;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ModelLoaderFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $modelLoader = new ModelLoader();
        $modelLoader->setServiceLocator($serviceLocator);
        return $modelLoader;
    }
}
module.config.php
    'service_manager' => array(
        'factories' => array(
            'ModelLoader' => 'Hoge\Model\ModelLoaderFactory',
        ),
    ),

    'models' => array(
        'Fuga' => 'Hoge\Model\Fuga'
    ),
コントローラー
class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $hoge = $this->modelLoader()->get('Fuga');
    }

    public function modelLoader()
    {
        return $this->getServiceLocator()->get('ModelLoader');
    }
}

シングルトンとして使用

上の方で説明したように、ServiceManagerで登録されたサービスのインスタンスを複数回取得する場合、基本的には同一のインスタンスが返ってきます。
この性質を利用し、コントローラーやモデルなどをまたいで同一のインスタンスを使用したい場合や、イベントをまたいで同一のインスタンスを使用したい場合などに、いちいち引数で引き回さなくてもよくなります。

シングルトンとは、特定のクラスのインスタンスはかならず一つしか存在しないことを保証する仕組みで、オブジェクト指向のデザインパターンの一つです。

これが有効なのは、例えばデータベース接続のAdapterクラスです。
Adapterクラスはデータベース接続を行うクラスですが、復数のインスタンスを生成すると、インスタンスごとに別々にデータベースへの接続を行います。これを例えばテーブルのモデルクラスの中などでいちいちnewしていたら、一度のHTTPリクエストに対する処理でたくさんの接続を行ってしまい、データベース・サーバーに負担をかけることになる可能性があります。また、Adapterの生成にはユーザーやパスワードなどの接続情報が必要ですが全てのデータのモデルクラスでこの接続設定を参照してAdapterをnewしていては、あまりに無駄です。
そこでAdapterをサービスとして登録しておけば、一度生成したAdapterのインスタンスは、ServiceManagerによってずっと同じインスタンスを返し続けてくれるわけです。Zend2はそれをちゃんと想定してくれていて、そのためのファクトリクラスを用意してくれています。それはZend\Db\Adapter\AdapterServiceFactoryです。

これをfactoriesとしてServiceManagerに登録するだけです。
ただし、そのためにはデータベース接続設定をコンフィグに設定しておく必要があります。

config/autoload/global.php
return array(
    // データベース接続設定
    'db' => array(
        'driver'         => 'Pdo',
        'dsn'            => '',
        'driver_options' => array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ),
    ),

    // AdapterServiceFactory
    'service_manager' => array(
        'factories' => array(
            'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
        ),
    ),
);

データベース接続設定は、マニュアルではglobal.phpとlocal.phpに設定するようになっているので、上記のように設定しておくだけで、ServiceManagerを通してAdapterのインスタンスが取得できます。

module/Hoge/src/Controller/IndexController.php
    public function indexAction()
    {
        $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
        $tableGateway = new Zend\Db\TableGateway\TableGateway('fuga', $adapter);
    }