Virando请求没有Symfony框架
#网络开发人员 #php #api #symfony

今天,Symfony是市场上最成熟和最强大的框架之一,因此,它用于徒劳的项目,包括创建API。最近,Symfony包括法律功能,例如mapear os dados da Request para um Objeto的功能,该功能在6.3中出现。

这样,我们将利用PHP的最后一个版本的一些最佳功能,这是对attributesatributos readonly的支持,并为Symfony Repands创建验证。

为此,我们将使用Symfony Validation组件。

我没有病人,告诉我路!

好吧,好吧!如果您没有耐心地阅读本文,我将在下面的链接上实施本文的测试项目。

https://github.com/joubertredrat/symfony-request-validator

基本示例

在文档的重现之后,只需创建一个我们将用来映射请求值的类,如下示例。

<?php declare(strict_types=1);

namespace App\Dto;

use App\Validator\CreditCard;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Positive;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Type;

class CreateTransactionDto
{
    public function __construct(
        #[NotBlank(message: 'I dont like this field empty')]
        #[Type('string')]
        public readonly string $firstName,

        #[NotBlank(message: 'I dont like this field empty')]
        #[Type('string')]
        public readonly string $lastName,

        #[NotBlank()]
        #[Type('string')]
        #[CreditCard()]
        public readonly string $cardNumber,

        #[NotBlank()]
        #[Positive()]
        public readonly int $amount,

        #[NotBlank()]
        #[Type('int')]
        #[Range(
            min: 1,
            max: 12,
            notInRangeMessage: 'Expected to be between {{ min }} and {{ max }}, got {{ value }}',
        )]
        public readonly int $installments,

        #[Type('string')]
        public ?string $description = null,
    ) {
    }
}

这样,只需使用注释#[MapRequestPayload]并准备就绪的控制器,将值自动映射到对象。

<?php declare(strict_types=1);

namespace App\Controller;

use App\Dto\CreateTransactionDto;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;

class TransactionController extends AbstractController
{
    #[Route('/api/v1/transactions', name: 'app_api_create_transaction_v1', methods: ['POST'])]
    public function v1Create(#[MapRequestPayload] CreateTransactionDto $createTransaction): JsonResponse
    {
        return $this->json([
            'resonse' => 'ok',
            'datetime' => (new DateTimeImmutable('now'))->format('Y-m-d H:i:s'),
            'firstName' => $createTransaction->firstName,
            'lastName' => $createTransaction->lastName,
            'amount' => $createTransaction->amount,
            'installments' => $createTransaction->installments,
            'description' => $createTransaction->description,
        ]);
    }
}

这样,只需提出请求并查看结果即可。

curl --request POST \
  --url http://127.0.0.1:8001/api/v1/transactions \
  --header 'Content-Type: application/json' \
  --data '{
  "firstName": "Joubert",
  "lastName": "RedRat",
  "cardNumber": "4130731304267489",
  "amount": 35011757,
  "installments": 2
}'
< HTTP/1.1 200 OK
< Content-Type: application/json

{
  "resonse": "ok",
  "datetime": "2023-07-04 19:36:37",
  "firstName": "Joubert",
  "lastName": "RedRat",
  "cardNumber": "4130731304267489",
  "amount": 35011757,
  "installments": 2,
  "description": null
}

在上面的示例中,如果未根据定义规则正确填充值,则将触发一个例外,我们收到一个带有发现错误的答案。

Request error exception

部分这是ValidationFailedException标准的例外,当我们构建API时,需要一个json形状的响应。

考虑到这一点,我们可以尝试一种将要解释的方法。

抽象请求类

Symfony的巨大优势之一是通过其强大的注射依赖性容器在Autowire Support的情况下对“ Dependency inversion principle”的巨大支持。

这样,我们将创建我们的摘要类,该类将包含负责执行paraspit和验证的整个时间,如下示例。

<?php declare(strict_types=1);

namespace App\Request;

use Fig\Http\Message\StatusCodeInterface;
use Jawira\CaseConverter\Convert;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class AbstractJsonRequest
{
    public function __construct(
        protected ValidatorInterface $validator,
        protected RequestStack $requestStack,
    ) {
        $this->populate();
        $this->validate();
    }

    public function getRequest(): Request
    {
        return $this->requestStack->getCurrentRequest();
    }

    protected function populate(): void
    {
        $request = $this->getRequest();
        $reflection = new \ReflectionClass($this);

        foreach ($request->toArray() as $property => $value) {
            $attribute = self::camelCase($property);
            if (property_exists($this, $attribute)) {
                $reflectionProperty = $reflection->getProperty($attribute);
                $reflectionProperty->setValue($this, $value);
            }
        }
    }

    protected function validate(): void
    {
        $errors = $this->validator->validate($this);
        $messages = [];

        foreach ($errors as $message) {
            $messages[] = [
                'property' => self::snakeCase($message->getPropertyPath()),
                'value' => $message->getInvalidValue(),
                'message' => $message->getMessage(),
            ];
        }

        if (count($messages) > 0) {
            $response = new JsonResponse(['errors' => $messages], StatusCodeInterface::STATUS_BAD_REQUEST);
            $response->send();
            exit;
        }
    }

    private static function camelCase(string $attribute): string
    {
        return (new Convert($attribute))->toCamel();
    }

    private static function snakeCase(string $attribute): string
    {
        return (new Convert($attribute))->toSnake();
    }
}

在上面的类中,我们可以看到它接收ValidatorInterface和koud3作为依赖性,并且对构建器进行了属性的完成和验证。

