Cara mudah untuk mengesahkan DTO menggunakan atribut Symfony

Susan Sarandon
Lepaskan: 2024-09-21 20:16:02
asal
315 orang telah melayarinya

An easy way to validate DTO

pengenalan

DTO ialah objek ringkas yang merangkum atribut data tanpa mengandungi sebarang logik perniagaan. Ia sering digunakan untuk mengagregat data daripada pelbagai sumber ke dalam satu objek, menjadikannya lebih mudah untuk diurus dan dihantar. Dengan menggunakan DTO, pembangun boleh mengurangkan bilangan panggilan kaedah, meningkatkan prestasi dan memudahkan pengendalian data, terutamanya dalam sistem atau API teragih.

Sebagai contoh, kami boleh menggunakan DTO untuk memetakan data yang diterima melalui Permintaan HTTP. DTO tersebut akan menyimpan nilai muatan yang diterima pada harta mereka dan kami boleh menggunakannya dalam aplikasi, contohnya, dengan mencipta objek entiti doktrin yang sedia untuk disimpan ke pangkalan data daripada data yang disimpan dalam DTO. Memandangkan data DTO sudah pun disahkan, ia boleh mengurangkan kebarangkalian menjana ralat semasa pangkalan data berterusan.

Atribut MapQueryString dan MapRequestPayload

Atribut MapQueryString dan MapRequestPayload membolehkan kami masing-masing memetakan rentetan pertanyaan dan parameter muatan yang diterima. Mari lihat contoh kedua-duanya.

Contoh MapQueryString

Mari bayangkan kita mempunyai laluan Symfony yang boleh menerima parameter berikut dalam rentetan pertanyaan:

  • dari: Wajib dari tarikh
  • kepada: Wajib setakat ini
  • umur: Umur pilihan

Berdasarkan parameter di atas, kami ingin memetakannya ke dto berikut:

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 
   ){}
}
Salin selepas log masuk

Untuk memetakannya, kita hanya perlu menggunakan atribut MapQueryString mengikut cara ini:

#[Route('/my-get-route', name: 'my_route_name', methods: ['GET'])]
public function myAction(#[MapQueryString] QueryInputDTO $queryInputDto) 
{
   // your code
}
Salin selepas log masuk

Seperti yang anda lihat, apabila symfony mengesan bahawa argumen $queryInputDto telah dibenderakan dengan atribut #[MapQueryString], ia secara automatik memetakan rentetan pertanyaan yang diterima parameter ke dalam argumen itu yang merupakan contoh bagi Kelas QueryInputDTO.

Contoh MapRequestPayload

Dalam kes ini, mari bayangkan kita mempunyai laluan Symfony yang menerima data yang diperlukan untuk mendaftarkan pengguna baharu dalam muatan permintaan JSON. Parameter tersebut adalah seperti berikut:

  • nama: wajib
  • e-mel: wajib
  • tarikh lahir (dob): wajib

Berdasarkan parameter di atas, kami ingin memetakannya ke dto berikut:

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 
    ){}
 }
Salin selepas log masuk

Untuk memetakannya, kita hanya perlu menggunakan atribut MapRequestPayload mengikut cara ini:

#[Route('/my-post-route', name: 'my_post_route', methods: ['POST'])]
public function myAction(#[MapRequestPayload] PayloadInputDTO $payloadInputDto) 
{
   // your code
}
Salin selepas log masuk

Seperti yang telah kita lihat dalam bahagian sebelumnya, apabila symfony mengesan bahawa argumen $payloadInputDto telah dibenderakan dengan atribut #[MapRequestPayload], ia secara automatik memetakan parameter muatan yang diterima ke dalam hujah tersebut iaitu contoh kelas PayloadInputDTO.

MapRequestPayload berfungsi untuk muatan JSON dan muatan berkod url borang.

Mengendalikan ralat pengesahan DTO

Jika pengesahan gagal semasa proses pemetaan (contohnya, e-mel mandatori belum dihantar) Symfony membuang pengecualian 422 Kandungan Tidak Boleh Diproses. Jika anda ingin menangkap jenis pengecualian ini dan mengembalikan ralat pengesahan sebagai, contohnya, json kepada klien, anda boleh membuat pelanggan acara dan terus mendengar acara 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));
            }
        }
    }
}
Salin selepas log masuk

Selepas kaedah onException dicetuskan, ia menyemak sama ada pengecualian acara ialah tika UnprocessableEntityHttpException. Jika ya, ia juga menyemak sama ada ralat yang tidak boleh diproses datang daripada pengesahan yang gagal yang memeriksa sama ada pengecualian sebelumnya ialah contoh kelas ValidationFailedException. Jika ya, ia menyimpan semua ralat pelanggaran dalam tatasusunan (hanya laluan sifat sebagai kunci dan mesej pelanggaran sebagai ralat), mencipta respons JSON daripada ralat ini dan menetapkan respons baharu kepada acara tersebut.

Imej berikut menunjukkan respons JSON untuk permintaan yang gagal kerana e-mel belum dihantar:

@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"
  }
]
Salin selepas log masuk

Permintaan imej di atas telah dijana menggunakan fail http.

Mencipta penyelesai tersuai anda

Mari bayangkan kita mempunyai beberapa laluan yang menerima parameter rentetan pertanyaan ke dalam tatasusunan bernama "f". Sesuatu seperti ini:

/my-get-route?f[from]=2024-08-20 16:24:08&f[to]=&f[age]=14
Salin selepas log masuk

Kami boleh mencipta penyelesai tersuai untuk menyemak tatasusunan itu dalam permintaan dan kemudian mengesahkan data. Jom kodkan.

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);
    }
}
Salin selepas log masuk

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
}
Salin selepas log masuk

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

Atas ialah kandungan terperinci Cara mudah untuk mengesahkan DTO menggunakan atribut Symfony. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!