baptiste-contreras/symfony-request-param-bundle

Symfony 包,可使用对象请求参数

0.0.1 2022-12-03 21:59 UTC

This package is auto-updated.

Last update: 2024-09-10 00:28:22 UTC


README

目标

本包旨在在PHP的Symfony中重现Java Spring的 Request Param注解

使用此包,您可以使用PHP 8.1的本地属性来获得所需的结果

#[Route('/demo', name: 'demo_')]
class RegisterController extends AbstractApiController
{
    #[Route(path: '/{uid}', name: 'register', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function register(#[DtoRequestParam] RegisterRequest $registerRequest, ?string $uid = null): Response
    {
        dd($registerRequest);
    }
}

在我们的例子中,$registerRequest 对象将使用请求中的数据构建并验证。

安装

composer require baptiste-contreras/symfony-request-param-bundle

DtoRequestParam 参数

DtoRequestParam 提供了几个参数,并允许您修改DTO注入的行为。

  • sourceType
  • throwDeserializationException
  • validateDto
  • validationGroups

sourceType

  • string sourceType. 默认值 SourceType::JSON。这允许您指示输入数据类型。

更改此值时,必须确保存在一个可以支持该类型sourceType的 DtoProviderDriverInterface。否则,您将收到一个 NoDtoProviderDriverFoundException

此文档中稍后将对打包的sourceType进行详细说明。

示例

#[Route('/demo', name: 'demo_')]
class RegisterController extends AbstractApiController
{
    #[Route(path: '/', name: 'register', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function register(#[DtoRequestParam(sourceType: SourceType::JSON)] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest);
    }
    
    #[Route(path: '/xml', name: 'register_xml', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function registerXml(#[DtoRequestParam(sourceType: 'xml')] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest);
    }
}

throwDeserializationException

  • bool throwDeserializationException. 默认值 true

如果为 true,则在反序列化阶段发生的任何异常都不会被捕获,并将重新抛出。如果您将该参数设置为 false,则反序列化过程中发生的异常将被捕获、记录,并将 null 注入到DTO中。

示例

#[Route('/demo', name: 'demo_')]
class RegisterController extends AbstractApiController
{
    #[Route(path: '/', name: 'register', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function register(#[DtoRequestParam(throwValidationException: true)] RegisterRequest $registerRequest): Response
    {
        // If something went bad during the deserialization, the exception is rethrown and this code will not be called...
        dd($registerRequest);
    }
    
    #[Route(path: '/test2', name: 'test2', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function test2(#[DtoRequestParam(throwValidationException: false)] ?RegisterRequest $registerRequest): Response
    {
        // Notice the type difference with the first method, we add "?RegisterRequest" because $registerRequest
        // will be null if there is a problem during the deserialization.
        dd($registerRequest); 
    }
}

validateDto

  • bool validateDto. 默认值 true

如果为 true,则将执行验证阶段,使用 Symfony的验证器。如果存在约束违反情况,则包将抛出自定义异常并处理错误格式化和显示(稍后详细介绍)。

如果为 false,则不会进行验证,您的DTO将在反序列化后直接注入到控制器的方法中。

要设置您的验证约束,您可以使用官方的 Symfony文档,但这里有一些简要说明。

final class RegisterRequest
{
    #[NotBlank]
    private ?string $name = null;
    
    #[Positive]
    private ?int $age = null;
    
    // getters, setters, ...
}

示例

#[Route('/demo', name: 'demo_')]
class RegisterController extends AbstractApiController
{
    #[Route(path: '/', name: 'register', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function register(#[DtoRequestParam(validateDto: true)] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest); // My DTO is validated
    }
    
    #[Route(path: '/test2', name: 'test2', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function registerXml(#[DtoRequestParam(validateDto: false)] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest); // No validation
    }
}

validationGroups

  • array|string validationGroups. 默认值 ['Default']

由于此包内部使用Symfony的验证器,我们可以指定验证组以仅验证约束子集。您可以在这里了解更多信息。

您可以传递单个字符串,表示一个验证组,或者如果您想使用多个验证组,则可以传递字符串数组。

