-ao- ramune blog

©2024 unio / GO2直営からふるラムネ
2018年04月15日

Symfony4でアノテーションによるコントローラ制御

[NEW] PHP8の新機能 Attributes を利用したコントローラ制御はこちら

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作成