今天,Symfony是世界上最成熟的PHP框架之一,因此,它用于包括APIS Creation在内的各种项目中。最近,Symfony包含了各种很酷的功能,例如mapping request data to typed objects,它出现在6.3版中。
这样,我们将利用PHP的最后版本中的一些最佳资源,这些资源为attributes和readonly properties提供了支持,并为Symfony的请求创建验证。
为此,我们将使用Symfony Validation组件。
我没有耐心,给我看代码!
好吧,好吧!如果您没有耐心阅读此帖子,我将在下面的链接中进行此帖子的实现。
https://github.com/joubertredrat/symfony-request-validation
基本示例
在Symfony文档之后,我们只需要创建一个将用于从请求中映射值的类,如下面的示例。
<?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([
'response' => '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
{
"response": "ok",
"datetime": "2023-07-04 19:36:37",
"firstName": "Joubert",
"lastName": "RedRat",
"cardNumber": "4130731304267489",
"amount": 35011757,
"installments": 2,
"description": null
}
在上面的示例中,如果值未正确填充,则会列出异常,我们将收到带有发现错误的响应。
问题是这个例外是默认的ValidationFailedException
,但是当我们构建API时,有必要以JSON格式进行响应。
考虑到这一点,我们可以尝试一种不同的方法,这将被解释。
抽象请求类
Symfony的最大优势之一是您强大的依赖注入容器在AutowRire支持的情况下对“ Dependency inversion principle”的大量支持。
这样,我们将创建我们的摘要类,所有代码将负责进行请求和验证的分析,如下示例。
<?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
{
$violations = $this->validator->validate($this);
if (count($violations) < 1) {
return;
}
$errors = [];
/** @var \Symfony\Component\Validator\ConstraintViolation */
foreach ($violations as $violation) {
$attribute = self::snakeCase($violation->getPropertyPath());
$errors[] = [
'property' => $attribute,
'value' => $violation->getInvalidValue(),
'message' => $violation->getMessage(),
];
}
$response = new JsonResponse(['errors' => $messages], 400);
$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
和RequestStack
视为依赖项,并在构造函数中执行属性的填充和验证。
另外,有可能在属性和错误中看到样式snake_case
和camelCase
之间的转换,这发生了,这是因为存在一个惯例,即JSON中的字段必须是snake_case
,而PSR-2和PSR-12建议camelCase
的名称for camelCase
在类中的属性,然后有必要转换。因为这被使用了Case converter lib。
但是,请记住这不是绝对规则,如果您想在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;
}
上面类的最大优势是请求的所有必需属性都具有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([
'response' => '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')
,
],
]);
}
}
在上面的控制器中,有可能看到我们没有从httpfoundation中使用传统的Request
,而不是这样,而是我们的类CreateTransactionRequest
是依赖性,这是因为所有类依赖性都会注入并完成验证。
这种方法的优势
与基本示例相比,此方法具有2个优点。
-
可以根据需要自定义响应的JSON结构和状态代码。
-
可以从Symfony访问
Request
类,而Symfony被注入依赖项,因此可以从请求中访问任何信息,例如标题。在基本示例中,这是不可能的
这是测试时间!
准备好实施后,让我们去测试。
请求示例是有意的错误,以便我们看到验证响应。
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"
}
]
}
正如我们所看到的,验证发生在成功,字段未填写或错误的数据没有通过验证,我们得到了验证响应。
现在,让我们做一个有效的请求,看看这将在响应中取得成功,因为所有字段都会按照我们的意愿填写。
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
{
"response": "ok",
"datetime": "2023-07-01 16:39:48",
"first_name": "Joubert",
"last_name": "RedRat",
"card_number": "4130731304267489",
"amount": 35011757,
"installments": 2,
"description": null
}
限制
可选字段不能为readonly
,因为如果您想在没有初始化的情况下访问数据,则PHP会引发异常。然后,目前,我正在使用此情况的普通字段。
我仍在寻找任何选项作为能够在可选字段中使用readonly
的解决方案,例如使用反射,我接受建议。
最后,我感谢我的好朋友Vinicius Dias帮助我修订了这篇文章。