🕸️

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 をデコレートしようと以下のようなコードを書きました。

app/Providers/AppServiceProvider.php
<?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の

この辺で 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 を生成する必要があります。

そして最後に、

ここにあるとおり CallableProcessorWriteProcessor の依存であるため、

CallebleProcessorAppWriteProcessorWriteProcessorCallableProcessor という循環依存になってしまっていたのです。

この循環を断ち切るには、

  • AppWriteProcessor のコンストラクターの引数の型を WriteProcessor ではなく mixed などに変更する
  • そもそも AppWriteProcessorCallableProcessor の依存に含まれてしまうことが間違いなので[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 さん、相談乗ってくれてありがとうございました!)

脚注
  1. CallableProcessorWriteProcessor の依存になっていることから分かるように、CallableProcessor に含まれるべきは「アプリケーションレイヤーの State Processor」のみであり、System Processor である AppWriteProcessor が含まれてしまうのは間違いです。現状のLaravel版API Platformでは、アプリケーションのコードベースにSystem Processor(のデコレーター)がいることが想定されていなかったということですね。 ↩︎

GitHubで編集を提案

Discussion