Symfonyにおけるdecoratesオプションの使用
EC-CUBE4のプラグインを開発していて、既存のクラスをSymfonyのdecoratesオプションで拡張する機会がありました。
How to Decorate Services
https://symfony.com/doc/3.4/service_container/service_decoration.html (*1)
これは、いわゆるデコレーターパターンを使って既存のクラスを拡張する手法です。
Symfonyのdecoratesオプションを使ったクラスの拡張については、上記のSymfonyのドキュメントページにもPHPクラス側のコード例がないのと、ネット上のいくつかサンプルコードもinner idを使用せず、デコレーターというより単なる継承による拡張と変わらないものも多かったので、decoratesオプションの使い方をここにまとめておきます。
Decoratorパターンの簡単な説明
まず、一旦Symfonyから離れて、単純にDecoratorパターンの例を示します。 DecoratorパターンはDecoratorが既存のクラスをWrapし、メソッド呼び出し時に何らかの処理を挿入することで、既存クラスに機能追加することを可能にします。 クラス図やパターンの詳細はWikipedia - Decorator pattern(*2)でも参照してもらうとして、まずコード例を示します。
Decoratorパターンの例
// 基底クラス(クラス図のComponent相当) <?php abstract class Greeting { abstract public function say(): string; } // クラス図のConcreteComponent相当 class Hello extends Greeting { public function say(): string { return 'Hello'; } } // Decoratorの基底クラス(クラス図のDecorator相当) // Decoratorは装飾対象のクラスを内部に保持する($innerプロパティ)。 abstract class GreetingDecorator extends Greeting { protected Greeting $inner; // decoration対象を保持 public function __construct(Greeting $inner) { $this->inner = $inner; } public function say(): string { return $this->inner->say(); // メソッド呼び出しのチェーン } } // 挨拶を強調するDecorator(クラス図のConcreteDecorator相当) class EmphasisDecorator extends GreetingDecorator { public function say(): string { $greeting = parent::say(); return $greeting . '!!'; } } // 挨拶を繰り返すDecorator(クラス図のConcreteDecorator相当) class RepeatDecorator extends GreetingDecorator { public function say(): string { $greeting = parent::say(); return "$greeting $greeting"; } } // 様々な種類のGreetingオブジェクトを作成して返す function create($type): Greeting { switch ($type) { case 'hello': default: // decorationなし $greeting = new Hello(); break; case 'emphasis': // decoration // 大声で挨拶するようにdecoration $greeting = new EmphasisDecorator(new Hello()); break; case 'emphasis_repeat': // decoratorのチェーン // 大声で二度挨拶するようにdecoration $greeting = new RepeatDecorator(new EmphasisDecorator(new Hello())); break; case 'repeat_emphasis': // 順序の変更 $greeting = new EmphasisDecorator(new RepeatDecorator(new Hello())); break; } return $greeting; } foreach (['hello', 'emphasis', 'emphasis_repeat', 'repeat_emphasis'] as $type) { $hello = create($type); print $hello->say() . "\n"; }
この例では、挨拶をするGreeting抽象クラスとそれを装飾する二つのDecorator(EmphasisDecorator, RepeatDecorator)があります。また、'Hello'と挨拶をするGreetingを継承したHello具象クラスがあり、これをDecoratorでwrapすることでHelloクラスのsay()メソッドの動作をカスタマイズしています。
実行例
Hello デコレートなし Hello!! EmphasisDecoratorでデコレート Hello!! Hello!! EmphasisDecorator → RepeatDecoratorでデコレート Hello Hello!! RepeatDecorator → EmphasisDecoratorでデコレート
Decoratorパターンのポイントとしては、Decorator(GreetingDecoratorクラス)では装飾対象オブジェクトを保持し(上記例では$innerプロパティ)、メソッド呼び出し時は、$inner経由で呼び出しをチェーンしていく点になります(図1)。これにより、DecoratorをさらにDecoratorでwrapして複数のカスタマイズをすることもできます。