©m,可以在属性和错误中看到snake_casecamelCase模式之间的对话,这是因为有一个约定,json的领域必须是snake_case,而PSR-2PSR-12建议camelCase5用于类属性名称,因此进行了此对话。为此,使用了Abiaoqian Library13。

por©m,值得记住的是,这不是绝对规则,如果您想使用与JSON中的snake_case不同的模式,则可以。

请求类带有验证属性的类

通过负责所有验证的摘要类,现在我们将创建验证类,如下示例。

<?php declare(strict_types=1);

namespace App\Request;

use App\Validator\CreditCard;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Positive;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Type;

class CreateTransactionRequest extends AbstractJsonRequest
{
    #[NotBlank(message: 'I dont like this field empty')]
    #[Type('string')]
    public readonly string $firstName;

    #[NotBlank(message: 'I dont like this field empty')]
    #[Type('string')]
    public readonly string $lastName;

    #[NotBlank()]
    #[Type('string')]
    #[CreditCard()]
    public readonly string $cardNumber;

    #[NotBlank()]
    #[Positive()]
    public readonly int $amount;

    #[NotBlank()]
    #[Type('int')]
    #[Range(
        min: 1,
        max: 12,
        notInRangeMessage: 'Expected to be between {{ min }} and {{ max }}, got {{ value }}',
    )]
    public readonly int $installments;

    #[Type('string')]
    public ?string $description = null;
}

上一类的最大优势在于,请求o的所有义务属性都是状态readonly,并且有可能确保数据的不变性。另一个有趣的观点是能够使用Symfony验证属性进行必要的验证,甚至创建自定义验证。

在路线上使用请求类

准备好课程,现在它将其用作您要进行验证的路由的依赖,值得记住的是,与上一个示例不同,这里不需要注释#[MapRequestPayload],例如下面的示例。

<?php declare(strict_types=1);

namespace App\Controller;

use App\Request\CreateTransactionRequest;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class TransactionController extends AbstractController
{
    #[Route('/api/v2/transactions', name: 'app_api_create_transaction_v2', methods: ['POST'])]
    public function v2Create(CreateTransactionRequest $request): JsonResponse
    {
        return $this->json([
            'resonse' => 'ok',
            'datetime' => (new DateTimeImmutable('now'))->format('Y-m-d H:i:s'),
            'first_name' => $request->firstName,
            'last_name' => $request->lastName,
            'amount' => $request->amount,
            'installments' => $request->installments,
            'description' => $request->description,
            'headers' => [
                'Content-Type' => $request
                    ->getRequest()
                    ->headers
                    ->get('Content-Type')
                ,
            ],
        ]);
    }
}

在上面的控制器中,有可能看到我们不使用传统的httppountation11,是的,我们的CreateTransactionRequest类是依赖的,并且有母亲,因为所有必要的依赖都将被注入,并将执行验证。<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< /p>

这种方法的优势

关于基本示例,此方法具有两个很大的优势。

  • - 可以根据需要自定义JSON的结构和状态响应代码。

  • - 可以访问已注入依赖的Smyfony请求类,因此可以从请求中访问任何信息,例如标题。在基本示例中,这是不可能的,除非您还将请求类放在路由上,这很奇怪,因为它将具有两个不同的数据源。 /p> < /li>

测试时间!

准备好实施后,让我们去测试。

示例的请求是有目的的错误,我们可以看到验证的答案。

curl --request POST \
  --url http://127.0.0.1:8001/api/v2/transactions \
  --header 'Content-Type: application/json' \
  --data '{
  "last_name": "RedRat",
  "card_number": "1130731304267489",
  "amount": -4,
  "installments": 16
}'
< HTTP/1.1 400 Bad Request
< Content-Type: application/json

{
  "errors": [
    {
      "property": "first_name",
      "value": null,
      "message": "I dont like this field empty."
    },
    {
      "property": "card_number",
      "value": "1130731304267489",
      "message": "Expected valid credit card number."
    },
    {
      "property": "amount",
      "value": -4,
      "message": "This value should be positive."
    },
    {
      "property": "installments",
      "value": 16,
      "message": "Expected to be between 1 and 12, got 16"
    }
  ]
}

正如我们所看到的,验证成功发生并且字段没有填充,或者没有正确的值没有通过验证,我们对验证的响应。P>>

现在,我们将进行一次调查,并确保回应将取得成功,因为所有字段都将在我们想要的内部。

curl --request POST \
  --url http://127.0.0.1:8001/api/v2/transactions \
  --header 'Content-Type: application/json' \
  --data '{
  "first_name": "Joubert",
  "last_name": "RedRat",
  "card_number": "4130731304267489",
  "amount": 35011757,
  "installments": 2
}'
< HTTP/1.1 200 OK
< Content-Type: application/json

{
  "resonse": "ok",
  "datetime": "2023-07-01 16:39:48",
  "first_name": "Joubert",
  "last_name": "RedRat",
  "card_number": "4130731304267489",
  "amount": 35011757,
  "installments": 2,
  "description": null
}

lima㧵

可选字段不能是readonly,就好像您想在没有该信息的情况下访问该信息一样,php将摄入异常。我现在正在使用这些情况的普通属性。

我仍在研究某些选项,以便能够在可选字段中使用readonly,例如使用反射和接受的建议:)

Exception readonly attribute not initialized

最后,我感谢好朋友Vinícius Dias的感谢,他帮助我回顾了本文。

这也是如此,下一个!