ZendFramework2ガイド

解析編

サービスロケーター

サービスロケーターとは

サービスロケーターの定義

適当なことを言うと誤解を招くので正確な定義については言明する事は避けますが、サービスロケーターとはオブジェクト指向のデザインパターンの一つで、Javaの世界ではよく語られるものです。
まずサービスって何?って話からです。あるオブジェクトから見て、そのオブジェクトの役割を果たすためにはある別のオブジェクトの機能を必要とする場合があります。この場合にその必要な機能を提供してくれるオブジェクトをサービスって呼んだりするわけです。

ここでは以降、大元のオブジェクトがAとしましょう。Aが利用している別のオブジェクトをXとしましょう。

で、単純に考えると、このオブジェクトAがオブジェクトXの機能を必要とするということは、プログラミング的に見るとオブジェクトAの持つメソッドの中で、オブジェクトXをnewするっていうことになると思います。これが良くないので何とかする仕組みがサービスロケーターというわけです。
何がいけないか?直接newするっていうことは、クラスAにとってnewする特定のクラスXが必要であるということが、固定化されてしまうわけです。必要なんだからそれでいいじゃないかって聞こえてきそうですが、そのnewするクラスXは、他にもBとかCとかいろいろなクラスからnewされているとしましょう。そしてある日、そのnewしているクラスXの役割を果たしているクラスがYに変更になりました。となったら大変です。直接Xをnewしているところを全て洗い出し、全てYに書き換えていかなければなりません。しかも、もしかしたら引数の数とか変わっているかもしれません。

依存性・結合度

上のような状態を「結合度が高い」「密結合」なんて呼ばれ方をしたりします。クラス同士の依存度が高い状態です。

依存度を弱める手法としてよく言われるのがディペンデンシーインジェクション、いわゆるDIです。訳すと「依存性の注入」です。
上で言っているようなクラスAとクラスBの関係を改善し、クラスAの中で直接クラスBをnewするのではなく、例えばコンストラクタやメソッドの引数などで外側からBのインスタンスを渡してやるという手法です。

サービスロケーターとDIはよく比較されます。DIを提供するクラスはZend2にも用意されていますが、Zend2のMVCを実現する仕組みとして利用されているのがサービスロケーターの方です。サービスロケーターがZend2の核の一つと言えるしょう。

サービスロケーターの役目

Zend2の思想の一つに「疎結合」という事があります。上のような状態を出来る限り排除した作りですね。
Zend2ではこれをどうやって実現しているかということの答えの一つがサービスロケーターです。

サービスロケーターとは何かというと、簡単に言えば、サービスと呼ばれるオブジェクトのインスタンスの生成を管理するオブジェクトです。
クラスAはクラスXを直接newするのではなく、サービスロケーターに対して、Xのオブジェクトを下さい、と言うわけです。
サービスロケーターはクラスXをnewし、Xのインスタンスを返すわけです。
ただ、この「Xクラスのオブジェクトを下さい」っていうのは、Xのクラス名そのものを指定して依頼するわけではありません。もしそうだとすると、サービスロケーターは指定された名前のクラスをそのままnewして返すだけなので、Aで直接newしているのと実質的には何ら変わりません。
そこで依頼の方法を「Xクラスのオブジェクトを下さい」ではなく、「別名xと名付けられたクラスのオブジェクトを下さい」とするわけです。
サービスロケーターでは、実際のクラス名と、それに付けられた別名のマッピングリストを持っており、「別名xのオブジェクト下さい」と言われたら、マッピングリストから別名xに対応するクラスの実際のクラス名を特定し、そのクラスのインスタンスを生成して返すわけです。そしてこのマッピング配列は別途のコンフィグファイルなどとして切り出しておきます。たまたま、別名がxのクラスは実際にはXであるという設定になっていたら、Aがサービスロケーターに対して「別名xと名付けられたクラスのオブジェクトを下さい」と依頼したことによって返ってくるのはXのインスタンスということになるわけです。

