DTO 是封裝資料屬性而不包含任何業務邏輯的簡單物件。它們通常用於將多個來源的資料聚合到單一物件中,從而更易於管理和傳輸。透過使用 DTO,開發人員可以減少方法呼叫的數量、提高效能並簡化資料處理,尤其是在分散式系統或 API 中。
舉個例子,我們可以使用 DTO 來對應透過 HTTP 請求接收到的資料。這些 DTO 會將接收到的有效負載值保存到其屬性中,我們可以在應用程式中使用它們,例如,透過建立一個條令實體對象,準備將 DTO 中保存的資料持久保存到資料庫中。由於 DTO 資料已經經過驗證,因此可以降低資料庫持久化過程中產生錯誤的機率。
MapQueryString 和 MapRequestPayload 屬性允許我們分別映射接收到的查詢字串和負載參數。讓我們看一下兩者的範例。
假設我們有一個 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 類別。
在這種情況下,假設我們有一個 Symfony 路由,它接收在 JSON 請求負載中註冊新用戶所需的資料。這些參數如下:
基於上述參數,我們希望將它們對應到以下 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 編碼的有效負載。
如果在映射過程中驗證失敗(例如,強制電子郵件尚未發送),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 takes two dependencies: The Validator service for validating objects and a the Serializer service for denormalizing data into objects.
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 is responsible for resolving the value of an argument based on the request and its metadata.
The onKernelControllerArguments method is called when the CONTROLLER_ARGUMENTS event is triggered.
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 }
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中文網其他相關文章!