Laravel版のAPI PlatformでSystem ProviderやSystem Processorをデコレートするのが難しかった話
3行で
- Laravel版の API Platofrm でSystem Processorをデコレートしようとしたら循環依存が発生した
- LLMや @fuwasegu さんに相談 しつつ調べたら、現状のAPI Platformの内部構造ではSystem ProviderやSystem Processorを適切な方法でデコレートすることができないということが分かった
- ので改善案を PR してみた
詳細
Laravelのサービスコンテナでは、extend()
を使ってサービスをデコレートすることができます。
この方法で、API PlatformのSystem Processor である WriteProcessor
をデコレートしようと以下のようなコードを書きました。
<?php
namespace App\Providers;
use ApiPlatform\State\Processor\WriteProcessor;
use App\State\AppWriteProcessor;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
}
public function boot(): void
{
$this->app->extend(WriteProcessor::class, function (WriteProcessor $inner) {
return new AppWriteProcessor($inner);
});
}
}
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
final class AppWriteProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $decorated,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
return $this->decorated->process($data, $operation, $uriVariables, $context);
}
}
すると、サービスコンテナにおいて循環依存が発生してしまい、
こうなってしまいました。
原因を調べたところ、API Platformの
- https://github.com/api-platform/laravel/blob/v4.2.2/ApiPlatformDeferredProvider.php#L103
- https://github.com/api-platform/laravel/blob/v4.2.2/ApiPlatformDeferredProvider.php#L183
- https://github.com/api-platform/laravel/blob/v4.2.2/ApiPlatformDeferredProvider.php#L325-L337
この辺で ProcessorInterface
の実装クラスすべてに ProcessorInterface::class
タグが付けられ、
ProcessorInterface::class
タグが付けられたクラスは
ここで CallableProcessor
(アプリケーションが実装している各種Processorを保持するChain of Responsibility)の依存に含まれるという実装になっていました。
つまり、AppWriteProcessor
にも ProcessorInterface::class
タグが付けられて CallableProcessor
の依存に含まれることになります。
そして、AppWriteProcessor
のコンストラクタ引数 $decorated
の型としてある ProcessorInterface
には、
ここで HydraLinkProcessor
または WriteProcessor
がバインドされており、前者であった場合でも、HydraLinkProcessor
は
ここにあるとおり WriteProcessor
に依存しているため、結論として、Laravelが AppWriteProcessor
に対して ProcessorInterface
型の引数を注入するためには、WriteProcessor
を生成する必要があります。
そして最後に、
ここにあるとおり CallableProcessor
が WriteProcessor
の依存であるため、
CallebleProcessor
→ AppWriteProcessor
→ WriteProcessor
→ CallableProcessor
という循環依存になってしまっていたのです。
この循環を断ち切るには、
-
AppWriteProcessor
のコンストラクターの引数の型をWriteProcessor
ではなくmixed
などに変更する - そもそも
AppWriteProcessor
がCallableProcessor
の依存に含まれてしまうことが間違いなので[1]、それを回避する
のいずれかの方法しかありません。
そこで、SkipAutoconfigure
というアトリビュートを作って、
<?php
declare(strict_types=1);
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS)]
class SkipAutoconfigure
{
}
これを AppWriteProcessor
に付与し、
+ use App\Attribute\SkipAutoconfigure;
+ #[SkipAutoconfigure]
final class AppWriteProcessor implements ProcessorInterfacefinal
{
// ...
}
API Platformのコードの この辺 に
foreach ($classes as $className => $refl) {
foreach ($refl->getAttributes() as $attribute) {
if ($attribute->getName() === SkipAutoconfigure::class) {
unset($classes[$className]);
}
}
}
こんなコードを挿入することで、循環しなくなることを確認しました。
この対応を 本家にPRしてみました が、自分でもこれが(現状の実装に合わせた最小コストの対応ではあるかもしれないけど)理想的な対応だとは思ってないので、どうなるかは分かりません。
おわり
( @fuwasegu さん、相談乗ってくれてありがとうございました!)
-
CallableProcessor
がWriteProcessor
の依存になっていることから分かるように、CallableProcessor
に含まれるべきは「アプリケーションレイヤーの State Processor」のみであり、System Processor であるAppWriteProcessor
が含まれてしまうのは間違いです。現状のLaravel版API Platformでは、アプリケーションのコードベースにSystem Processor(のデコレーター)がいることが想定されていなかったということですね。 ↩︎
Discussion