同様のメソッドの拡張は継承によっても実現できますが、継承による拡張と異なり、動的(実行時)に拡張内容を変更できるのもDecoratorパターンの特徴となります。
以上がDecoratorパターンの簡単な説明になります。
Symfonyでの例
ここでSymfonyに話を戻し、先ほどの例をdecoratesオプションを使って実装します。といってもクラス類は上記の例を流用するので、主にservices.yamlの設定方法の説明になります。関連するファイルの一覧は以下のとおりです。
./config/services.yaml ./src/Command/GreetCommand.php ./src/Service/ Greeting.php Hello.php GreetingDecorator.php EmphasisDecorator.php RepeatDecorator.php
Service配下の各コードは先ほどのコード例をクラスごとのファイルにばらしたものです。namespaceは異なりますがクラスの実装は変わりません。このため、例としてEmphasisDecorator.phpの内容を示すのみにとどめます。
Service/EmphasisDecorator.php
<?php
namespace App\Service;
// クラスは先ほどの例と同じ
class EmphasisDecorator extends GreetingDecorator
{
public function say(): string
{
$greeting = parent::say();
return $greeting . '!!';
}
}
それでは、services.yamlの設定を見ていきます。クラスをDecoratorとして機能させるには、decoratesオプションで装飾対象のクラスを指定します。以下はEmphasisDecoratorに関する設定になります。
services.yamlでのDecoratorの設定
services: # Greetingのtype hintでHelloがDIされるようにする App\Service\Hello: ~ App\Service\Greeting: '@App\Service\Hello' # Helloに対するDecoratorの設定 App\Service\EmphasisDecorator: decorates: App\Service\Hello arguments: $inner: '@App\Service\EmphasisDecorator.inner'
decoratesオプションを指定することで、サービスコンテナに以下の変更が適用されます。
- HelloがEmphasisDecoratorに置き換えられる
これにより、Greetingに対するtype hintが行われた場合、HelloではなくEmphasisDecoratorがDIされるようになる。 - "クラス名".innerというIDが作成される
この.inner IDは装飾対象のオブジェクト(この場合Helloオブジェクト)を指す。
このIDをargumentsオプションでDecoratorに渡すようにすることで、Decoratorに装飾対象のオブジェクトを受け取ることができるようになる。この例では$inner引数に渡している。
以上で、EmphasisDecoratorの設定は完了です。services.yamlの設定のみで、Services側のクラスは最初の例から何ら変更していません。
Decoratorを複数指定してチェーンする設定とすることもできます。RepeatDecoratorの設定も追加したservices.yamlの例を以下に示します。
services.yamlの例
services: _defaults: autowire: true 略 # Greetingのtype hintでHelloがDIされるようにする App\Service\Hello: ~ App\Service\Greeting: '@App\Service\Hello' # Helloに対するDecoratorの設定 App\Service\EmphasisDecorator: decorates: App\Service\Hello arguments: $inner: '@App\Service\EmphasisDecorator.inner' # 複数指定してチェーンする App\Service\RepeatDecorator: decorates: App\Service\Hello arguments: $inner: '@App\Service\RepeatDecorator.inner'
一方、services.yamlで設定したDecoratorを使う例を見てみます。 GreetCommandがコンソールからコマンドを実行した際の例です。
GreetCommand.php
<?php namespace App\Command; use App\Service\Greeting; use Symfony\Component\Console; class GreetCommand extends Console\Command\Command { protected static $defaultName = 'greet'; private Greeting $greeting; // DecorateされたオブジェクトをDI public function __construct(Greeting $greeting) { parent::__construct(self::$defaultName); $this->greeting = $greeting; } protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int { $output->writeln($this->greeting->say()); return 0; } }
コンストラクタでGreeting型の引数を受け取っています。 この引数はサービスコンテナによるDIで注入されますが、実際に何が注入されるかというと、services.yamlの設定に従って new RepeatDecorator(new EmphasisDecorator(new Hello())) のようにDecorateしたオブジェクトが注入されます。 このため、実行例は以下のようになります。
実行例
Hello!! Hello!!
以上がdecoratesオプションを使ったクラスの拡張例になります。
EC-CUBE4での例
最後に実践的な例として、EC-CUBE4で実際にProductRepositoryを拡張した例を示しておきます。
EC-CUBEの多言語表示用のプラグインを作成した際、検索も多言語に対応するためにProductRepositoryのgetQueryBuilderBySearchData()メソッドを拡張する必要があり、この拡張をdecoratesオプションを使って行いました。services.yamlとDecoratorの内容を以下に示します。
ProductRepositoryをデコレートするservices.yaml
services: Plugin\MultiLingual\Repository\ProductRepositoryDecorator: autowire: true decorates: Eccube\Repository\ProductRepository arguments: $inner: '@Plugin\MultiLingual\Repository\ProductRepositoryDecorator.inner'
ProductRepositoryを拡張するDecorator(*3)
class ProductRepositoryDecorator extends ProductRepository { /** * @var ProductRepository 装飾対象のオブジェクト */ private $inner; public function __construct( ProductRepository $inner, RegistryInterface $registry, Queries $queries, EccubeConfig $eccubeConfig ) { $this->inner = $inner; parent::__construct($registry, $queries, $eccubeConfig); } public function getQueryBuilderBySearchData($searchData) { // メソッドの呼び出しをチェーン $qb = $this->inner->getQueryBuilderBySearchData($copied); // 多言語検索のための処理を追加 略 return $qb; } }
拡張対象のgetQueryBuilderBySearchData()は基底クラスではなくProductRepositoryに実装されているので、 ProductRepositoryDecoratorクラスがConcreteComponentにあたるProductRepositoryを直接継承していたり、Decoratorの基底クラスが存在していなかったりと、Decoratorパターンの教科書どおりの階層構造ではありません。ただ、decorationにおいて重要なのは装飾対象のオブジェクトを保持し、それをチェーンして呼び出していくという点で、その動作は変わっていないのがわかると思います。
まとめ
Symfonyのdecoratesオプションを使ったクラスの拡張例をPHPのコード例も含めて説明しました。
ネット上にもdecoratesオプションを使ったサンプルコードはあるにはあるのですが、.innerを使っておらず、単なる継承による拡張と変わらないものも多かったので、ちゃんとデコレートで拡張する方法について解説しました。
デコレートによる拡張を行うのであれば、innerオブジェクトを保持して、メッセージの呼び出しをチェーンしていくことになります。 innerが存在しないのであれば、その拡張はおそらくDecoratorである必要はなく、単に継承で済む話です。 また、decoratesオプションを使いつつも、サブクラスでメソッドを追加したりしているものも、デコレートではなく継承による拡張になります(*4)。
単に継承による拡張であれば、How to Decorate Servicesの冒頭にもあるように、以下のようにしておけば済む話ですし、意図も明確になると思います。
services: Eccube\Repository\ProductRepository: autowire: true autoconfigure: true class: Plugin\MultiLingual\Repository\ExProductRepository
継承による拡張
class ExProductRepository extends ProductRepository { // ここでメソッドを追加したりオーバーライドして拡張 }
(*1) リンク先のドキュメントはEC-CUBE4.0で使われているのと同じSymfony 3.4のものです。新しいSymfonyではAnnotationによる記述もできるようですが考え方は変わりません。
(*2) 日本語ページもありますが、情報量が少ないため英語ページの方がおすすめです。
(*3) 一部抜粋したものなので、ファイルの完全な形はこちらを参照。
(*4) Decoratorパターンは既存のメソッドを装飾して機能追加したり動作をカスタマイズするためのもので、メソッドの追加などを行うためのものではありません。
投稿日:2022/08/04 17:20