跳到内容

扩展操作参数解析

编辑此页

控制器指南 中,你已经了解到可以通过控制器中的参数获取 Request 对象。此参数必须通过 Request 类进行类型提示才能被识别。这通过 ArgumentResolver 完成。通过创建和注册自定义值解析器,你可以扩展此功能。

内置值解析器

Symfony 在 HttpKernel 组件 中附带了以下值解析器

BackedEnumValueResolver

尝试从路由路径参数中解析与参数名称匹配的 backed enum case。如果值不是枚举类型的有效 backing 值,则会导致 404 Not Found 响应。

例如,如果你的 backed enum 是

1
2
3
4
5
6
7
8
9
namespace App\Model;

enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

并且你的控制器包含以下内容

1
2
3
4
5
6
7
8
9
10
class CardController
{
    #[Route('/cards/{suit}')]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

当请求 /cards/H URL 时,$suit 变量将存储 Suit::Hearts case。

此外,你可以使用 EnumRequirement 将路由参数的允许值限制为一个(或多个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Routing\Requirement\EnumRequirement;

// ...

class CardController
{
    #[Route('/cards/{suit}', requirements: [
        // this allows all values defined in the Enum
        'suit' => new EnumRequirement(Suit::class),
        // this restricts the possible values to the Enum values listed here
        'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]),
    ])]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

上面的示例仅允许请求 /cards/D/cards/S URL,并在其他两种情况下导致 404 Not Found 响应。

RequestPayloadValueResolver

将请求负载或查询字符串映射到类型提示的对象中。

因为这是一个 targeted value resolver,你必须使用 MapRequestPayloadMapQueryString 属性才能使用此解析器。

RequestAttributeValueResolver
尝试查找与参数名称匹配的请求属性。
DateTimeValueResolver

尝试查找与参数名称匹配的请求属性,并在使用扩展 DateTimeInterface 的类进行类型提示时注入 DateTimeInterface 对象。

默认情况下,PHP 可以解析为日期字符串的任何输入都被接受。你可以使用 MapDateTime 属性限制输入的格式。

提示

DateTimeInterface 对象是使用 Clock 组件 生成的。这使你可以在测试应用程序和使用 MockClock 实现时,完全控制控制器接收的日期和时间值。

RequestValueResolver
如果使用 Request 或扩展 Request 的类进行类型提示,则注入当前的 Request
ServiceValueResolver
如果使用有效的服务类或接口进行类型提示,则注入服务。这类似于 自动装配
SessionValueResolver
如果使用 SessionInterface 或实现 SessionInterface 的类进行类型提示,则注入配置的实现 SessionInterface 的会话类。
DefaultValueResolver
如果存在默认值且参数是可选的,则将设置参数的默认值。
UidValueResolver

尝试将路由路径参数中的任何 UID 值转换为 UID 对象。如果值不是有效的 UID,则会导致 404 Not Found 响应。

例如,以下代码会将 token 参数转换为 UuidV4 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\UuidV4;

class DefaultController
{
    #[Route('/share/{token}')]
    public function share(UuidV4 $token): Response
    {
        // ...
    }
}
VariadicValueResolver
验证请求数据是否为数组,并将所有数据添加到参数列表中。当调用 action 时,最后一个(可变参数)参数将包含此数组的所有值。

此外,一些组件、桥梁和官方扩展包提供了其他值解析器

UserValueResolver

如果使用 UserInterface 进行类型提示,则注入表示当前登录用户的对象。你也可以类型提示你自己的 User 类,但你必须向参数添加 #[CurrentUser] 属性。如果匿名用户可以访问控制器,则可以将默认值设置为 null。它需要安装 SecurityBundle

如果参数不可为空,并且没有登录用户,或者登录用户具有与类型提示类不匹配的用户类,则解析器会抛出 AccessDeniedException 以阻止访问控制器。

SecurityTokenValueResolver

如果使用 TokenInterface 或扩展它的类进行类型提示,则注入表示当前登录令牌的对象。

如果参数不可为空,并且没有登录令牌,则解析器会抛出状态代码为 401 的 HttpException 以阻止访问控制器。

EntityValueResolver

自动查询实体并将其作为参数传递给你的控制器。

例如,以下代码将查询以 {id} 作为主键的 Product 实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DefaultController
{
    #[Route('/product/{id}')]
    public function share(Product $product): Response
    {
        // ...
    }
}

要了解有关 EntityValueResolver 的更多信息,请参阅专门的章节 自动获取对象

PSR-7 对象解析器
注入从 Psr\Http\Message\ServerRequestInterfacePsr\Http\Message\RequestInterfacePsr\Http\Message\MessageInterface 类型的 PSR-7 对象创建的 Symfony HttpFoundation Request 对象。它需要安装 PSR-7 Bridge 组件。

管理值解析器

对于每个参数,将调用每个标记为 controller.argument_value_resolver 的解析器,直到其中一个提供值。它们的调用顺序取决于它们的优先级。例如,SessionValueResolver 将在 DefaultValueResolver 之前调用,因为它的优先级更高。这允许编写例如 SessionInterface $session = null 以获取会话(如果有),或者 null(如果没有)。