こうしておくと、別名xに対する実際のクラス名のマッピングのみを書き換えることで簡単に差し替えが可能になるので、クラスAにとってのサービスとは何であるかは固定化されることはなく、クラスAと、サービスの関連性はこのコンフィグファイルのみの一点が握ることになります。
このようにして結合度を下げているのがサービスロケーターです。

DIとサービスロケーターの違いは非常に難しいところですが、著者の解釈が正しければ、あるオブジェクトと、そのオブジェクトが必要とする別のオブジェクトの間に挟まり、要求されたオブジェクトを生成して返すのがサービスロケーター。対して、オブジェクトとオブジェクトの間の依存関係を把握しているのがDIで、あるオブジェクトが、更に別のオブジェクトを要求しているということを把握し、あるオブジェクトに対して別のオブジェクトを注入することまでやるのがDIということになると思います。

依存性の異なる3つの例

以下の3つは結果的には同じ事をやっているわけですが、下のものほどオブジェクト同士の依存性が弱くなっています。
1番目は直接依存、2番目は依存性の注入、3番めはサービスロケーター利用です。
2番めもCart自体はUserに依存していませんが、外側ではUserがnewされており、例えば同じようなUserを必要とするクラスが他にもあったとしたら、同じようにそこでもnew Userと書くでしょう。仮にUserを他のクラスに差し替えたいと思っても、new Userはあちこちに散らばっています。
サービスロケーターの例では、クラス名が直接記述されるのは冒頭の連想配列の部分のみです。

CartがUserに依存
class Cart
{
    private $user;

    public function __construct()
    {
        $this->user = new User();
    }
}

$cart = new Cart($userId);
Userを外から注入
class Cart
{
    private $user;

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

$user = new User();
$cart = new Cart($user);
サービスロケーターを利用
$serviceMap = array(
    'user' => 'User',
    'product' => 'Product',
);

class ServiceLocator
{
    private $services = array();

    public function addService($key, $className)
    {
        $this->array[$key] = $className;
    }

    public function get($key)
    {
        $className = $this->services[$key];
        $instance = new $className();
        return $instance;
    }
}

class Cart
{
    private $serviceLocator;
    private $user;

    public function __construct($serviceLocator)
    {
        $this->serviceLocator = $serviceLocator;
        $this->user = $this->serviceLocator->get('user');
    }
}

$serviceLocator = new ServiceLocator();
foreach ($serviceMap as $key => $className) {
    $serviceLocator->addService($key, $className);
}

$cart = new Cart($serviceLocator);

ServiceManagerクラス

Zend2でサービスロケーターの役割を果たすのはZend\ServiceManager\ServiceManagerクラスです。
ServiceManagerはZendのMVCに深く絡みこんでおり、切っても切り離せない状態です。しかしこれにより、各種コンポーネント同士は依存性が非常が薄い状態とすることが実現されています。

まずはこのServiceManagerがどこにいるかを見てみましょう。
ServiceManagerのインスタンスはコントローラークラスが持っています。コントローラークラスで$this->getServiceLocator()としてみましょう。ServiceManagerのインスタンスが返ってきます。

module/Hoge/src/Hoge/Controller/IndexController.php
class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        var_dump(get_class($this->getServiceLocator()));
    }
}
string(34) "Zend\ServiceManager\ServiceManager" 

ではServiceManagerはどのようにして利用するのか。
ServiceManagerクラスの中身を見てみると、getというメソッドを持っています。このgetメソッドこそが、上の方で説明している、サービスロケーターから関節的にインスタンスを得るためのメソッドです。第1引数を見てみましょう。$nameで文字列を要求しています。これがまさにクラスに付けられた別名です。
では別名と実際のクラス名のマッピングはどこに持っているでしょうか。
フィールド定義を見てみると、$factoriesとか$invokableClassesとかいくつかの配列があります。このへんがそれに当たります。それぞれの違いは後ほど説明します。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
class ServiceManager implements ServiceLocatorInterface
{

    /**
     * @var array
     */
    protected $invokableClasses = array();

