-ao- ramune blog

©2020 unio / GO2直営からふるラムネ

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

2018年04月15日
  • PHP
  • 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作成
プロフィール画像
なかのひと:unio

数十年前の牧歌的なインターネッツが好きだった、永遠のモラトリアム人。 ただ、モラトリアムしててもお金は増えないので、しゃかいの厳しさを斜め後ろから眺めつつほそぼそと生活しています。

Twitter GitHub
[広告]