fusonic / http-kernel-extensions
Requires
- php: >=8.1
- phpdocumentor/reflection-docblock: ^5.3
- symfony/http-kernel: ^5.4|^6.0
- symfony/property-access: ^5.4|^6.0
- symfony/property-info: ^5.4|^6.0
- symfony/serializer: ^5.4.12|^6.0
- symfony/validator: ^5.4|^6.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.14
- phpstan/phpstan: ^1.10
- phpstan/phpstan-phpunit: ^1.3
- phpstan/phpstan-strict-rules: ^1.5
- phpstan/phpstan-symfony: ^1.3
- phpunit/phpunit: ^9.6
- symfony/cache: ^5.4|^6.0
- symfony/phpunit-bridge: ^5.4|^6.0
- tomasvotruba/type-coverage: ^0.2
README
⚠️ 停产通知 ⚠️
警告
由于有专门的 Symfony 扩展包位于 fusonic/http-kernel-bundle
,因此已停用 fusonic/http-kernel-extensions
包(请参阅 fusonic/php-http-kernel-bundle)。
此包仅与及包含 Symfony 6.3 兼容,且不会接收任何其他功能更新。请考虑切换到新包以利用未来的更改。
请参阅 UPGRADE-1.0.md 以获取有关如何升级到新包的说明。
关于
此库包含了对 Symfony HttpKernel 组件 的各种扩展。以下将详细介绍此库提供的每个扩展及其工作方式。
目前主要开发在 Gitlab.com 的私有仓库进行。Github.com 上的项目会定期更新,但不包括 Gitlab 管理的问题。然而,我们也很高兴在 Github 上接受问题和拉取请求!请随意打开问题或合并请求。如果未来我们看到更广泛的社区参与,我们可能会考虑将主要开发转移到 Github。
安装
使用 composer 从 Packagist 安装库。
composer require fusonic/http-kernel-extensions
使用
RequestDtoResolver
在 Symfony 中存在一种称为 参数解析器 的东西。它们可以用于在动作被调用之前设置控制器动作参数的值。例如,存在 RequestValueResolver
,它将当前请求作为参数注入到被调用的动作中。与此类似,我们创建了我们的参数解析器,但它比仅仅注入对象做了更多的事情。
它做什么?
我们的 RequestDtoResolver
可以用于将请求数据直接映射到对象。您无需手动从请求中获取所有信息并将其放入对象或 - 天哪 - 传递通用数据数组,此类将利用 Symfony Serializer 将请求映射到对象,并使您能够拥有自定义对象来传输请求数据(即数据传输对象)从控制器到您的业务逻辑。此外,如果您设置了验证注释,它还将使用 Symfony Validation 验证生成的对象。
- 将对带有
Fusonic\HttpKernelExtensions\Attribute\FromRequest
属性 的参数执行映射。或者,此属性也可以设置在参数的类上(请参阅下面的示例)。 - 在序列化过程中将强制执行 PUT、POST、PATCH 和 DELETE 的强类型检查,并且如果请求体中的类型与 DTO 中期望的类型不匹配,则将导致错误。
- 对于所有其他请求,例如 GET 将作为查询参数始终以字符串形式传输,将禁用类型强制执行。
- 请求体将与PUT、POST、PATCH和DELETE请求的路由参数合并(在这种情况下将忽略查询参数)。
- 对于所有其他请求,查询参数将与路由参数合并(在这种情况下将忽略请求体)。
- 路由参数将始终覆盖具有相同名称的查询参数或请求体值。
- 将请求反序列化为对象之后,将进行验证。
- 当以下情况发生时,将抛出
BadRequestHttpException
异常:- 生成的DTO对象根据Symfony验证无效
- 请求体无法反序列化
- 请求包含无效的JSON
- 请求包含有效的JSON,但层次级别超过512
- 如果您正在使用ConstraintViolationErrorHandler错误处理器,当您的对象验证失败时,将抛出ConstraintViolationException异常。您还可以通过实现ErrorHandlerInterface来自定义自己的处理器。
- 根据给定的内容类型,它将解析请求体作为普通表单,或者如果设置了相应的内容类型,则将内容解析为JSON。
如何使用?
假设您正在使用完整的Symfony设置,您必须在您的services.yaml
中将解析器注册为服务,如下所示,以便由Symfony调用。
Fusonic\HttpKernelExtensions\Controller\RequestDtoResolver: tags: - { name: controller.argument_value_resolver, priority: 50 }
创建您的DTO,例如我们这里的UpdateFooDto
。所有验证内容都是可选的,但序列化器需要getters和setters。
// ... final class UpdateFooDto { #[Assert\NotNull] #[Assert\Positive] private int $id; #[Assert\NotBlank] private string $clientVersion; #[Assert\NotNull] private array $browserInfo; public function getClientVersion(): string { return $this->clientVersion; } public function setClientVersion(string $clientVersion): void { $this->clientVersion = $clientVersion; } public function getBrowserInfo(): array { return $this->browserInfo; } public function setBrowserInfo(array $browserInfo): void { $this->browserInfo = $browserInfo; } public function getId(): int { return $this->id; } public function setId(int $id): void { $this->id = $id; } }
参数属性
最后,将DTO添加到您的控制器操作中,使用RequestDtoArgument
。当然,路由参数也是可选的。
// ... use Fusonic\HttpKernelExtensions\Attribute\FromRequest; final class FooController extends AbstractController { /** * @Route("/{id}/update", methods={"POST"}, requirements={"id"="\d+"}) */ public function updateAction(#[FromRequest] UpdateFooDto $dto): Response { // do something with your $dto here } }
类属性
或者,您也可以将属性添加到DTO类本身,而不是控制器操作中的参数,如果您喜欢这种方式。
// ... use Fusonic\HttpKernelExtensions\Attribute\FromRequest; #[FromRequest] final class UpdateFooDto { // ... }
// ... final class FooController extends AbstractController { /** * @Route("/{id}/update", methods={"POST"}, requirements={"id"="\d+"}) */ public function updateAction(UpdateFooDto $dto): Response { // do something with your $dto here } }
解析和收集模型数据
默认情况下,任何json
或form
请求体类型将被相应解析。要覆盖此行为,您可以向Fusonic\HttpKernelExtensions\Request\RequestDataCollectorInterface
的实施数据收集器中注入自己的请求体解析器(通过实现Fusonic\HttpKernelExtensions\Request\BodyParser\RequestBodyParserInterface
),该数据收集器被注入到Fusonic\HttpKernelExtensions\Controller\RequestDtoResolver
中。在RequestDataCollectorInterface
中,您还可以修改如何以及从Request
对象中获取哪些值的行为。
错误处理
该扩展提供默认的错误处理器(http-kernel-extensions/src/ErrorHandler/ConstraintViolationErrorHandler.php
),该处理器处理常见的去规范化错误,这些错误应被视为类型错误。它将创建一个ConstraintViolationException异常,该异常可以与提供的ConstraintViolationExceptionNormalizer一起使用。此标准化器用于Symfony的内置Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer
,并通过一些额外信息增强它:一个errorCode
和一个messageTemplate
。这两个都很有用,可以用于在客户端解析验证错误。如果这不满足您的需求,您可以简单地通过实现Fusonic\HttpKernelExtensions\ErrorHandler\ErrorHandlerInterface
并传递它到RequestDtoResolver
来提供自己的错误处理器。
您必须像这样注册标准化器作为服务:
Fusonic\HttpKernelExtensions\Normalizer\ConstraintViolationExceptionNormalizer: arguments: - "@serializer.normalizer.constraint_violation_list" tags: - { name: serializer.normalizer }
使用异常监听器/订阅者
在Symfony中,您可以使用异常监听器或订阅者将ConstraintViolationException
最终转换为实际响应,使用Fusonic\HttpKernelExtensions\Normalizer\ConstraintViolationExceptionNormalizer
。例如:
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Fusonic\HttpKernelExtensions\Exception\ConstraintViolationException; use Symfony\Component\HttpKernel\Event\ExceptionEvent; final class ExceptionSubscriber implements EventSubscriberInterface { public function __construct(private NormalizerInterface $normalizer) { } public static function getSubscribedEvents(): array { return [ KernelEvents::EXCEPTION => 'onKernelException', ]; } public function onKernelException(ExceptionEvent $event): void { $throwable = $event->getThrowable(); if ($throwable instanceof ConstraintViolationException) { $data = $this->normalizer->normalize($throwable); $event->setResponse(new JsonResponse($data, 422)); } } }
请查看事件和事件监听器的详细信息。
上下文感知提供者
有些情况下,您希望在DTO中添加数据,但不是通过API的消费者,而是根据当前登录的用户,例如。您可以在控制器收到DTO后手动执行此操作,获取用户,为DTO设置用户,然后继续处理。由于您在DTO创建后设置它,您无法进行验证,还必须将其设置为可空。之后,您可能还需要在业务逻辑中进行一些额外的检查,以确保已设置所需的所有内容。
或者,您只需创建并注册一个提供者,实现(并测试)一次,然后完成。所有提供者都将由RequestDtoResolver
调用,检索所需数据以支持DTO,将其设置在DTO中,然后进行验证。当您在控制器中收到它时,它是完整且经过验证的。您如何做到这一点?
- 创建一个提供者并实现
ContextAwareProvideInterface
的两个方法。
<?php // ... final class UserIdAwareProvider implements ContextAwareProviderInterface { public function __construct(private UserProviderInterface $userProvider) { } public function supports(object $dto): bool { return $dto instanceof UserIdAwareInterface; } public function provide(object $dto): void { if(!($dto instanceof UserIdAwareInterface)){ throw new \LogicException('Object is no instance of '.UserIdAwareInterface::class); } $user = $this->userProvider->getUser(); $dto->withUserId($user->getId()); } }
- 创建一个接口以标记您支持的类并设置数据。
<?php //... interface UserIdAwareInterface { public function withUserId(int $id): void; }
- 在DTO中实现该接口。
- 最后,将提供者传递给解析器。如果您使用Symfony,您将在
services.yaml
中执行此操作,其外观类似于此。
#... _instanceof: Fusonic\HttpKernelExtensions\Provider\ContextAwareProviderInterface: tags: - {name: fusonic.http_kernel_extensions.context_aware_provider} Fusonic\HttpKernelExtensions\Controller\RequestDtoResolver: tags: - { name: controller.argument_value_resolver, priority: 50 } arguments: - '@serializer' - '@validator' - null - !tagged_iterator fusonic.http_kernel_extensions.context_aware_provider