    /**
     * @var string|callable|\Closure|FactoryInterface[]
     */
    protected $factories = array();

    /**
     * @var AbstractFactoryInterface[]
     */
    protected $abstractFactories = array();

    /**
     * @var array[]
     */
    protected $delegators = array();

    /**
     * Retrieve a registered instance
     *
     * @param  string  $name
     * @param  bool    $usePeeringServiceManagers
     * @throws Exception\ServiceNotFoundException
     * @return object|array
     */
    public function get($name, $usePeeringServiceManagers = true)
    {
        ~~~(中略)~~~
    }


}

これらの配列フィールドは全てデフォルトでは空の配列であるため、外側からセットされるのが前提ということがわかると思います。
どこでセットされるか。その答えの一つはコンストラクタです。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
    /**
     * Constructor
     *
     * @param ConfigInterface $config
     */
    public function __construct(ConfigInterface $config = null)
    {
        if ($config) {
            $config->configureServiceManager($this);
        }
    }

コンストラクタの引数でコンフィグらしきクラスのインスタンスを受けていて、いかにもコンフィグをServiceManagerに適用している雰囲気ですね。
ではServiceManagerをnewしているところを見てみましょう。それはZend\Mvc\Application.phpです。

vendor/ZF2/library/Zend/Mvc/Application.php
class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{
    ~~~(中略)~~~

    public static function init($configuration = array())
    {
        $smConfig = isset($configuration['service_manager']) ? $configuration['service_manager'] : array();
        $serviceManager = new ServiceManager(new Service\ServiceManagerConfig($smConfig));
        $serviceManager->setService('ApplicationConfig', $configuration);
        $serviceManager->get('ModuleManager')->loadModules();

        $listenersFromAppConfig     = isset($configuration['listeners']) ? $configuration['listeners'] : array();
        $config                     = $serviceManager->get('Config');
        $listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : array();

        $listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));

        return $serviceManager->get('Application')->bootstrap($listeners);
    }

ApplicationはZend2におけるMVCのメインクラスです。
initという、いかにも初期処理を行っていそうなメソッドの中でServiceManagerがnewされています。その引数として渡されているのが、Zend\Mvc\Service\Service\ServiceManagerConfigというクラスのインスタンスです。
それではServiceManagerConfigの中身を見てみましょう。

vendor/ZF2/library/Zend/Mvc/Service/Service/ServiceManagerConfig.php
class ServiceManagerConfig implements ConfigInterface
{
    /**
     * Services that can be instantiated without factories
     *
     * @var array
     */
    protected $invokables = array(
        'SharedEventManager' => 'Zend\EventManager\SharedEventManager',
    );

    /**
     * Service factories
     *
     * @var array
     */
    protected $factories = array(
        'EventManager'  => 'Zend\Mvc\Service\EventManagerFactory',
        'ModuleManager' => 'Zend\Mvc\Service\ModuleManagerFactory',
    );

まさに、クラス名に対して別名のマッピングを持っています。これが、ServiceManagerにセットされていくわけです。
ServiceManagerのコンストラクタで、引数で受けたインスタンス、つまりServiceManagerConfigのconfigureServiceManagerというメソッドを実行していましたね。そのメソッドを見てましょう。

vendor/ZF2/library/Zend/Mvc/Service/Service/ServiceManagerConfig.php
    public function configureServiceManager(ServiceManager $serviceManager)
    {
        foreach ($this->invokables as $name => $class) {
            $serviceManager->setInvokableClass($name, $class);
        }

        foreach ($this->factories as $name => $factoryClass) {
            $serviceManager->setFactory($name, $factoryClass);
        }

        foreach ($this->abstractFactories as $factoryClass) {
            $serviceManager->addAbstractFactory($factoryClass);
        }

        foreach ($this->aliases as $name => $service) {
            $serviceManager->setAlias($name, $service);
        }

ServiceManagerにセットしているのがわかりますね。上の方のServiceManagerのコンストラクタのを見てみて下さい。configureServiceManagerの引数に自分自身、つまりServiceManager自身のインスタンスを渡しています。ServiceManagerConfigのconfigureServiceManagerメソッドではそのServiceManagerのインスタンスにマッピング配列の情報をセットしていっています。

なので、ServiceManagerConfigに定義されている配列のキーを、ServiceManagerのgetメソッドに指定することで、そのキーの右辺に定義されているクラスのインスタンスが取得できるというわけです。

実際にやってみましょう。

module/Hoge/src/Hoge/Controller/IndexController.php
class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $eventManager = $this->getServiceLocator()->get('EventManager');
        var_dump(get_class($eventManager));
    }
}
string(30) "Zend\EventManager\EventManager" 

