Symfonyでサービスロケーター



次のような3つのサービスクラスがあります。

```php
<?php

namespace App\Services\SomeServices;

use stdClass;

class FetchService
{
    public function fetch(int $id): stdClass
    {
        $data = new stdClass();
        $data->id = $id;
        return $data;
    }
}

```


```php
<?php

namespace App\Services\SomeServices;

use Psr\Log\LoggerInterface;
use stdClass;

class DeleteService
{
    public function __construct(protected LoggerInterface $logger)
    {}

    public function delete(stdClass $class)
    {
        $this->logger->info('delete');
    }
}

```

```php
<?php

namespace App\Services\SomeServices;

use Psr\Log\LoggerInterface;
use stdClass;

class UpdateService
{
    public function __construct(protected LoggerInterface $logger)
    {}

    public function update(stdClass $class)
    {
        $this->logger->info('delete');
    }
}
```

そして、これらのサービスを利用するこのようなサービスがあります。

```php
<?php

namespace App\Services;

use App\Services\SomeServices\DeleteService;
use App\Services\SomeServices\FetchService;
use App\Services\SomeServices\UpdateService;

class ServiceManager
{
    public function __construct(
        protected UpdateService $updateService,
        protected DeleteService $deleteService,
        protected FetchService $fetchService
    ) {}

    public function update(int $id)
    {
        $data = $this->fetchService->fetch($id);
        $this->updateService->update($data);
    }

    public function delete(int $id)
    {
        $data = $this->fetchService->fetch($id);
        $this->deleteService->delete($data);
    }
}

```

SymfonyのDIコンテナにより、コンストラクタの各サービスは自動的に注入されます。しかし、よくみると`update`メソッドでは`UpdateService`と`FetchService`しか使っておらず、同じく`delete`メソッドでは`DeleteService`と`FetchService`しか使っていません。
利用しないのに、コンストラクタへ渡すためだけにそのサービスがインスタンス化されるのはメモリの無駄ですね。そんな時に使えるのがサービスロケーターです。


## サービスロケーターを使う

サービスロケーターとは、簡単にいうと「サービスの取得を抽象化する」ものです。このサービスロケーターがサービスの場所や生成方法を管理しているので、必要な時に必要なサービスだけを取得することができます。

サービスロケーターを使うパターンに書き直すと、このようになります。

```php
<?php

namespace App\Services;

use App\Services\SomeServices\DeleteService;
use App\Services\SomeServices\FetchService;
use App\Services\SomeServices\UpdateService;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

class ServiceManagerWithLocator implements ServiceSubscriberInterface
{
    public function __construct(
        protected ContainerInterface $locator
    ) {}

    public static function getSubscribedServices(): array
    {
        return [
            DeleteService::class,
            FetchService::class,
            UpdateService::class,
        ];
    }

    public function update(int $id)
    {
        $data = $this->locator->get(FetchService::class)->fetch($id);
        $this->locator->get(UpdateService::class)->update($data);
    }

    public function delete(int $id)
    {
        $data = $this->locator->get(FetchService::class)->fetch($id);
        $this->locator->get(DeleteService::class)->delete($data);
    }
}

```

コンストラクタで渡されている`$locator`がサービスロケーターです。
サービスロケーターを使うクラスは、`ServiceSubscriberInterface`を実装し、`getSubscribedServices()`でロケーターに管理してほしいサービスのクラスを返します。
サービスを取得する時は`$locator->get(FetchService::class)`のように、サービスロケーターの`get()`メソッドに対象のクラスを指定します。ここで`get()`できるサービスは、`getSubscribedServices()`で指定したものに限られます。

`$locator->get(FetchService::class)`メソッドが呼ばれた時、`FetchService`が既に一度でも生成されていればそのインスタンスを返し、まだ未生成の場合は、**初めてget()が呼ばれたタイミングでインスタンス化が行われます**。なので、サービスクラスの生成が必要な時のみ行われることになるため、メモリの節約に繋がります。