🎻

[Symfony] EntityTypeの選択肢をquery_builderではなくPHPの処理でフィルタリングする

2023/02/06に公開

小ネタというかメモです。

EntityTypeの選択肢をフィルタリングしたい場合、通常は query_builder オプション を使用します。

class UserChoiceType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => User::class,
            'query_builder' => function (UserRepository $repository) {
                return ($qb = $repository->createQueryBuilder('u'))
                    ->andWhere($qb->expr()->in('u.state', [User::STATE_X, User::STATE_Y, User::STATE_Z]))
                ;
            },
        ]);
    }

    public function getParent(): string
    {
        return EntityType::class;
    }
}

オプションを使って利用者側が任意に絞り込み条件を変えられるようにするなら以下のようになるでしょう。

class UserChoiceByStateType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => User::class,
            'query_builder' => function (Options $options) {
                return function (UserRepository $repository) use ($options) {
                    return $repository->createQueryBuilder('u')
                        ->andWhere($qb->expr()->in('u.state', $options['states']))
                    ;
                };
            },
            'states' => [],
        ]);

        $resolver->setRequired('states');
        $resolver->setAllowedTypes('states', ['array']);
    }

    public function getParent(): string
    {
        return EntityType::class;
    }
}

しかし、この方法ではあくまで絞り込みの処理はDBレイヤーに任せるしかなく、PHP側の処理でフィルタリングすることはできません。

そんなことをしたくなるケースは稀ではありますが、例えばユーザーのROLEを元にフィルタリングしたい場合などは、role_hierarchy が設定されているとDBのカラムには必ずしも当該ROLEの文字列が格納されていない場合があるため、あくまでPHP側でSecurity Componentを使って判定する必要があります。

この場合、以下のように choice_loader を使えばやりたいことが実現できます。

class UserChoiceByRoleType extends AbstractType
{
    public function __construct(private UserRepository $repository, private RoleHierarchyInterface $roleHierarchy)
    {
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => User::class,
            'choice_loader' => function (Options $options) {
                $users = array_filter($this->repository->findAll(), function (User $user) use ($options) {
                    foreach ($options['roles'] as $role) {
                        if (in_array($role, $this->roleHierarchy->getReachableRoleNames($user->getRoles()), true)) {
                            return true;
                        }
                    }

                    return false;
                });

                $userIds = array_map(fn (User $user) => $user->getId(), $users);

                return new CallbackChoiceLoader(fn () => array_combine($userIds, $users));
            },
            'roles' => [],
        ]);

        $resolver->setAllowedTypes('roles', ['array']);
    }

    public function getParent(): string
    {
        return EntityType::class;
    }
}

choice_loader を使ってPHP側で選択肢を用意するならEntityTypeではなくChoiceTypeを使えばいいのでは?と一瞬思いますが、それだと各選択肢が自動でエンティティにマッピングされないので自前でDataTransformerなどで変換しなければならなくなります。

GitHubで編集を提案

Discussion