使用 Symfony 屬性驗證 DTO 的簡單方法

Susan Sarandon
發布: 2024-09-21 20:16:02
原創
322 人瀏覽過

An easy way to validate DTO

介紹

DTO 是封裝資料屬性而不包含任何業務邏輯的簡單物件。它們通常用於將多個來源的資料聚合到單一物件中,從而更易於管理和傳輸。透過使用 DTO,開發人員可以減少方法呼叫的數量、提高效能並簡化資料處理,尤其是在分散式系統或 API 中。

舉個例子,我們可以使用 DTO 來對應透過 HTTP 請求接收到的資料。這些 DTO 會將接收到的有效負載值保存到其屬性中,我們可以在應用程式中使用它們,例如,透過建立一個條令實體對象,準備將 DTO 中保存的資料持久保存到資料庫中。由於 DTO 資料已經經過驗證,因此可以降低資料庫持久化過程中產生錯誤的機率。

MapQueryString 和 MapRequestPayload 屬性

MapQueryString 和 MapRequestPayload 屬性允許我們分別映射接收到的查詢字串和負載參數。讓我們看一下兩者的範例。

MapQueryString 範例

假設我們有一個 Symfony 路由,它可以在查詢字串中接收以下參數:

  • 來自:必填的起始日期
  • :必填日期
  • 年齡:可選年齡

基於上述參數,我們希望將它們對應到以下 dto:

readonly class QueryInputDto {
   public function __construct(
      #[Assert\Datetime(message: 'From must be a valid datetime')]
      #[Assert\NotBlank(message: 'From date cannot be empty')]
      public string $from,
      #[Assert\Datetime(message: 'To must be a valid datetime')]
      #[Assert\NotBlank(message: 'To date cannot be empty')]
      public string $to,
      public ?int $age = null 
   ){}
}
登入後複製

要映射它們,我們只需按照以下方式使用 MapQueryString 屬性:

#[Route('/my-get-route', name: 'my_route_name', methods: ['GET'])]
public function myAction(#[MapQueryString] QueryInputDTO $queryInputDto) 
{
   // your code
}
登入後複製

如您所見,當symfony 偵測到參數$queryInputDto 已被標記為#[MapQueryString] 屬性時,它會自動將接收到的查詢字串參數對應到該參數,該參數是QueryInputDTO 類別。

MapRequestPayload 範例

在這種情況下,假設我們有一個 Symfony 路由,它接收在 JSON 請求負載中註冊新用戶所需的資料。這些參數如下:

  • 姓名:必填
  • 電子郵件:必填
  • 出生日期(dob):必填

基於上述參數,我們希望將它們對應到以下 dto:

readonly class PayloadInputDto {
    public function __construct(
       #[Assert\NotBlank(message: 'Name cannot be empty')] 
       public string $name,
       #[Assert\NotBlank(message: 'Email cannot be empty')]
       #[Assert\Email(message: 'Email must be a valid email')]
       public string $email,
       #[Assert\NotBlank(message: 'From date cannot be empty')]
       #[Assert\Date(message: 'Dob must be a valid date')]
       public ?string $dob = null 
    ){}
 }
登入後複製

要映射它們,我們只需使用 MapRequestPayload 屬性,如下所示:

#[Route('/my-post-route', name: 'my_post_route', methods: ['POST'])]
public function myAction(#[MapRequestPayload] PayloadInputDTO $payloadInputDto) 
{
   // your code
}
登入後複製

如我們在上一節所看到的,當symfony 偵測到參數$payloadInputDto 已被標記為#[MapRequestPayload] 屬性時,它會自動將接收到的有效負載參數對應到在該參數中,即PayloadInputDTO 類別的實例。

MapRequestPayload 適用於 JSON 有效負載和表單 url 編碼的有效負載。

處理 DTO 驗證錯誤

如果在映射過程中驗證失敗(例如,強制電子郵件尚未發送),Symfony 會拋出 422 Unprocessable Content 異常。如果您想捕獲此類異常並將驗證錯誤以 json 等形式傳回給用戶端,您可以建立事件訂閱者並繼續監聽 KernelException 事件。

class KernelSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::EXCEPTION => 'onException'
        ];
    }

    public function onException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        if($exception instanceof UnprocessableEntityHttpException) {
            $previous = $exception->getPrevious();
            if($previous instanceof ValidationFailedException) {
                $errors = [];
                foreach($previous->getViolations() as $violation) {
                    $errors[] = [
                        'path' => $violation->getPropertyPath(),
                        'error' => $violation->getMessage() 
                    ];
                }

                $event->setResponse(new JsonResponse($errors));
            }
        }
    }
}
登入後複製

觸發onException方法後,它會檢查事件異常是否是UnprocessableEntityHttpException的實例。如果是這樣,它還會檢查不可處理的錯誤是否來自驗證失敗,檢查先前的異常是否是 ValidationFailedException 類別的實例。如果是這樣,它將所有違規錯誤儲存在一個陣列中(僅屬性路徑作為鍵,違規訊息作為錯誤),根據這些錯誤建立 JSON 回應,並將新回應設為事件。