在那种特定情况下,你不需要在 SessionValueResolver 之前运行任何解析器,因此跳过它们不仅可以提高性能,还可以防止其中一个在 SessionValueResolver 有机会之前提供值。

ValueResolver 属性允许你通过“targeting”你想要的解析器来做到这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/SessionController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver;
use Symfony\Component\Routing\Attribute\Route;

class SessionController
{
    #[Route('/')]
    public function __invoke(
        #[ValueResolver(SessionValueResolver::class)]
        SessionInterface $session = null
    ): Response
    {
        // ...
    }
}

在上面的示例中,SessionValueResolver 将首先被调用,因为它被 targeted。DefaultValueResolver 将在没有提供值时接下来被调用;这就是为什么你可以将 null 分配为 $session 的默认值。

你可以通过将解析器的名称作为 ValueResolver 的第一个参数来 target 解析器。为了方便起见,内置解析器的名称是它们的 FQCN。

也可以通过将 ValueResolver$disabled 参数传递给 true 来禁用 targeted 解析器;这就是 MapEntity 允许为特定控制器禁用 EntityValueResolver 的方式。是的,MapEntity 扩展了 ValueResolver

添加自定义值解析器

在下一个示例中,你将创建一个值解析器,以便在控制器参数具有实现 IdentifierInterface(例如 BookingId)的类型时注入 ID 值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;

class BookingController
{
    public function index(BookingId $id): Response
    {
        // ... do something with $id
    }
}

添加新的值解析器需要创建一个实现 ValueResolverInterface 的类并为其定义服务。

此接口包含一个 resolve() 方法,该方法针对控制器的每个参数调用。它接收当前的 Request 对象和一个 ArgumentMetadata 实例,该实例包含来自方法签名的所有信息。

resolve() 方法应返回空数组(如果它无法解析此参数)或包含已解析值的一个数组。通常,参数解析为单个值,但可变参数需要解析多个值。这就是为什么你必须始终返回一个数组,即使是单个值也是如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use App\IdentifierInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

class BookingIdValueResolver implements ValueResolverInterface
{
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        // get the argument type (e.g. BookingId)
        $argumentType = $argument->getType();
        if (
            !$argumentType
            || !is_subclass_of($argumentType, IdentifierInterface::class, true)
        ) {
            return [];
        }

        // get the value from the request, based on the argument name
        $value = $request->attributes->get($argument->getName());
        if (!is_string($value)) {
            return [];
        }

        // create and return the value object
        return [$argumentType::fromString($value)];
    }
}

此方法首先检查它是否可以解析该值

  • 参数必须使用实现自定义 IdentifierInterface 的类进行类型提示;
  • 参数名称(例如 $id)必须与请求属性的名称匹配(例如,使用 /booking/{id} 路由占位符)。

当满足这些要求时,该方法会创建自定义值对象的新实例,并将其作为此参数的值返回。

就是这样!现在你所要做的就是为服务容器添加配置。这可以通过向你的值解析器添加以下标签之一来完成。

controller.argument_value_resolver

此标签会自动添加到每个实现 ValueResolverInterface 的服务,但你可以自己设置它以更改其 priorityname 属性。

1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    _defaults:
        # ... be sure autowiring is enabled
        autowire: true
    # ...

    App\ValueResolver\BookingIdValueResolver:
        tags:
            - controller.argument_value_resolver:
                name: booking_id
                priority: 150

虽然添加优先级是可选的,但建议添加一个优先级以确保注入预期值。内置的 RequestAttributeValueResolverRequest 中获取属性,其优先级为 100。如果你的解析器也获取 Request 属性,请将优先级设置为 100 或更高。否则,将优先级设置为低于 100,以确保在 Request 属性存在时不会触发参数解析器。

为了确保你的解析器添加到正确的位置,你可以运行以下命令来查看哪些参数解析器存在以及它们的运行顺序

1
$ php bin/console debug:container debug.argument_resolver.inner --show-arguments

你还可以配置传递给 ValueResolver 属性的名称以 target 你的解析器。否则,它将默认为服务的 ID。

controller.targeted_value_resolver

如果你希望仅在 ValueResolver 属性 target 它时才调用你的解析器,请设置此标签。与 controller.argument_value_resolver 一样,你可以自定义你的解析器可以被 target 的名称。

作为替代方案,你可以将 AsTargetedValueResolver 属性添加到你的解析器,并将你的自定义名称作为其第一个参数传递

1
2
3
4
5
6
7
8
9
10
11
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;

#[AsTargetedValueResolver('booking_id')]
class BookingIdValueResolver implements ValueResolverInterface
{
    // ...
}

然后,你可以将此名称作为 ValueResolver 的第一个参数传递,以 target 你的解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;

class BookingController
{
    public function index(#[ValueResolver('booking_id')] BookingId $id): Response
    {
        // ... do something with $id
    }
}
包括代码示例在内的这项工作,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本