Diversity w polskim IT
Iden Eal
Iden EalFreelance Full-stack Developer

Prostsza obsługa JSON w Symfony

Sprawdź, jak możesz przetwarzać żądania JSON w Symfony, by zaoszczędzić czas na deserializacji i walidacji parametrów.
20.07.20203 min
Prostsza obsługa JSON w Symfony

Czy czułeś się kiedyś jak małpa pisząca ciągle ten sam kod? Tak to właśnie wyglądało, gdy używałem Symfony 5 bez wsparcia FOSRestBundle. Dekodowanie treści żądań JSON i walidacja każdego pola dla każdej akcji moich kontrolerów były bardzo frustrujące. Tymczasem przy użyciu FOSRestBundle, który obsługuje nowszą wersję Symfony, zdecydowałem się stworzyć prosty, ale przydatny pakiet do automatycznej deserializacji i walidacji treści żądań o nazwie IdenealRequestContentConverterBundle.

Za kulisami, czyli jak działa mój pakiet

Podstawą jest tutaj rozszerzenie ParamConverters z SensioFrameworkExtraBundle. Są one sposobem na tworzenie obiektów i wstrzykiwanie ich jako argumenty do metod kontrolera.

<?php


namespace Ideneal\Bundle\RequestContentConverterBundle\Request\ParamConverter;


use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;


/**
 * Class ContentParamConverter
 *
 * @package Ideneal\Bundle\RequestContentConverterBundle\Request\ParamConverter
 */
class ContentParamConverter implements ParamConverterInterface
{
    use ParamConverterValidationTrait;

    /**
     * @var SerializerInterface
     */
    private $serializer;

    /**
     * @var ValidatorInterface
     */
    private $validator;

    /**
     * FormatParamConverter constructor.
     *
     * @param SerializerInterface $serializer
     * @param ValidatorInterface  $validator
     */
    public function __construct(SerializerInterface $serializer, ValidatorInterface $validator)
    {
        $this->serializer = $serializer;
        $this->validator  = $validator;
    }

    /**
     * @param Request        $request
     * @param ParamConverter $configuration
     *
     * @return bool|void
     */
    public function apply(Request $request, ParamConverter $configuration)
    {
        $name     = $configuration->getName();
        $class    = $configuration->getClass() ?: \stdClass::class;
        $options  = $configuration->getOptions();
        $format   = $options['format'];
        $groups   = isset($options['groups']) ? $options['groups'] : null;

        $object = $this->serializer->deserialize($request->getContent(), $class, $format, [
            'groups' => $groups,
        ]);

        if ($this->shouldValidate($options)) {
            $this->validate($object, $options);
        }

        $request->attributes->set($name, $object);

        return true;
    }

    /**
     * @param ParamConverter $configuration
     *
     * @return bool
     */
    public function supports(ParamConverter $configuration)
    {
        $options = $configuration->getOptions();
        return isset($options['format']);
    }
}


ContentParamConverter ma na celu deserializację treści żądania do wcześniej zdefiniowanego obiektu, a następnie zweryfikowanie go, jeśli wymagana jest walidacja. ContentParamConverter zostanie zastosowany tylko wtedy, gdy adnotacja ma opcję format. Aby kod był tak czysty, jak to tylko możliwe, zaimplementowałem także kilka adnotacji.

Zobaczmy, jak to działa

Musisz najpierw dodać pakiet do swojego projektu, wpisując następujące polecenie:

composer require ideneal/request-content-converter-bundle


Załóżmy, że tworzysz subskrypcję, aby uzyskać potencjalnych klientów dla swojej platformy. Musisz utworzyć klasę Lead oraz SubscribeController, do którego wyślemy Lead.

<?php

namespace App\Inputs;

use Symfony\Component\Validator\Constraints as Assert;

class Lead
{
    /**
     * @Assert\Type("string")
     * @Assert\NotBlank
     */
    private $name;

    /**
     * @Assert\Email
     */
    private $email;

    public function setName($name)
    {
        $this->name = $name;
    }
    
    public function getName()
    {
        return $this->name;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function getEmail()
    {
        return $this->email;
    }
}


Jak widać, Lead ma tylko dwie właściwości: imię i nazwisko oraz adres e-mail, które mają odpowiednio ograniczenia w postaci ciągu znaków i adresu e-mail. Nasz kontroler będzie więc wyglądał tak:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Ideneal\Bundle\RequestContentConverterBundle\Configuration\Json;
use App\Inputs\Lead;

class SubscribeController extends AbstractController
{
    /**
     * @Route("/subscribe", name="subscribe", methods={"POST"})
     * @Json("lead")
     */
    public function subscribe(Lead $lead)
    {
        /* Do some operations .... */
         
        dump($lead);
        return new JsonResponse(['message' => 'ok']);
    }
}


W metodzie subscribe użyliśmy adnotacji @Json, określającej format, w jakim zostanie przekazany Lead. ContentParamConverter deserializuje treść żądania JSON do obiektu Lead, sprawdza go (domyślnie walidacja ma wartość true) i umieszcza go w miejsce argumentu $lead. Możliwe jest również deserializowanie treści żądania do istniejącej encji za pomocą adnotacji @JsonEntity, jak poniżej:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Ideneal\Bundle\RequestContentConverterBundle\Configuration\JsonEntity;
use App\Entity\Product;

class ApiController extends AbstractController
{
    /**
     * @Route("/api/products/{id}", name="update_product", methods={"PUT"})
     * @JsonEntity("product", class="App\Entity\Product")
     */
    public function update(Product $product)
    {
        /* Do some operations... */

        dump($product);
        $em = $this->getDoctrine()->getManager();
        $em->flush();
        
        return new JsonResponse(['message' => 'ok']);
    }
}


Mamy tutaj API aktualizowania produktu, w którym produkt wewnątrz metody został właśnie załadowany z ORM, wypełniony z treści żądania oraz zwalidowany. Jest teraz zatem gotowy do zapisania i użycia.

Podsumowanie

FOSRestBundle obsługuje teraz również najnowszą wersję Symfony, ale rozwiązanie powyższego problemu było ekscytującym wyzwaniem, które ostatecznie zaoszczędziło mi sporo czasu!

Mam nadzieję, że artykuł Ci się podobał i był przydatny. Na koniec zostawiam Wam link do pakietu i życzę udanego kodowania!


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>