OTOBANK Engineering Blog

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

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

こんにちは。@mrtry_です。 最近、低温調理機を自作しまして、毎週ハナマサで肉塊を買って、調理する週末を過ごしています。

さて、今回から2回ほどService Containerについて紹介したいと思います。 初回のこの記事では、Service Containerの仕組みの元となるデザインパターンDependency injection(DI)について紹介したいと思います。

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

  • Dependency injection(DI)とは
  • 実際のコード例
  • DIすると嬉しいこと
  • SymfonyでDIを行うにはSymfonyでDIを行うには

Dependency injection(DI)とは

Dependency injection(DI)とは、デザインパターンの一種です。 よく「依存性の注入」とよく言われています。 和訳だけ見てもどんなものなのか想像しにくいですね…。

英語のwikipediaを見てみると、以下のように説明されています。

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object.A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state.[1] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern. Dependency injection - Wikipedia

意訳すると、だいたい以下のようなことが書いてあります。

  • Dependency injectionは、オブジェクト間の依存関係を解決するデザインパターンのひとつ
  • 「Dependency」は、利用しているオブジェクト(サービス)とのことを指す
  • 「Iinjection」は、「クライアント」に「依存オブジェクト(サービス)」を渡すこと
  • 「依存するオブジェクトをクライアントに渡す」という振る舞いが、DIパターンの基本

また、以下のような説明もあります。

As with other forms of inversion of control, dependency injection supports the dependency inversion principle. The client delegates the responsibility of providing its dependencies to external code (the injector). The client is not allowed to call the injector code.[2] It is the injecting code that constructs the services and calls the client to inject them. This means the client code does not need to know about the injecting code. The client does not need to know how to construct the services. The client does not need to know which actual services it is using. The client only needs to know about the intrinsic interfaces of the services because these define how the client may use the services. This separates the responsibilities of use and construction. Dependency injection - Wikipedia

意訳すると、だいたい以下のようなことが書いてあります。

  • DIには「依存性反転の原理」がある
  • クライアントは、自身に注入される依存オブジェクトの準備を全て「インジェクタ」に任せる
  • クライアントは、依存オブジェクトの生成方法、そのオブジェクトがどんなものであるか知る必要はない
  • クライアントは、注入される依存オブジェクトのInterfaceだけわかれば良い
  • こうすることで、「依存オブジェクトを準備する責任」と「依存オブジェクトを使用する責任」に分けることができる

というわけで、DIをまとめると以下のようになります。

  • 「依存するオブジェクトを外から渡してあげる」という「デザインパターン」のこと
  • 要素として、クライアントとインジェクタというものがある
    • クライアント
      • 依存するオブジェクトを外から受け取り、そのオブジェクトを使用する責任を持つ
      • 渡されるオブジェクトのInterfaceだけ知っている
    • インジェクタ
      • 依存オブジェクトを用意する責任を持つ
      • クライアントが知っているInterfaceを持つクラスを用意する

というものだとわかりました。

実際のコード例

実際のコードでDIしたもの、していないもので比較してみます。 掲載されているソースコードは、やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or somethingを参考にしています。

ClientServiceというクラスを作成します。 また、ServiceClientの依存オブジェクトとします。

DIしていないコード

Clientのconstruct時に、Serviceもnewし、内部で保持するような実装になっています。

<?php

class Client
{
    private $service;

    public function __construct()
    {
        $this->service = new Service();
    }

    public function doSomething()
    {
        $this->service->doSomething();
    }

    ...
}

class Service
{
    public function doSomething()
    {
        ...
    }

    ...
}

DI しているコード

Clientのconstruct時に、$serviceを外部から受け取るようになっています。 $serviceは、既に準備されたものを受け取るようになっています (この$serviceのオブジェクトの準備は、何かしらで実装されたインジェクタが用意してくれます)。 Clientがどんな$serviceを受け取るかはServiceInterfaceで決定しています。

<?php

class Client
{
    private $service;

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

    public function doSomething()
    {
        $this->service->doSomething();
    }

    ...
}

class Service implements ServiceInterface
{
    public function doSomething()
    {
        ...
    }

    ...
}

DIすると嬉しいこと

先程のコード例を見比べて、DIした時にどんな嬉しいことがあるのでしょう?

DIしていないと困ること

  • 結合度が高い
    • 依存オブジェクトの置換や更新をする時、依存オブジェクトを利用しているクラスのソースコードを修正する必要がある
    • 例: ServiceクラスのAPIに変更があったとき、Clientクラスも合わせて修正しなければならない
  • テストがしにくい
    • 依存オブジェクトを直接参照しているので、テスト時にスタブやモックに置換することができない
    • 例: Serviceが本番DBを扱っているオブジェクトとする。Clientのテストをするとき、Serviceに依存しているためモックなどに差し替えができず、DBにテスト用のデータを準備したりする必要がある

