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.