请注意,如果 validateDtotrue,则不能传递空数组或空字符串 ([] 或 ''),否则您将收到一个 EmptyValidationGroupsException

示例

#[Route('/demo', name: 'demo_')]
class RegisterController extends AbstractApiController
{
    #[Route(path: '/', name: 'register', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function register(#[DtoRequestParam(validationGroups: 'register-validation-1')] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest); 
    }
    
    #[Route(path: '/test2', name: 'test2', methods: ['POST'])]
    #[AutoProvideRequestDto]
    public function registerXml(#[DtoRequestParam(validationGroups: ['register-validation-1', 'register-validation-2'])] RegisterRequest $registerRequest): Response
    {
        dd($registerRequest); 
    }
}

可用来源

目前仅支持 json 来源。您可以通过创建一个针对您需求的 DtoProviderDriverInterface 来扩展此包。

当您的自定义提供者的 supports 方法被调用并返回 true 时,它将被选中用于反序列化。

以下是一个XML提供者的示例。

class CustomXmlProvider implements DtoProviderDriverInterface
{
    public function __construct(private readonly SerializerInterface $serializer)
    {
    }

    public function fromRequest(DtoProviderContext $context, Request $request): mixed
    {
        try {
            // The Symfony's serialize is used here but feel free to handle the raw data your way ! 
            return $this->serializer->deserialize($request->getContent(), $context->getDtoClass(), 'xml', []);
        } catch (\Throwable $exception) {
            // This is optional, but you should do it, otherwise the 
            // throwDeserializationException parameter will be useless...
            if ($context->shouldThrowDeserializationException()) {
                throw $exception;
            }

            return null;
        }
    }

    public function supports(DtoProviderContext $dtoProviderContext): bool
    {
        return 'xml' === $dtoProviderContext->getSourceType(); // You can add more logic if needed
    }
}

如果您想确保它首先被调用,您可以调整您自定义提供者的标签优先级。有关文档,请在此处查看。

request_param.dto-provider-driver 是与 DtoProviderDriverInterface 相关的标签。

以下是一个修改自定义提供者优先级的示例

# services.yaml

    App\CustomProvider:
        class: 'App\CustomProvider'
        tags:
            - { name: "request_param.dto-provider-driver", priority: 20 }

JsonDtoProvider

如果您指定 SourceType::JSON 作为源类型,将使用 JsonDtoProvider 服务。

内部使用 Symfony 的序列化器

目前使用一个非常基本的序列化器配置,并使用设置器填充您的 DTO。