DIすると嬉しいこと

  • 結合度の低下
    • 依存オブジェクトを利用しているクラスのソースコードに修正を加えず、依存オブジェクトの置換や更新を行える
    • 例: ServiceクラスのAPIに変更があったとしても、Interfaceで実装を統一しているので、変更する必要はない
  • テストがしやすくなる
    • スタブやモックを準備することで、そのクラスを単体テストすることができる
    • 例: Clientのテストをするとき、Serviceと同様のInterfaceを持つオブジェクトを作成すれば、テスト時は開発サーバに接続したりすることができる

SymfonyでDIを行うには

DIの仕組みを提供するフレームワークのことを、DIコンテナと言います。 Symfonyには、Service ContainerというDIコンテナが初めから導入されて、とても簡単にDIをすることができます。

実際の使い方に関しては、次回説明したいと思います。

おわり

今回は、Dependency injectionについて紹介しました。 次回は、Service Containerの利用方法について紹介したいと思います。

また、この記事は、@mrtry_の勉強の一環で書いていますので、 お気づきの点などがありましたら、コメント等でご指摘いただければ幸いです!

参考

Symfony2で利用されているDoctrineに入門する(後編)

こんにちは!@mrtryです。
最近、つくりおきした鍋を冷蔵庫に入れ忘れて、おかずを腐らせる失態を2度ほど犯しています…。
みなさん…。最近暖かくなってきていますので、食中毒には気を付けましょう…。

さて、「Symfony2入門」の8回目の記事です。
前回のSymfony2で利用されているDoctrineに入門する(中編)に引き続き、今日は後編をお送りします。

前回は、DoctrineとDBを連携するための設定 エンティティの作成 エンティティを元したテーブル作成 を紹介しました。
今回は、 Doctrineを介したCRUD操作 について紹介したいと思います。

Create

Createする際の手順は以下のようになります。

  1. EntityManagerを準備する
  2. エンティティを新しく生成する
  3. EntityManagerを介して、生成したエンティティをDoctrineの管理下におく
  4. DBに永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function createAction()
{
    // EntityManagerを準備する
    $em = $this->getDoctrine()->getManager();

    // 新しいエンティティを作成し、プロパティを設定する
    $product = new Product();
    $product->setName('Apple');
    $product->setPrice(100);
    $product->setDescription('From Aomori');

    // $productを永続化するエンティティとして管理する
    $em->persist($product);
    // DBへ永続化する
    $em->flush();

    ...
    
}

順に見ていきましょう。

    // EntityManagerを取得する  
    $em = $this->getDoctrine()->getManager();

EntityManagerというものが出てきました。 EntityManagerとは、エンティティの情報をDBへ永続化してくれるものです。 エンティティの情報をDBへ永続化する際は、このEntityManegerを介して行うことになります。

なお、今回はRDBとしてmysqlを用いているので、EntityManagerが返ってきますが、 MongoDBなどのドキュメント指向データベースを用いた際は、DocumentManagerが返ってきます。

    // 新しいエンティティを作成し、プロパティを設定する  
    $product = new Product();  
    $product->setName('Apple');  
    $product->setPrice(100);  
    $product->setDescription('From Aomori');  

前回生成したProductエンティティを新しく生成し、各セッターを用いてプロパティを設定しています。

    // $productを永続化するエンティティとして管理する  
    $em->persist($product);  
    // DBへ永続化する  
    $em->flush();

EntityManegerを介して、DBにエンティティの内容を反映します。 まず、

    $em->persist($product);

で、引数で渡されたエンティティを永続化するエンティティとしてDoctrineで管理します。 persistという名前を見て「これで永続化されるのかな?」と勘違いしてしまうかもしれませんが、 あくまで管理しているだけで、まだDBへの永続化処理は行われていません。

その後、

    $em->flush();

を行うと、それまでにpersist()されていたエンティティがDBへ永続化されます。

Read

Readする際の手順は、以下のようになります。

  1. 読み取りたいエンティティのリポジトリを準備する
  2. リポジトリのfind系メソッドを用いて、エンティティを取得する

以下は、実際のコードの例です。

public function readAction()
{
    // ProductRepositoryを取得
    $productRepository = $this->getDoctrine()->getRepository('AppBundle:Product');
  
    // productテーブルにあるカラム`name`について、`Apple`という文字列に一致するエンティティを取得する
    $product = $productRepository->findByName('Apple');
      
    ...
    
}

順に見ていきましょう。

    // ProductRepositoryを取得  
    $productRepository = $this->getDoctrine()->getRepository('AppBundle:Product');