下圖顯示了由於電子郵件尚未發送而失敗的請求的 JSON 回應:

@baseUrl = http://127.0.0.1:8000

POST {{baseUrl}}/my-post-route
Content-Type: application/json

{
    "name" : "Peter Parker",
    "email" : "",
    "dob" : "1990-06-28"
}

-------------------------------------------------------------
HTTP/1.1 422 Unprocessable Entity
Cache-Control: no-cache, private
Content-Type: application/json
Date: Fri, 20 Sep 2024 16:44:20 GMT
Host: 127.0.0.1:8000
X-Powered-By: PHP/8.2.23
X-Robots-Tag: noindex
Transfer-Encoding: chunked

[
  {
    "path": "email",
    "error": "Email cannot be empty"
  }
]
登入後複製

以上的圖像請求是使用http檔產生。

建立您的自訂解析器

假設我們有一些路由將查詢字串參數接收到名為「f」的陣列中。像這樣的東西:

/my-get-route?f[from]=2024-08-20 16:24:08&f[to]=&f[age]=14
登入後複製

我們可以建立一個自訂解析器來檢查請求中的該數組,然後驗證資料。讓我們來編碼吧。

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class CustomQsValueResolver implements ValueResolverInterface, EventSubscriberInterface
{
    public function __construct(
        private readonly ValidatorInterface $validator,
        private readonly SerializerInterface $serializer
    ){}

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments',
        ];
    }

    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        $attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;

        if (!$attribute) {
            return [];
        }

        if ($argument->isVariadic()) {
            throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
        }

        $attribute->metadata = $argument;
        return [$attribute];
    }

    public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
    {
        $arguments = $event->getArguments();
        $request  = $event->getRequest();

        foreach ($arguments as $i => $argument) {
            if($argument instanceof MapQueryString ) {
                $qs = $request->get('f', []);
                if(count($qs) > 0) {
                    $object = $this->serializer->denormalize($qs, $argument->metadata->getType());
                    $violations = $this->validator->validate($object);
                    if($violations->count() > 0) {
                        $validationFailedException = new ValidationFailedException(null, $violations);
                        throw new UnprocessableEntityHttpException('Unale to process received data', $validationFailedException);
                    }

                    $arguments[$i] = $object;
                }
            }

        }

        $event->setArguments($arguments);
    }
}
登入後複製

The CustomQsValueResolver implements the ValueResolverInterface and the EventSubscriberInterface, allowing it to resolve arguments for controller actions and listen to specific events in the Symfony event system. In this case, the resolver listens to the Kernel CONTROLLER_ARGUMENTS event.
Let's analyze it step by step:

The constructor

The constructor takes two dependencies: The Validator service for validating objects and a the Serializer service for denormalizing data into objects.

The getSubscribedEvents method

The getSubscribedEvents method returns an array mapping the KernelEvents::CONTROLLER_ARGUMENTS symfony event to the onKernelControllerArguments method. This means that when the CONTROLLER_ARGUMENTS event is triggered (always a controller is reached), the onKernelControllerArguments method will be called.

The resolve method

The resolve method is responsible for resolving the value of an argument based on the request and its metadata.

  • It checks if the argument has the MapQueryString attribute. If not, it returns an empty array.
  • If the argument is variadic, that is, it can accept a variable number of arguments, it throws a LogicException, indicating that mapping variadic arguments is not supported.
  • If the attribute is found, it sets the metadata property of the attribute and returns it as a php iterable.

The onKernelControllerArguments method

The onKernelControllerArguments method is called when the CONTROLLER_ARGUMENTS event is triggered.

  • It retrieves the current arguments and the request from the event.
  • It iterates over the arguments, checking for arguments flagged as MapQueryString
  • If found, it retrieves the query string parameters holded by the "f" array using $request->get('f', []).
  • If there are parameters, it denormalizes them into an object of the type specified in the argument's metadata (The Dto class).
  • It then validates the object using the validator. If there are validation violations, it throws an UnprocessableEntityHttpException which wraps a ValidationFailedException with the validation errors.
  • If validation passes, it replaces the original argument with the newly created object.

Using the resolver in the controller

To instruct the MapQueryString attribute to use our recently created resolver instead of the default one, we must specify it with the attribute resolver value. Let's see how to do it:

#[Route('/my-get-route', name: 'my_route_name', methods: ['GET'])]
public function myAction(#[MapQueryString(resolver: CustomQsValueResolver::class)] QueryInputDTO $queryInputDto) 
{
   // your code
}
登入後複製

Conclusion

In this article, we have analized how symfony makes our lives easier by making common application tasks very simple, such as receiving and validating data from an API. To do that, it offers us the MapQueryString and MapRequestPayload attributes. In addition, it also offers us the possibility of creating our custom mapping resolvers for cases that require specific needs.

If you like my content and enjoy reading it and you are interested in learning more about PHP, you can read my ebook about how to create an operation-oriented API using PHP and the Symfony Framework. You can find it here: Building an Operation-Oriented Api using PHP and the Symfony Framework: A step-by-step guide

以上是使用 Symfony 屬性驗證 DTO 的簡單方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板