アクション引数でログインしているユーザーの情報を取得する


Symfony Advent Calendar 2017 13日目の記事です。
枠が空いてたので埋めていこうという気持ちで頑張って書いてみました。

Symfonyのコントローラクラスでログイン中のユーザー情報を取得する場合に、よくこのようなコードを書いているかと思います。

<?php

namespace App\Controller;


use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class UserController extends Controller
{
    public function index()
    {
        $user = $this->getUser();

    }
}

しかし僕はControllerクラスを継承したくない。
Contorllerを継承しないということは$this->getUser()メソッドを使うことができない。

$this->getUser()はどのような実装になっているのか?

https://git.io/vbgwG
実装を見てみるとsecurity.token_storageからtokenを取得してそのトークンからgetUser()をしているだけ。

コントローラを継承しない場合は以下のように実装すればユーザー情報が取得できる。

<?php


namespace App\Controller;


use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;

class UserController extends Controller
{
    /**
     * @var TokenStorage
     */
    private $tokenStorage;

    /**
     * @param TokenStorage $tokenStorage
     */
    public function __construct(TokenStorage $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }


    public function index()
    {
        $uesr = null;
        if (null !== $token = $this->tokenStorage->getToken()) {
            $user = $token->getUser();
        }
    }
}

しかしTokenStorageをInjectしたり、tokenがnullかどうかの判定もしなければいけない・・・
正直これならControllerクラスを継承したほうがいいような・・・って気持ちになってしまう。

ArgumentValueResolverを使ってログインユーザーの情報を取得する

ArgumentResolverはコントローラのアクションの引数をみて、適切なオブジェクトを引数に渡してくれます。
例えば、以下のようなRequestオブジェクトを引数として受け取るコード。


<?php


namespace App\Controller;


use Symfony\Component\HttpFoundation\Request;

class UserController
{
    public function index(Request $request)
    {
        $page = $request->query->getInt('page', 1);
    }
}

Requestの場合はRequestValueResolverのおかげで、引数として受け取れるようです。
https://git.io/vbgrU

ArgumentValueResolverInterfaceをimplementsすれば、自分でカスタムArgumentResolverを用意することができます。
つまり以下のようにArgumentResolverを用意すればコントローラの引数でログインしているユーザーを取得できる。

<?php
// src/ArgumentResolver/UserValueResolver.php

namespace App\ArgumentResolver;

use App\Entity\User;
use Prophecy\Argument\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;

final class UserValueResolver implements ArgumentValueResolverInterface
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }

    public function supports(Request $request, ArgumentMetadata $argument)
    {
        if (User::class !== $argument->getType()) {
            return false;
        }

        $token = $this->tokenStorage->getToken();

        if (!$token instanceof TokenInterface) {
            return false;
        }

        return $token->getUser() instanceof User;
    }

    public function resolve(Request $request, ArgumentMetadata $argument)
    {
        yield $this->tokenStorage->getToken()->getUser();
    }

}

<?php


namespace App\Controller


use Symfony\Component\HttpFoundation\Request;
use App\Entity\User;

class UserController
{
    public function index(User $user)
    {
        return ['user' => $user];
    }
}

このようにArgumentValueResolverを利用すれば、どのコントローラでもアクションの引数としてログインしているユーザーの情報を取得できますね。
まあでもControllerクラスを継承したほうが楽ですよね・・・。
ただ、ログインユーザー以外でも使えるし、便利なので積極的に使っていきたい気持ちあります。

参考: Extending Action Argument Resolving