インスタンスが返ってきました。しかしよく見ると、少し定義とは違うものが返ってきました。
上のServiceManagerConfigクラスの頭の部分を見てみて下さい。

'EventManager' => 'Zend\Mvc\Service\EventManagerFactory'

となっていますよね。上辺に定義されているのはEventManagerではなくEventManagerFactoryです。
何が起きているのか。見てみましょう。まず見るべきははServiceManagerのgetメソッドです。
長いので少しずつ見てみましょう。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
    public function get($name, $usePeeringServiceManagers = true)
    {
        // inlined code from ServiceManager::canonicalizeName for performance
        if (isset($this->canonicalNames[$name])) {
            $cName = $this->canonicalNames[$name];
        } else {
            $cName = $this->canonicalizeName($name);
        }

最初の部分では、引数で来た文字列を整形しています。canonicalizeNameメソッドです。ここでは何をやっているかというと、来た文字列に含まれる特定の記号をを全て消し、かつ全ての文字を小文字に変換しています。まあ、余りここは深く考える必要はありません。記号が入っているといろいろ邪魔なので消してる、くらいに思って下さい。

この後、少し飛ばして重要な部分へ行きます。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
    public function get($name, $usePeeringServiceManagers = true)
    {

        ~~~(中略)~~~

        if (!$instance) {
            $this->checkNestedContextStart($cName);
            if (
                isset($this->invokableClasses[$cName])
                || isset($this->factories[$cName])
                || isset($this->aliases[$cName])
                || $this->canCreateFromAbstractFactory($cName, $name)
            ) {
                $instance = $this->create(array($cName, $name));
            } elseif ($isAlias && $this->canCreateFromAbstractFactory($name, $cName)) {
                /*
                 * case of an alias leading to an abstract factory :
                 * 'my-alias' => 'my-abstract-defined-service'
                 *     $name = 'my-alias'
                 *     $cName = 'my-abstract-defined-service'
                 */
                $instance = $this->create(array($name, $cName));
            } elseif ($usePeeringServiceManagers && !$this->retrieveFromPeeringManagerFirst) {
                $instance = $this->retrieveFromPeeringManager($name);
            }
            $this->checkNestedContextStop();
        }

よく見ると自身のcreateメソッドに処理を託しているのがわかります。

$instance = $this->create(array($cName, $name));

の部分です。createメソッドを見てみましょう。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
    public function create($name)
    {
        if (is_array($name)) {
            list($cName, $rName) = $name;
        } else {
            $rName = $name;

            // inlined code from ServiceManager::canonicalizeName for performance
            if (isset($this->canonicalNames[$rName])) {
                $cName = $this->canonicalNames[$name];
            } else {
                $cName = $this->canonicalizeName($name);
            }
        }

        if (isset($this->delegators[$cName])) {
            return $this->createDelegatorFromFactory($cName, $rName);
        }

        return $this->doCreate($rName, $cName);
    }

更に、doCreateメソッドへ託しています。
doCreateを見てみましょう。

vendor/ZF2/library/Zend/ServiceManager/ServiceManager.php
    public function doCreate($rName, $cName)
    {
        $instance = null;

        if (isset($this->factories[$cName])) {
            $instance = $this->createFromFactory($cName, $rName);
        }

        ~~~(中略)~~~

        return $instance;
    }

今回取得しようとしたのは、"EventManager"の名前で登録されているオブジェクトです。
上の方のServiceManagerConfigのところを見てもらえれば分かる通り、"EventManager"は$factoriesとして定義されています。
そしてconfigureServiceManagerメソッドで、ServiceManagerのsetFactoryでその定義が注入されています。
ServiceManagerのsetFactoryでは、引数で受け取った設定を$this->factoriesにセットしています。
なので、doCreateメソッドでは、上の部分のifで引っかかり、createFromFactoryメソッドを介してインスタンスを取得しています。

$this->factoriesにセットされている設定は、

'EventManager' => 'Zend\Mvc\Service\EventManagerFactory'

で、EventManagerFactoryが返ってくるように見えるのに、EventManagerが返ってくる秘密はcreateFromFactoryメソッドにありそうです。
見てみましょう。

    protected function createFromFactory($canonicalName, $requestedName)
    {
        $factory = $this->factories[$canonicalName];
        if (is_string($factory) && class_exists($factory, true)) {
            $factory = new $factory;
            $this->factories[$canonicalName] = $factory;
        }
        if ($factory instanceof FactoryInterface) {
            $instance = $this->createServiceViaCallback(array($factory, 'createService'), $canonicalName, $requestedName);
        } elseif (is_callable($factory)) {
            $instance = $this->createServiceViaCallback($factory, $canonicalName, $requestedName);
        } else {
            throw new Exception\ServiceNotCreatedException(sprintf(
                'While attempting to create %s%s an invalid factory was registered for this instance type.',
                $canonicalName,
                ($requestedName ? '(alias: ' . $requestedName . ')' : '')
            ));
        }
        return $instance;
    }

最初の1行で、$factoryに'Zend\Mvc\Service\EventManagerFactory'という、クラス名を示す文字列が入っていることになります。
その直後、それをnewしているので、$factoryにはEventManagerFactoryのインスタンスが入ります。
その後、そのインスタンスを引数に、createServiceViaCallbackというメソッドを更に呼んでいます。
createServiceViaCallbackを見てみましょう。

    protected function createServiceViaCallback($callable, $cName, $rName)
    {

        ~~~(中略)~~~

        try {
            $circularDependencyResolver[$depKey] = true;
            $instance = call_user_func($callable, $this, $cName, $rName);
            unset($circularDependencyResolver[$depKey]);
        } catch (Exception\ServiceNotFoundException $e) {

        ~~~(中略)~~~

        return $instance;
    }

かなり端折りましたが、重要なのは

$instance = call_user_func($callable, $this, $cName, $rName);

の部分です。
$callableにはarray($factory, 'createService')が入っています。つまり、EventManagerFactoryのcreateServiceメソッドを実行しています。
EventManagerFactoryのcreateServiceを見てみましょう。

vendor/ZF2/library/Zend/Mvc/Service/Service/EventManagerFactory.php
class EventManagerFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $em = new EventManager();
        $em->setSharedManager($serviceLocator->get('SharedEventManager'));
        return $em;
    }
}

やっと答えにたどり着いたというところです。
EventManagerFactoryはEventManagerのインスタンスを生成して返す役割を持っているわけです。

そもそもFactoryというのもデザインパターンの一つです。これについてはまた別途解説しますが、要は間接的にインスタンスを作り出すクラスがFactoryです。

ServiceManagerが持つ、サービスのマッピング設定はいくつかありましたが、その中の$factoriesは、最終的に必要なインスタンスを生成するという仕組みに則ったFactoryクラスをサービスとして登録することを前提にしたものです。ServiceManagerは基本的に一度生成したサービスのインスタンスは保持しておき、同じ要求が来たら同一のインスタンスを返すようになっています。しかし、Factory自体は文字通りインスタンスを量産する機能を提供します。つまり、ServiceManagerが握っているサービスはあくまでFactoryで、Factoryを利用することにより、ServiceManagerにオブジェクトを量産する機能をもたせているような形になっているのです。