はじめに

BEAR.Sunday Advent Calendar 2019 7日目の記事です。

Ray.Di を実務に使い始めてそろそろ1年半になります。
今回はもともとSymfony2でyamlにDI設定を書くのに慣れていた私が、Ray.Diを使うときに使っている技の一部をご紹介したいと思います。

小技集

パラメータ(stringやint)の注入

Symfonyでは % で囲むことでサービスにパラメータを注入できます。

# services.yml
parameters:
  app_my_param: 123

services:
  App\Service\SleighService:
    class: App\Service\SleighService
    arguments:
      - '%app_my_param%'

bind()->annotatedWith('app_my_param')->toInstance(123) のように、 annotatedWith() を使って名前を指定することで、文字列や数値も注入できます。 annotatedWith で指定した名前は注入したい先のクラスに toConstructor() したり Provider@Named で注入したりして使います。

<?php

class MyModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind()->annotatedWith('app_my_param')->toInstance(123);
        $this->bind(SleighService::class)->toConstructor(SleighService::class, [
            'myArg' => 'app_my_param',
        ]);
    }
}

factory

とあるクラスをFactoryクラスを使って生成しなければならないとき、Symfonyならこう書きますね。

# services.yaml
parameters:
  app_my_param: 123

services:
  App\Service\SleighServiceFactory:
    class: SleighServiceFactory
  App\Service\SleighService:
    class: App\Service\SleighService
    factory: ['@App\Service\SleighServiceFactory', 'create']
    arguments:
      - '%app_my_param%'


RayにはFactoryを指定するメソッドはありません。
注入先クラスにFactoryそのものを注入するか、

<?php

class ReindeerService
{
    public function __construct(SleighServiceFactory $sleighServiceFactory)
    {
        $this->sleighServiceFactory = $sleighServiceFactory;
    }

    public function execute()
    {
        // ...

        $sleighService = $sleighServiceFactory->create($myArg);

        // ...
    }
}

Provider を定義してその中でFactoryを使うようにします。

<?php

class MyModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind()->annotatedWith('app_my_param')->toInstance(123);
        $this->bind(SleighService::class)->toProvider(SleighServiceProvider::class);
    }
}

class SleighServiceProvider implements ProviderInterface
{
    private $myArg;

    /**
     * @Named("myArg=app_my_param")
     */
    public function __construct(int $myArg)
    {
        $this->myArg = $myArg;
    }

    public function get()
    {
        return new SleighService($this->myArg);
    }
}

calls

インスタンスを作った後になにかのメソッドコールが必要な場合、Symfonyでは下記のように書くことができます。

# services.yml

services:
  App\Service\SleighService:
    class: App\Service\SleighService
    calls:
      - ['setReindeer', ['@App\Service\Reindeer']]

Rayにはcallsはありません。
Factoryクラスを用意して、Factoryクラスを注入します。(1個前の例参照)
あるいは、DIの領域内で解決したいときはProviderを作ってその中でcallしたものを渡すといいかもしれません。

<?php

class SleighProvider implements ProviderInterface
{
    private $reindeer;

    /**
     * @Named("reindeer=app_service_reindeer")
     */
    public function __construct(Reindeer $reindeer)
    {
        $this->reindeer = $reindeer;
    }

    public function get()
    {
        $sleighService = new SleighService();
        $sleighService->setReindeer($this->reindeer);

        return $sleighService;
    }
}

同じ具象クラスのコンストラクタ引数だけ変えた複数のオブジェクトを使いたい

# services.yml

services:
  brown_nose_reindeer:
    class: App\Reindeer
    arguments:
      - "brown"
  red_nose_reindeer:
    class: App\Reindeer
    arguments:
      - "red"


  App\Service\SleighService:
    class: App\Service\SleighService
    calls:
      - ['addReindeer', ['@brown_nose_reindeer']]
      - ['addReindeer', ['@red_nose_reindeer']]

Rayでも annotatedWith() を使ってそれぞれに別名を付けて扱うことができます。

<?php

class MyModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(Reindeer::class)->annotatedWith('brown_nose_reindeer')->toConstructor(Reindeer::class, [
            'nose_color' => 'nose_color_brown',
        ]);
        $this->bind(Reindeer::class)->annotatedWith('red_nose_reindeer')->toConstructor(Reindeer::class, [
            'nose_color' => 'nose_color_red',
        ]);
    }
}

あるいは、 abstract classの邪悪じゃない使い方 に変えることもできます。

<?php

class RedNoseReindeer extends AbstractReindeer
{
    public function __construct()
    {
        parent::__construct('red');
    }
}

class RedNoseReindeer extends AbstractReindeer
{
    public function __construct()
    {
        parent::__construct('red');
    }
}

class MyModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(BrownNoseReindeer::class)->annotatedWith('brown_nose_reindeer')->to(BrownNoseReindeer::class);
        $this->bind(RedNoseReindeer::class)->annotatedWith('red_nose_reindeer')->to(RedNoseReindeer::class);
    }
}

パッケージを分けて開発していて、DI設定をマージしたい

SymfonyではyamlやxmlでDI設定を書いておいて、アプリケーションから読み込む仕組みがありますね。
Rayでは各パッケージごとに定義したModuleを、メインのアプリケーションのModuleをinstallすることで、DIを再利用することができます。

<?php

class ReindeerModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(BrownNoseReindeer::class)->annotatedWith('brown_nose_reindeer')->to(BrownNoseReindeer::class);
        $this->bind(RedNoseReindeer::class)->annotatedWith('red_nose_reindeer')->to(RedNoseReindeer::class);
    }
}

class AppModule extends AbstractModule
{
    protected function configure()
    {
        $this->install(new ReindeerModule());

        $this->bind(SleighService::class)->toProvider(SleighProvider::class);
        // SleighProviderの中でbrown_nose_reindeerやred_nose_reindeerを参照できる
    }
}

まとめ

SymfonyのDIは強力ですが、コマンドラインで動かすちょっとしたツール的なライブラリで使うにはちょっとオーバースペックかなと思っています。 :sweat_drops:
そういうときにはサクッと便利に使えるRay.Diがおすすめです。