OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

SymfonyのService Containerについて(後編)

こんにちは。@mrtry_です。
最近ずっとサンダルで出社していたら、日焼けでサンダルの紐の跡ができました。

さて、今回は前回に引き続き、DIについて書こうと思います。 前回でDIとはなにか、という話をしたので、今回はSymfonyでDIする際に利用するService Containerについて書きたいと思います

以下、今回の記事の目次になります。

  • Service Contaier とは
  • DIするまでの手順
  • サービスを作る
  • サービスの設定をする
  • コントローラでサービスを呼び出してみる

Service Container とは

Service Containerとは、Symfonyで提供されているDIコンテナです。 Symfonyでは、ビジネスロジックをまとめたクラスをサービスと呼んでいます。 Service Containerは、サービスをインスタンス化するときに、必要とする定数や他のサービスのオブジェクトを注入したりと、依存関係を解決する役割を担ってくれます。 前回の記事で言うところの「インジェクタ」の役割にあたります。

今回は、実際に簡単なサービスを作ってみて、Service Containerを利用して依存する定数やサービスを注入していきたいと思います。

DIするまでの手順

実際にサービスを作成し、依存する他のサービスを注入してみたいと思います。 以下のような手順で進めていきたいと思います。

  • サービスを作る
  • サービスの設定をする
    • ローダーの作成
    • 設定ファイルの作成
    • コマンドラインからの確認

サービスを作る

はじめに、元となるビジネスロジックをまとめたクラス、サービスを作ろうと思います。

まず、サービスのソースコードを配置するディレクトリを作っておきます。

$ mkdir src/AppBundle/Service

次に、サービスクラスを作ります。

今回は、GreetingServiceというサービスを作成しました。 sayGreeting()すると、時間帯にあった挨拶を返してくれるサービスです。 このサービスには、TimeSlotInterfaceを継承したクラスと、VariousGreetingsInterfaceを継承したクラスが注入されています。 時間帯の判定をTimeSlotInterfaceを継承したクラス、時間帯に依ったあいさつをVariousGreetingsInterfaceを継承したクラスにまかせるようになっています。

ちなみに、今回の例では、DIをコンストラクタで依存オブジェクトを受け取るConstruct Injectionで行っていますが、Setter InjectionProperty Injectionも行うことができます。

src/AppBundle/Service/GreetingService.php

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;
use AppBundle\Service\TimeSlotInterface;

class GreetingService
{
    private $variousGreetings;
    private $timeSlot;

    public function __construct(TimeSlotInterface $timeslot, VariousGreetingsInterface $variousGreetings)
    {
        $this->timeSlot = $timeSlot;
        $this->variousGreetings = $variousGreetings;
    }

    public function sayGreeting()
    {
        $greeting = '';

        switch ($this->timeSlot->getCurrentTimeSlot()) {
            case TimeSlotInterface::MORNING:
                $greeting = $this->variousGreetings->sayMorningGreeting();
                break;

            case TimeSlotInterface::NOON:
                $greeting = $this->variousGreetings->sayNoonGreeting();
                break;

            case TimeSlotInterface::NIGHT:
                $greeting = $this->variousGreetings->sayNightGreeting();
                break;
        }

        return $greeting;
    }
}

続いて、注入しているクラスのインターフェースと実際のクラスを見ていきます。

src/AppBundle/Service/TimeSlotInterface.php

TimeSlotInterfaceは以下のようなコードになっています。 時間帯を表す定数と、現在の時間帯を表す値を返すメソッドgetCurrentTimeSlot()が定義されています。

<?php

namespace AppBundle\Service;

interface TimeSlotInterface
{
    const MORNING = 0;
    const NOON = 1;
    const NIGHT = 2;

    /**
     * 現在の時間帯を表す値を返す
     *
     * @return int
     */
    public function getCurrentTimeSlot();
}

src/AppBundle/Service/TimeSlotService.php

