Symfony4でアノテーションによるコントローラ制御
APIコントローラとかとか
Symfony4で、Controllerに自前のアノテーションを付けてリクエストを制御する方法です。 今回はXMLHttpRequestをアノテーションで制御するコードを作成します。 ちなみに今回の場合だと、apacheやnginxなどのサーバ側で制御してしまうやり方もあります。
- X-Requested-Withヘッダーの無いリクエストの場合に、4xxや指定したページへ飛ばします
- アノテーションはReflectionで取ります
- DoctrineのAnnotationReaderを使うと楽できます
- kernel.request イベントで処理できるように、EventListenerを使います
コントローラとアノテーション
まずはコントローラーを用意します。 @XMLHttpRequestアノテーションをつけて、XMLHttpRequestを制御します。
XHRController.php (gist)
<?php
namespace App\Controller;
use App\Annotation\XMLHttpRequest;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* @XMLHttpRequest()
*/
class XHRController extends Controller
{
/**
* 関数のアノテーションが優先される
*
* @Route("/xhr", name="list", methods={"GET"})
* @XMLHttpRequest("index")
*/
public function list() {...}
/**
* 関数にアノテーションが付いていない場合はクラスのアノテーションで動作
*
* @Route("/xhr", name="post", methods={"POST"})
*/
public function post() {...}
}
アノテーションは、Route名を受け取れるように $value を用意します。
XMLHttpRequest.php (gist)
<?php
namespace App\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* アノテーション
*
* @Annotation
*/
class XMLHttpRequest
{
public $value;
}
$valueは@XMLHttpRequest("route_hoge")のようにアノテーションの引数を受け取ることができます。
名前付きプロパティが必要な場合は、同じ名前のプロパティをアノテーションクラスに用意します。 例えば$hogeを用意すると、@XMLHttpRequest(hoge="piyo")と記述できます。
リスナーの作成
コントローラーとアノテーションの用意はできたので、次はリスナーを作成します。
XMLHttpRequestListener.php (gist)
<?php
namespace App\EventListener;
use App\Annotation\XMLHttpRequest;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class XMLHttpRequestListener
{
/** @var AnnotationReader */
private $reader;
/** @var ControllerResolverInterface */
private $resolver;
/** @var Router */
private $router;
public function __construct(
AnnotationReader $reader,
ControllerResolverInterface $resolver,
Router $router
)
{
$this->reader = $reader;
$this->resolver = $resolver;
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
// twigのrenderなど、内部リクエストはアノテーション処理しない
if (!$event->isMasterRequest()) {
return;
}
// アノテーション取得
$annotations = $this->getAnnotations($event);
foreach ($annotations as $annotation) {
// XMLHttpRequestアノテーションでない場合はスキップ
if (!($annotation instanceof XMLHttpRequest)) {
continue;
}
// X-Requested-Withヘッダーを持っている場合はスキップ
if ($event->getRequest()->isXmlHttpRequest()) {
continue;
}
// 以下から、4xxや別ページに飛ばしたりする処理
if (!is_string($annotation->value)
|| trim($annotation->value) === '') {
throw new BadRequestHttpException();
}
try {
$url = $this->router->generate($annotation->value);
$event->setResponse(new RedirectResponse($url));
} catch (\Exception $e) {
throw new NotFoundHttpException($e);
}
return;
}
}
private function getAnnotations(GetResponseEvent $event)
{
// [0]にControllerのオブジェクト、[1]にメソッド名が入る
$controller = $this->resolver->getController($event->getRequest());
$class = get_class($controller[0]);
$methodName = $controller[1];
if (!class_exists($class)) {
throw new \AssertionError('class does not exist: ' . $class);
}
$reflection = new \ReflectionClass($class);
return array_merge(
$this->reader->getMethodAnnotations(
$reflection->getMethod($methodName)),
$this->reader->getClassAnnotations($reflection)
);
}
}
$annotation->valueが空の場合は400を返し、Route名が入っている場合はL62でリダイレクト先URLを作成しています。
$url = $this->router->generate($annotation->value);
L85-89では、関数のアノテーションを優先させるために、関数のリフレクションを元にarray_mergeしています。
return array_merge(
$this->reader->getMethodAnnotations(
$reflection->getMethod($methodName)),
$this->reader->getClassAnnotations($reflection)
);
最後はserviceのyaml設定です。
services.yaml
# EventListenerはAutowiringではなくマニュアルで設定
services:
App\EventListener\XMLHttpRequestListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
arguments:
- '@annotations.reader'
- '@controller_resolver'
- '@router'
argumentsで設定したサービスは下記の通りです。
サービス | パッケージ | 役割 |
---|---|---|
AnnotationReader | Doctrine | アノテーション解析 |
ControllerResolver | Symfony | Requestをコントローラから取得 |
Router | Symfony | リダイレクトURL作成 |