今度は、ProductRepositoryというものが出てきました。 このリポジトリとは、DBのテーブルをオブジェクトとしたものです。 エンティティの永続化はEntityManagerを介して行いますが、 エンティティを取得するときは、このリポジトリを介して取得します。

    // productテーブルにあるカラム`name`について、`Apple`という文字列に一致するエンティティを取得する  
    $product = $productRepository->findByName('Apple');

$productRepositoryfindByName()を用いて、 productテーブルにあるカラムnameについて、Appleという文字列に一致するエンティティを取得します。

このように、エンティティを取得するは、Repositoryに実装されているfindXXXと名前についたメソッドを用いて取得します。

以下で、その他のfind系メソッドの例を紹介します

参考:Fetching Objects from the Database

// 主キーで検索し、一致するエンティティを取得する (普通はID)
$product = $repository->find($productId);

// 任意のカラム名に基づき、任意の値で検索し、該当するうちの最初のエンティティを取得
$product = $repository->findOneById($productId);
$product = $repository->findOneByName('Keyboard');

// 任意のカラム名に基づき、任意の値を検索し、一致するエンティティをすべて取得する
$products = $repository->findByPrice(19.99);

// すべてのエンティティを取得
$products = $repository->findAll();

Update

Updateする際の手順は、以下のようになります。

  1. リポジトリを介して、更新対象とするエンティティを取得する
  2. 取得したエンティティのプロパティを更新する
  3. DBへ永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function updateAction()
{
    // EntityManagerを取得する
    $em = $this->getDoctrine()->getManager();

    $productId = XXX;

    // productテーブルからIDを指定してエンティティを取得する
    $product = $this->getDoctrine()
      ->getRepository('AppBundle:Product')
      ->find($productId);

    // priceに`100`をセットする
    $product->setPrice(100);

    // DBへ永続化する
    $em->flush();

    ...
    
}

CreateとReadで紹介したメソッドの組み合わせで実現できますが、1箇所だけ違うところがあります。 DBへの永続化の過程で、persist()が省略されています。 persist()はDoctrine側でこのエンティティを管理するよという意味で実行するものでした。 ですが、上記のコードのように、Repositoryを介して取得したエンティティについては、既にDoctrine側の管理対象となっているので、書かなくても良いです。

Notice that calling $em->persist($product) isn’t necessary. Recall that this method simply tells Doctrine to manage or “watch” the $product object. In this case, since you fetched the $product object from Doctrine, it’s already managed.

Databases and the Doctrine ORM (2.7) | updating-an-object

Delete

Updateする際の手順は、以下のようになります。

  1. リポジトリを介して削除対象とするエンティティを取得する。
  2. EntityManager->remove()を行い、削除するエンティティとして管理する
  3. DBへ永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function deleteAction()
{
    // EntityManagerを取得する
    $em = $this->getDoctrine()->getManager();

    // productテーブルからIDを指定してエンティティを取得する
    $product = $this->getDoctrine()
      ->getRepository('AppBundle:Product')
      ->find($productId);

    //$productを削除するエンティティとして管理する
    $em->remove($product);
    // DBへ永続化する
    $em->flush();
}

エンティティを削除する際は、EntityManagerで用意されているremove()というメソッドを利用します。 remove()の引数となるエンティティは、実行したタイミングでDoctrineで管理されるので、flush()を行うと、DBへ永続化されます。

Doctrine Query Language(DQL)

複数のテーブルを組み合わせたクエリを発行する時、上記の手法だとなかなか難しいこともあります。 今回は詳しく紹介しませんが、DoctrineにはObject Query Language(OQL)として、Doctrine Query Language(DQL)というものも実装されています。

おわりに

今回は、DoctrineでCRUDをする例を紹介しました。
次回は、サービスコンテナについて紹介したいと思います。

また、この記事は、@mrtryの勉強の一環で書いていますので、 お気づきの点などがありましたら、コメント等でご指摘いただければ幸いです!

参考文献

Industry Tech Kaigi (4/27) の仲間に入れてもらってイベントやります!

@riaf です。

4/27 (木) に開催される#02 Industry Tech Kaigi「巨大産業をテクノロジーでハックせよ!」 というイベントにオトバンクも一緒に登壇することになりました。

公開時にちょっと面白いことになっていたので、シェアして遊んだりしていましたが、

現在はちゃんと修正されている通り、各社の開発部門の責任者が集まるパネルディスカッションをやるはずです。

詳細はイベントページをご覧いただければと思いますが、

industry-tech-kaigi.connpass.com

普段は共通点がなさすぎて集まることのないメンバーが集まりますので、面白くなるのかどうかも検討つきませんが、いつもとは違うお話ができると良いなあなどと思っております。

「こんな話が聞きたい!」といったリクエストは @frkout まで送ってください!