TimeSlotInterfaceを継承し、TimeSlotServiceを作成しました。 GreetingService に注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\TimeSlotInterface;

class TimeSlotService implements TimeSlotInterface
{
    public function getCurrentTimeSlot()
    {
        $now = new \DateTime('now');

        $morningOfTheDay = clone $now;
        $morningOfTheDay->setTime(6, 0);

        $noonOfTheDay = clone $now;
        $noonOfTheDay->setTime(11, 0);

        $nightOfTheDay = clone $now;
        $nightOfTheDay->setTime(18, 0);

        if ($morningOfTheDay <= $now && $now < $noonOfTheDay) {
            return TimeSlotInterface::MORNING;
        } else if ($noonOfTheDay <= $now && $now < $nightOfTheDay) {
            return TimeSlotInterface::NOON;
        } else {
            return TimeSlotInterface::NIGHT;
        }
    }
}

src/AppBundle/Service/VariousGreetingsInterface.php

VariousGreetingsInterfaceは以下のようなコードになっています。 朝昼夜、それぞれにあたるあいさつを返すメソッドが定義されています。

<?php

namespace AppBundle\Service;

interface VariousGreetingsInterface
{
    /**
     * 朝のあいさつを返す
     *
     * @return string
     */
    public function sayMorningGreeting();

    /**
     * 昼のあいさつを返す
     *
     * @return string
     */
    public function sayNoonGreeting();

    /**
     * 夜のあいさつを返す
     *
     * @return string
     */
    public function sayNightGreeting();
}

src/AppBundle/Service/JapaneseGreetingService.php

VariousGreetingsInterfaceを継承し、JapaneseGreetingServiceを作成しました。 GreetingServiceに注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;

class JapaneseGreetingService implements VariousGreetingsInterface
{
    public function sayMorningGreeting()
    {
        return 'おはようございます';
    }

    public function sayNoonGreeting()
    {
        return 'こんにちわ';
    }

    public function sayNightGreeting()
    {
        return 'こんばんわ';
    }
}

DIすることで得られた利点

このように実装したことで、以下の利点があります。

結合度の低下

依存するクラスをInterfaceで実装を統一しているため、結合度が低くなっています。 今回の例で言うと、多言語対応が必要となった時に嬉しいです。 VariousGreetingsInterfaceを継承したクラスを新しく作成し、GreetingServiceに注入すれば、それだけで別言語でのあいさつを返すサービスを実装することができます。 英語のあいさつを返すサービスにしたければ、VariousGreetingsInterfaceを継承したEnglishGreetingServiceを作成し、注入するだけで対応できます。

※ なお、今回の実装での多言語化はDIの例で挙げているだけで、通常は以下のような手法を使います。

テストのしやすさ

スタブやモックを用いたテストが状態になっています。 今回の例で言うと、時間帯ごとに意図したあいさつが返ってくるかを確認したい時に嬉しいです。 TimeSlotInterfaceを継承したクラスを作成し、getCurrentTimeSlot()で確認したい時間帯の定数を返すことで、時間帯ごとに意図したあいさつが返ってくるかを確認することができます。

サービスの設定をする

次に、サービスに注入する依存オブジェクトを定義したりする設定ファイルを作成していきます。

この記事では、サービスの設定ファイルをyamlで作成します。 作成した設定ファイルの配置場所として、以下の2箇所があります。

  • app/config
    • composerなどで導入したBundleをService Containerに登録したいときはここに置く
  • Bundle内のResouces/config
    • 自身で定義したサービスをService Containerに登録するときは、ここに置く

今回は、自身で定義したサービスをService Containerに登録したいので、Bundle内のResouces/configに配置したいと思います。

ただ、$ app/console generate:bundleでBundleを生成した際は、 設定ファイルを配置するディレクトリや、設定ファイルを読み込むためのローダーが準備されているのですが、プロジェクトを立ち上げたときに生成されるAppBundleでは準備がされていません。 ローダーの準備と設定ファイルの配置場所の作成、設定ファイルの作成をしていきます。

