こんにちは。下田です。
Symfonyで開発していると必ず触ることになる、services.ymlについて。
最近ですが、このservices.ymlに記述するだけで実現できてしまう機能が意外とたくさんあることを知りました。
今回はそれらの機能について、どんなものがあるのか調べてみました。
(以下、Symfony4.2での記述です)
services.ymlでできること
インジェクションいろいろ
まずは基本、 あるオブジェクトを他のオブジェクトに注入したい時に書く方法です。
コンストラクタインジェクション
基本形の arguments
です。
コンストラクタ引数の変数名をkey、実際に渡すオブジェクトをvalueに書きます。
services:
App\Service\MessageGenerator:
arguments:
$logger: '@monolog.logger.request'
セッターインジェクション
コンストラクタではなく、setterメソッドを使ってオブジェクトを注入する時のyamlの書き方です。
calls
を使うと任意のsetterを呼ぶことができます。
(※実際、setterではなくとも任意の関数をcallできるようです。用途がぱっと思いつきませんが)
services:
App\Service\MessageGenerator:
calls:
- method: setLogger
arguments:
- '@logger'
省略形で以下のようにも書くことができます。
services:
App\Service\MessageGenerator:
calls:
- [setLogger, ['@logger']]
参考:https://symfony.com/doc/current/service_container/calls.html
また引数に@?
を使うと、 注入したいオブジェクトがcontainerにない場合は無視する という書き方もできます。
services:
App\Service\MessageGenerator:
calls:
- [setLogger, ['@?logger']]
上記の例では、 @logger
のインスタンスが存在しない場合はそもそも setLogger()
がcallされません。
(@?
でオプショナルにできる機能はついさっき先輩に教えてもらいました。多謝)
タグによる自動注入
symfonyの開発を進めていると tags: ['twig.extension']
とか tags: [{name: form.type }]
などなど、tags:
を書くことがあるかと思います。
あれは特定タグのつけられたオブジェクトたちを、まとめて別のオブジェクトに注入する働きがあります。
(例えば、tags:['form.type']
を付けられたものは、ここらへんでまとめて注入処理をしているようです)
参考:https://symfony.com/doc/current/service_container/tags.html
また、symfonyに最初から用意されているタグ以外にも、オリジナルのタグを作って好きなオブジェクトにまとめて注入することもできます!
services:
app_sample.foo:
class: App\Sample\SampleFoo
app_sample.bar:
class: App\Sample\SampleBar
# ...
App\SampleResolver:
class: App\SampleResolver
calls:
- ["addSample", ["@app_sample.foo"]]
- ["addSample", ["@app_sample.bar"]]
- ["addSample", ["@app_sample.baz"]]
- ["addSample", ["@app_sample.foo2"]]
- ["addSample", ["@app_sample.foo3"]]
# ...
- ["addSample", ["@app_sample.foo100"]]
これが
# config/services.yaml
services:
app_sample.foo:
class: App\Sample\SampleFoo
tags:
- "myapp.sample"
app_sample.bar:
class: App\Sample\SampleBar
tags:
- "myapp.sample"
# ...
App\SampleResolver:
class: App\SampleResolver
# 大量のcallsが減ってスッキリ!
こう書けます。
具体的な方法は弊社記事に詳しい説明があります。
前述のcalls:
をたくさん書かなければいけない場面に遭遇したときなどにオススメです
遅延読み込み
実際にはめったに使われないのに、他のインスタンスの初期化に必要なため毎回インスタンス化しなければならず、しかもインスタンス化すると重くて困るようなオブジェクトやサービス(例えば、処理失敗の時だけ必要なloggerや、外部APIクライアント等)に遭遇したことはありませんか?
そういったオブジェクトには lazy: true
を付けることで、そのオブジェクトが実際に必要になるまでインスタンス化を遅らせることができます。
依存関係が複雑になってくるとインスタンスの生成順序なども問題になってきますが、そんな時でもlazy:
オプションで実際のインスタンス生成を後回しにすることで解決できるかも知れません。
参考:https://symfony.com/doc/current/service_container/lazy_services.html
Factoryパターン
あるクラスを直接インスタンス化せず、ファクトリを利用してオブジェクトを生成したいときがあります。(同種のインスタンスがたくさん必要なときや、インスタンス生成手順に複雑な処理が必要なときなど)
そんなときはファクトリクラスを作ると思うのですが、services.ymlでもファクトリクラス名とメソッド名を記述することで、ファクトリを使ったインスタンス生成を利用できます。
factory:
を使います。
例えばこんな感じでファクトリクラスを作った場合、
class NewsletterManagerStaticFactory
{
public static function createNewsletterManager()
{
$newsletterManager = new NewsletterManager();
// ...
return $newsletterManager;
}
}
services.ymlの記述はこんな形になります。
keyに生成したいクラス名を書き、生成に使うファクトリクラス名とメソッド名を factory:
に記述します。
services:
App\Email\NewsletterManager:
factory: 'App\Email\NewsletterManagerFactory:createNewsletterManager'
# 配列でも書けるが、古い書き方
# factory: ['App\Email\NewsletterManagerStaticFactory', createNewsletterManager]
上記はファクトリメソッドがstaticの場合ですが、そうでない場合でもファクトリクラス自体を先にインスタンス化しておけば普通に使えます。
services:
# 先にファクトリをインスタンス化して、サービス登録しておく
App\Email\NewsletterManagerFactory: ~
App\Email\NewsletterManager:
factory: 'App\Email\NewsletterManagerFactory:createNewsletterManager'
参考:https://symfony.com/doc/current/service_container/factories.html
まとめ
以上、個人的に便利でスゴい!と思ったものや、実際に使っているものを羅列してみました。
services.ymlの記述を工夫するだけで実現できてしまうことが思いの外多くて驚きでした。
また今回、それらの機能について調べることで、背後にある有用な設計パターンについてもたくさん知ることができました。
設計の引き出しをたくさん増やしておけば、「何だかうまくいかないな…」と思った時でも、ひらめきが生まれるかもしれません
今回紹介した機能は公式のサービスコンテナのドキュメントにまとまっていますので、皆様も是非services.ymlについて調べてみて下さい!
https://symfony.com/doc/current/service_container.html#learn-more