こんにちは。@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 Injection
やProperty 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を全然さわっていないというのも、あります。)
今まで書いた記事が、私と同じような初心者の方々の参考になったなら嬉しく思います。 ありがとうございました!