ローダーの作成

まず、ローダーのソースコードを配置するディレクトリを作成します。

$ mkdir src/AppBundle/DependencyInjection

その後、以下のようにローダーを作成します。

src/AppBundle/DependencyInjection/AppExtension.php

<?php

namespace AppBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yml');
    }

    public function getAlias()
    {
        return 'app';
    }
}

継承しているExtensionというクラスは、サービスコンテナに対して、何かしらの拡張を行うためのクラスです。 YAMLなどの設定ファイルからサービスの定義を読み取ったり、設定に基づいてサービスの構成を行ったりします。

今回は、AppBundle/Resources/configに配置した設定ファイルを読み込むためにYamlFileLoaderを用意し、services.ymlを読み込む処理をしています。

設定ファイルの作成

先程Resources/configに配置したservices.ymlを読み込むようにローダーの準備をしたので、ディレクトリを作成し、設定を配置します。

$ mkdir -p src/AppBundle/Resources/config

src/AppBundle/Resources/config/services.yml

services:
    # サービスID
    app.japanese_greeting_service:
        # 実際のクラス
        class: AppBundle\Service\JapaneseGreetingService

    app.timeslot_service:
        class: AppBundle\Service\TimeSlotService

    app.greeting_service:
        class: AppBundle\Service\GreetingService
        # 注入するサービスIDを渡す (サービスIDの頭には`@`をつける)    
        arguments:
            - @app.timeslot_service
            - @app.japanese_greeting_service

servicesのキー以下では、サービスの定義をすることができます。 任意の名前のサービスIDを設定し、実際のクラス名指定することで、フレームワーク側にサービスを登録することができます。 また、依存する定数や他のクラスを注入することもできます。

今回の例だと、以下のように定義しています。

クラス名 サービスID 注入したもの
JapaneseGreetingService japanese_greeting_service なし
TimeSlotService timeslot_service なし
GreetingService greeting_service japanese_greeting_service, timeslot_service

コマンドラインからの確認

一通りの設定ができたら、$ php app/console debug:containerを叩いてサービスがSymfony側に登録されたことを確認します。 きちんと設定ができていれば、以下のようにサービスIDを確認することができます。

$ app/console debug:container | grep app
  app.greeting_service                                                 AppBundle\Service\GreetingService
  app.japanese_service                                                 AppBundle\Service\Japanese
  app.timeslot_service                                                 AppBundle\Service\TimeSlotService
  doctrine.orm.default_entity_listener_resolver                        Doctrine\ORM\Mapping\DefaultEntityListenerResolver

これで、サービスを利用するまでの準備は一通り終わりました。

コントローラでサービスを呼び出してみる

では、定義したサービスをコントローラで利用してみます。 サービスを利用する際は、$this->get(サービスID)を実行するとサービスのインスタンスが取得できます。

今回の例では、MessageServiceを取得し、getMessage()した内容をJsonで返すコントローラを書きました。

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $greetingService = $this->get('app.greeting_service');
        $message = [
            'message' => $greetingService->sayGreeting(),
        ];

        return new JsonResponse($message);
    }
}

app/console server:startをし、http://127.0.0.1:8000/へアクセスしてみると、時間帯に合ったあいさつが表示されていることを確認できたかと思います。

おわりに

今回は、SymfonyのService Containerを利用してDIする例を紹介しました。 Service Containerは、実際に開発でも高頻度で触る箇所なので、きちんと理解しておきたい所ですね!!

さて、今回をもちまして、「Symfony2入門」の投稿は、一旦終えたいと思います。 いろいろ紹介していない内容は多々あるのですが、全部紹介していたら永遠と続くので今回を区切りにしたいと思います。笑 (最近、私の開発担当がモバイルアプリになって、Symfonyを全然さわっていないというのも、あります。)

今まで書いた記事が、私と同じような初心者の方々の参考になったなら嬉しく思います。 ありがとうございました!

参考