Symfony Component Advent Calendar 2022の5日目の記事です。
最初に
SymfonyはPHPのフレームワークのひとつです。しかし、公式サイトの説明文には
Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
(SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。)
と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。
今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。
※記事内ではautoloadのインポートは省略します。
依存性の注入、"DependencyInjection"
DependencyInjectionは、依存性の注入の設定・実行を行います。PSR-11互換のサービスコンテナを提供し、依存したオブジェクトを簡単に注入することができます。Symfony以外でも利用できます。
インストール
composer require symfony/dependency-injection
そもそも依存性の注入ってなに?
たとえば、以下のようなクラスがあったとします。
class OrderService
{
public function invoke(Cart $cart)
{
// 注文処理
...
// メールを送信
$mailer = new Mailer();
$mailer->send('ユーザのメールアドレス', '注文ありがとう!');
}
}
OrderService
では注文処理をしますが、注文処理完了後にメール送信します。メール送信を行うためにMailer
のオブジェクトを生成し、メール送信します。この場合、OrderService
はMailer
に依存していますが、このMailer
のオブジェクトをメソッド内で生成しているので、依存が内包されてしまっています。
この場合、OrderService
を実行・テストしようとすると、必ずMailer
クラスが必要となります。OrderService
はMailer
にべったり依存しています。この依存に一定の距離を与えるのが依存性の注入です。(いわゆる、『関心の分離』)
依存性の注入にはいくつかやり方があり、コンストラクターインジェクション
、セッターインジェクション
が有名ですが、SymfonyのDependencyInjectionはいずれも対応しています。
class OrderService
{
public function __construct(private readonly Mailer $mailer) // コンストラクターインジェクション
{
}
public function setMailer(Mailer $mailer) // セッターインジェクション
{
$this->mailer = $mailer;
}
}
サービスコンテナによる、依存情報の登録
まず、ContainerBuilder
という依存を管理するオブジェクトに、クラスの依存情報を登録します。ここには依存する側と依存される側の両方を登録します。
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
$containerBuilder = new ContainerBuilder();
// パラメータの登録
$containerBuilder->setParameter('mailer.from', 'Fromのメールアドレス');
// 依存される側の登録
$containerBuilder
->register('mailer', Mailer::class)
->addArgument('%mailer.from%') // コンストラクタで渡す引数の値の設定
;
// 依存する側の登録(コンストラクタインジェクション)
$containerBuilder
->register('order_service', OrderService::class)
->addArgument(new Reference('mailer')) // コンストラクタで渡す依存の設定
;
// 依存する側の登録(セッターインジェクション)
$containerBuilder
->register('order_service', OrderService::class)
->addMethodCall('setMailer', [new Reference('mailer')]) // セッターで渡す依存の設定
そして、利用する際は、このContainerBuilder
からオブジェクトを取得します。
$orderService = $containerBuilder->get('order_service');
はい。めんどくさい。
設定ファイルに依存情報を記述する
DependencyInjectionでは設定ファイルを読み込んで依存情報を設定するやり方も提供されています。
YAMLで記述するので、YAMLコンポーネントを別途インストールします。
composer require symfony/yaml
まず、上記と同じ設定をYAMLに記述します。
parameters:
mailer.from: 'Fromアドレス'
services:
mailer:
class: Mailer
arguments: ['%mailer.from%']
#コンストラクタインジェクションの場合
order_service:
class: OrderService
arguments: ['@mailer']
# セッターインジェクションの場合
order_service:
class: OrderService
calls:
- [setMailer, ['@mailer']]
そして、ContainerBuilder
に設定ファイルを読み込ませるにはYamlFileLoader
を使います。
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
$containerBuilder = new ContainerBuilder();
$loader = new YamlLoader($containerBuilder, new FileLocator(__DIR__));
$loader->load('services.yaml');
$orderService = $containerBuilder->get('order_service');
オブジェクトの呼び出しは同じくget
メソッドを使って呼び出します。
はい。めんどくさい。
設定おまかせ、"オートワイヤリング"
上記のやり方でももちろん設定ができますが、オートワイヤリングという機能により、これらの設定をせずに依存の設定が完了します。
parameters:
mailer.from: 'Fromアドレス'
services:
_defaults:
autowire: true
autoconfigure: true
# ...
services:
mailer:
class: Mailer
arguments: ['%mailer.from%']
# オートワイヤリング
order_service:
class: OrderService
autowire: true
コンストラクタやセッターを自動解析して、依存しているクラスのオブジェクトも登録します。依存しているクラスがvendor
内のクラスであってもOKです。
また、コンストラクタ・セッターにInterfaceが使われている場合は、実装クラスを探し出して登録します。定数のパラメータや実装クラスが複数ある時で自動解決が難しい場合のみ、services.yaml
に設定を追記します。
まだ、ちょいめんどくさい。
Symfonyでは!
なお、Symfonyではあらかじめservices.yamlが用意されていますが、以下のようになっています。
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
この設定により、DependencyInjection
, Entity
, Kernel.php
以外の全てのクラスが自動的にContainerBuilder
に登録されます。
また、セッターインジェクションの場合は、オートワイヤリングせず、本来はservices.yaml
に記述する必要があるのですが、クラスのセッターメソッドに#[Required]
アトリビュートをつけると、こちらもオートワイヤリングしてくれるようになります。
class OrderService
{
#[Required] // これをつけると自動で呼んでくれる
public function setMailer(Mailer $mailer) // セッターインジェクション
{
$this->mailer = $mailer;
}
}
はい。便利。
まとめ
今回はDependencyInjection
の紹介でした。単体で使っても非常に強力なコンポーネントですが、Symfony内では一段と輝きます。このオートワイヤリングでの依存性注入は、非常におすすめなので、ぜひ一度Symfonyを使って体験してもらいたい機能です。