扩展操作参数解析
在 控制器指南 中,你已经了解到可以通过控制器中的参数获取 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,你必须使用 MapRequestPayload 或 MapQueryString 属性才能使用此解析器。
- RequestAttributeValueResolver
- 尝试查找与参数名称匹配的请求属性。
- DateTimeValueResolver
-
尝试查找与参数名称匹配的请求属性,并在使用扩展
DateTimeInterface
的类进行类型提示时注入DateTimeInterface
对象。默认情况下,PHP 可以解析为日期字符串的任何输入都被接受。你可以使用 MapDateTime 属性限制输入的格式。
- 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\ServerRequestInterface
、Psr\Http\Message\RequestInterface
或Psr\Http\Message\MessageInterface
类型的 PSR-7 对象创建的 Symfony HttpFoundationRequest
对象。它需要安装 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
的服务,但你可以自己设置它以更改其 priority
或 name
属性。
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
虽然添加优先级是可选的,但建议添加一个优先级以确保注入预期值。内置的 RequestAttributeValueResolver
从 Request
中获取属性,其优先级为 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
}
}