Symfony5で自作ParamConverter
AttributeでオリジナルのParamConverterを作ろう
ParamConverter
便利ですよね。(Symfony6から
コア機能として提供される予定です)
独自にカスタムしたいケースでは ParamConverterInterface を実装したコンバーターを作成することになるのですが、
今回は勉強がてらPHP8のAttributeを使ってオリジナルのParamConverterを作成してみました。
- SymfonyのAutowiring設定が有効になっている前提です
- AttributeはReflectionで取ります
- kernel.requestなどの カーネルイベント で処理します
イメージとしては、下記のようなコントローラで hoge ID に対応するMyObjオブジェクトを取得できるようにします。
MyController.php
class MyController
{
// 引数は必ずnullable(?MyObj)で設定してください
#[MyConverter(class: MyObj::class, routeKey: 'hoge')]
#[Route('/{hoge}', name: 'my_controller')]
public function index(?MyObj $object): Response
{
...
}
}
Attributeの作成
MyConverter.php
<?php
namespace App\Annotation;
use Attribute;
// Attribute::TARGET_METHOD でメソッドのみ利用できるAttributeになります
// Attribute::IS_REPEATABLE を設定すると、重複してAttributeを設置することができます
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class MyConverter
{
public string $class;
public string $routeKey;
// コンバート対象のクラスと、Routeのキーを設定します
public function __construct(string $class, string $routeKey)
{
$this->class = $class;
$this->routeKey = $routeKey;
}
}
EventSubscriberの作成
MyConvertSubscriber.php
<?php
namespace App\EventSubscriber;
use App\Annotation\MyConverter;
use App\Entity\MyObj;
use App\Repository\MyRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class MyConvertSubscriber implements EventSubscriberInterface
{
private MyRepository $repository;
public function __construct(MyRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents(): array
{
return [
// コントローラの呼び出し直前に発火するイベントです
// https://symfony.com/doc/current/reference/events.html
KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments',
];
}
public function onKernelControllerArguments(ControllerArgumentsEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$controller = $request->attributes->get('_controller');
try {
// リクエストを受け取ったコントローラのメソッドリクレクションを取得します
$method = new \ReflectionMethod($controller);
} catch (\Exception $e) {
return;
}
// メソッドに設定されたMyConverter Attributeを取得します
$refs = $method->getAttributes(MyConverter::class);
$attributes = array_map(fn(\ReflectionAttribute $ref) => $ref->newInstance(), $refs);
if ($attributes === []) {
return;
}
// メソッド引数の何番目が対象のオブジェクトかを把握するためのマップを作成します
$paramIndexes = [];
foreach ($method->getParameters() as $i => $param) {
// メソッド引数のクラス名がキーになります
$paramIndexes[$param->getType()->getName()] = $i;
}
$args = $event->getArguments();
foreach ($attributes as $attribute) {
// Attributeで指定したクラスが引数に存在しない場合はスキップします
if (!isset($paramIndexes[$attribute->class])) {
continue;
}
// SymfonyのRouteで指定したキーに対応した値を取得します
// #[Route('/{hoge}')] の場合、routeKeyに hoge を指定することで取得可能です
$id = $request->attributes->getInt($attribute->routeKey);
// クラスに応じてオブジェクト生成方法はスイッチさせる必要があります
switch ($class) {
case MyObj::class:
$entity = $this->repository->find($id);
break;
}
if ($entity === null) {
continue;
}
// 先ほど作成したインデックスでコントローラ引数にオブジェクトを設定します
$args[$paramIndexes[$attribute->class]] = $entity;
}
// コントローラ引数を設定しなおして完了です
$event->setArguments($args);
}
}
これでMyObjとMyRepositoryを適当に用意すれば冒頭のコントローラでオブジェクトが設定されるようになります。