稍后您将能够更改这一点。(实际上,您可以通过配置序列化器组件来更改它,更多信息请在此处查看

错误处理

展示者

错误展示者是负责以预定义格式返回响应的服务。例如 JSON 错误格式

{
  "error": true,
  "status": 400,
  "message": "Bad request"
}

如果您想添加自己的错误展示者,该展示者返回自定义格式,只需创建一个实现 ErrorPresenterDriverInterface 的对象即可。

像上面的自定义提供者一样,不需要采取任何进一步的操作(即,如果您的自定义错误展示者的 supports 方法返回 true,它将被使用!)。

您必须意识到,首先注册的错误处理器,其返回值为 true,将被使用。对于提供者而言,您可以通过标签优先级来调整您的处理器在选择链中的位置,以下是相关文档

request_param.error-presenter-driver 是与 ErrorPresenterDriverInterface 相关的标签。

以下是一个修改自定义提供者优先级的示例

# services.yaml

    App\CustomErrorPresenter:
        class: 'App\CustomErrorPresenter'
        tags:
            - { name: "request_param.error-presenter-driver", priority: 20 }

当您创建自己的展示者时,除了 supports 方法外,您必须实现两个重要方法

  • presentBadRequest:在验证异常或 HTTP 400 错误时被调用
  • presentTechnicalError:在任何其他情况下被调用

以下是一个自定义基本 HTML 错误展示者的示例

class BasicHtmlErrorPresenter implements ErrorPresenterDriverInterface
{

    public function presentBadRequest(RequestDtoException $requestDtoException, Request $request): Response
    {
        return new Response('<html><body><h1>Bad request</h1></body></html>', 400);
    }

    public function presentTechnicalError(RequestDtoException $requestDtoException, Request $request): Response
    {
        return return new Response('<html><body><h1>Technical error</h1></body></html>', 500);
    }

    public function supports(RequestDtoException $requestDtoException, Request $request): bool
    {
        return $request->headers->has('....'); // Your logic here
    }
}

JsonErrorPresenter

默认情况下,JsonErrorPresenter 服务将用于返回包含错误详细信息的 JSON 响应。

以下是两个响应示例

  • 给定一个无效请求,它将产生
{
  "error": true,
  "success": false,
  "message": "Bad request",
  "status": 400,
  "errors": [
    "[property_1] : Should not be blank"
  ]
}
  • 给定任何其他错误,结果将是
{
  "error": true,
  "success": false,
  "message": "Technical error",
  "status": 500
}

您可以轻松地修改此响应格式。实际上,此展示者使用装饰器堆栈来创建响应数组。

Symfony 的装饰器

Symfony 的装饰器堆栈

JsonErrorPresenter::presentBadRequest 使用 stack_response_formatter_json_bad_request 堆栈。JsonErrorPresenter::presentTechnicalError 使用 stack_response_formatter_json_technical_error 堆栈。

上述两个堆栈都是一系列 JsonFormatterInterface 逐个应用的。

让我们看看 stack_response_formatter_json_bad_request 的定义

<stack id="stack_response_formatter_json_bad_request">
    <service parent="stack_response_formatter_json_bad_request_default"/> <!--  It's a little trick to easily append a new formatter to the defaults defined in stack_response_formatter_json_bad_request_default -->
</stack>

<stack id="stack_response_formatter_json_bad_request_default">
    <service alias="request_param.response.formatter.json.validation" />
    <service alias="request_param.response.formatter.json.default" />
</stack>

让我们创建一个新的格式化器来添加一个 "test": "ok" 键,并假设移除 "success": false

class CustomJsonFormatter implements JsonFormatterInterface
{
    // $this->decorated is the next formatter in the chain (i.e. the one we decorate with our custom formatter)
    // It can be null if our formatter is the last to be called
    // the order depends on the stack definition you made in your services.yaml
    public function __construct(private readonly ?JsonFormatterInterface $decorated = null)
    {
    }

    public function format(array $currentResponse, RequestDtoException $requestDtoException, Request $request, string $defaultMessage, int $httpCode): array
    {
        $currentResponse['test'] = 'ok';

        if ($this->decorated) {
            $currentResponse = $this->decorated->format(
                $currentResponse, $requestDtoException, $request, $defaultMessage, $httpCode
            );
            
            unset($currentResponse['success']);
        }

        return $currentResponse;
    }
}

我们需要在 services.yaml 中添加一些额外的配置

  App\CustomJsonFormatter:
    class: 'App\CustomJsonFormatter'

  
  stack_response_formatter_json_bad_request:
    stack:
      - App\CustomJsonFormatter: ~
      - alias: stack_response_formatter_json_bad_request_default  
      # In this configuration, our formatter is the first in the chain, and we include the default chain
      # stack_response_formatter_json_bad_request_default is an alias for request_param.response.formatter.json.validation AND request_param.response.formatter.json.default  

然后就是

{
  "test": "ok",
  "error": true,
  "success": false,
  "message": "Bad request",
  "errors": [
    "[property_1] : Should not be blank"
  ]
}

使用这种装饰器方法,您真的可以很容易地自定义 JSON 响应

  App\CustomJsonFormatter:
    class: 'App\CustomJsonFormatter'

  
  stack_response_formatter_json_bad_request:
    stack:
      - App\CustomJsonFormatter: ~
      - alias: request_param.response.formatter.json.validation
      # In this configuration, our formatter is the first in the chain, and we only include the "validation formatter"
    

将产生

{
  "test": "ok",
  "errors": [
    "[property_1] : Should not be blank"
  ]
}

默认情况下,request_param.response.formatter.json.validation 负责上面的示例中的 errors 键,而 request_param.response.formatter.json.default 负责其他键。