Handling imporper data during deserialization when using Symfony Serializer Component - symfony

I am new to the Symfony serializer component. I am trying to properly deserialize a JSON body to the following DTO:
class PostDTO
{
/** #var string */
private $name;
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
}
The controller method as follows:
/**
* #Route (path="", methods={"POST"}, name="new_post")
* #param Request $request
* #return Response
*/
public function create(Request $request): Response
{
$model = $this->serializer->deserialize($request->getContent(), PostDTO::class, 'json');
// call the service with the model
return new JsonResponse();
}
My problem is that I wanted to handle business-validation after the body was deserialized. However, if i specify an invalid value for the name, such as false or [], the deserialization will fail with an exception: Symfony\Component\Serializer\Exception\NotNormalizableValueException: "The type of the "name" attribute for class "App\Service\PostDTO" must be one of "string" ("array" given)..
I do understand that it is because I intentionally set "name": []. However, I was looking for a way to set the fields to a default value or even perform some validation pre-deserialization.

I have found the proper way to handle this. That exception was thrown because the serializer was not able to create the PostDTO class using the invalid payload I have provided.
To handle this, I have created my custom denormalizer which kicks in only for this particular class. To do this, I have implemented the DenormalizerInterface like so:
use App\Service\PostDTO;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PostDTODeserializer implements DenormalizerInterface
{
/** #var ObjectNormalizer */
private $normalizer;
/**
* PostDTODeserializer constructor.
* #param ObjectNormalizer $normalizer
*/
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
return $type === PostDTO::class;
}
/**
* #param mixed $data
* #param string $type
* #param string|null $format
* #return array|bool|object
* #throws ExceptionInterface
*/
public function supportsDenormalization($data, string $type, string $format = null)
{
// validate the array which will be normalized (you should write your validator and inject it through the constructor)
if (!is_string($data['name'])) {
// normally you would throw an exception and leverage the `ErrorController` functionality
// do something
}
// convert the array to the object
return $this->normalizer->denormalize($data, $type, $format);
}
}
If you want to access the context array, you can implement the DenormalizerAwareInterface. Normally, you would create your custom validation and inject it into this denormalizer and validate the $data array.
Please not that I have injected the ObjectNormalizer here so that when the data successfully passed the validation, I can still construct the PostDTO using the $data.
PS: in my case, the autowiring has automatically registered my custom denormalizer. If yours is not autowired automatically, go to services.yaml and add the following lines:
App\Serializer\PostDTODeserializer:
tags: ['serializer.normalizer']
(I have tagged the implementation with serializer.normalizer so as it is recognized during the deserialization pipeline)

Related

How to validate the UUID parameter before SQL request for a "GET item" request in API-Platform?

Using:
PostgreSQL 11 with uuid_generate_v4 type
Symfony 4.4.11
Api Platform 2.5.6
I have an Entity with the following Id :
/**
* #ORM\Entity(repositoryClass="App\Repository\ContractRepository")
* #ORM\HasLifecycleCallbacks
*/
class Contract
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(name="id", type="guid", unique=true)
*/
private $id;
[...]
I generate the following route with Api Platform :
App\Entity\Contract:
itemOperations:
get:
So I get a generated route like /contracts/{id}
Currently, if I do /contracts/TEST, it will try to do the SQL request with "TEST" in a where clause and so will fail as a 500.
I would like to prevent this behaviour by asserting that the {id} parameter is a UUID_v4 and return a 400 if not.
This behaviour is DBMS specific, so you have to add your own logic.
The API-Platform component which retrieve an entity given an ID is the ItemDataProviderInterface.
First, I will declare a new exception MalformedUuidException.
Next, I will convert this exception to a 400 error.
Finally, I will create a new ItemDataProviderInterface implementation, wrapping the ORM one and adding some checks to the ID:
class ContractDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface
{
/** #var ItemDataProviderInterface */
private $realDataProvider;
public function __construct(ItemDataProviderInterface $realDataProvider)
{
$this->realDataProvider = $realDataProvider;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
$uuidPattern = '/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i';
if (preg_match($uuidPattern, $id) === 1) {
return $this->realDataProvider->getItem($resourceClass, ['id' => $id], $operationName, $context);
} else {
throw new MalformedUuidException("the given ID \"$id\" is not a valid UUID.");
}
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return $resourceClass === Contract::class;
}
}
# config/services.yaml
App\DataProvider\ContractDataProvider:
arguments:
$realDataProvider: '#api_platform.doctrine.orm.default.item_data_provider'
However, note that the getItem() method's contract does not specify the MalformedUuidException exception, so this implementation breaks the Liskov substitution principle.
Consider returning null instead and be satisfied with a 404 error.

Symfony4, how to properly handle missing form field ? (without exception in $this->propertyAccessor->setValue)

I have a Task entity, with two mandatory, non-nullable, fields:
title
dueDatetime
and Form to create task. The form is called by external scripts through POST with application/x-www-form-urlencoded (so no json or anything fancy), so I use standard symfony to handle this.
Problem is I don't control the scripts, and if the script forgot one of the argument, symfony4 will directly throw an exception at the handleRequest step, before I have the time to check if the form is valid or not. Which result in an ugly response 500.
My question: How to avoid that ? The best for me would be to just continue to use "form->isValid()" as before , but if there's an other standard way to handle that, it's okay too.
Note: it would be best if I don't have to put my entity's setter as accepting null values
The exception I got:
Expected argument of type "DateTimeInterface", "NULL" given.
in vendor/symfony/property-acces /PropertyAccessor.php::throwInvalidArgumentException (line 153)
in vendor/symfony/form/Extension/Core/DataMapper/PropertyPathMapper.php->setValue (line 85)
in vendor/symfony/form/Form.php->mapFormsToData (line 622)
in vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php->submit (line 108)
in vendor/symfony/form/Form.php->handleRequest (line 492)
A curl that reproduce the error :
curl -d 'title=foo' http://127.0.0.1:8080/users/api/tasks
The code :
Entity:
class Task
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="bigint")
*/
private $id;
/**
* #Assert\NotNull()
* #Assert\NotBlank()
* #ORM\Column(type="string", length=500)
*/
private $title;
/**
*
* #ORM\Column(type="datetimetz")
*/
private $dueDatetime;
public function getDueDatetime(): ?\DateTimeInterface
{
return $this->dueDatetime;
}
public function setDueDatetime(\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTitle()
{
return $this->title;
}
}
Form
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('dueDatetime')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Task::class]);
}
}
Controller:
class TaskController extends AbstractController
{
/**
* #Route(
* "/users/api/tasks",
* methods={"POST"},
* name="user_api_create_task"
* )
*/
public function apiCreateTask(Request $request)
{
$task = new Task();;
// the use of createNamed with an empty string is just so that
// the external scripts don't have to know about symfony's convention
$formFactory = $this->container->get('form.factory');
$form = $formFactory->createNamed(
'',
TaskType::class,
$task
);
$form->handleRequest($request); // <-- this throw exception
// but this code should handle this no ?
if (!$form->isSubmitted() || !$form->isValid()) {
return new JsonResponse([], 422);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($task);
$entityManager->flush();
return new JsonResponse();
}
}
There are at least 2 ways to handle this.
In the two ways you will have to add #Assert\NotNull() to the dueDatetime attribute.
1 - You can try/catch the exception of the handleRequest call.[edit] this one breaks the flow, not good.
2 - You can make nullable the setter setDueDatetime(\DateTimeInterface $dueDatetime = null). If you choose this one, please be sure to always validate your entity before an Insert/Update in DB else you will get an SQL error.
In the two cases it will be handled by the validator isValid() and you will have a nice error in your front end.
You need to allow nullable parameter (with "?") in method setDueDatetime
public function setDueDatetime(?\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}

Use deserialize or getters and setters in controller actions

When implementing a rest json api with Symfony, one can deserialize the data for a create route with Jms Serializer:
$user = $serializer->deserialize($data, 'AppBundle\Entity\User', 'json');
but this makes all parameters of the User Entity available to set from the POST request, which might not be that good.
An alternative to this is to use setters in the controller:
$user = new User();
$user->setUsername($request->request->get('username'));
$user->sePassword($request->request->get('password'));
...
The latter option makes it more clear which parameters are actually able to set, but it requires a lot of code for a large entity.
What is the preferred way here?
Is it a third option?
You can serialize json data from your controller natively in Symfony once you have the Serializer component installed.
$user = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json');
When your object is created via this method, using the json from your request (decoded and then denormalized), the setters of your object are utilized to populate the properties of your object.
Could you post your User Entity?
Alternatively you can use Form Classes to perform this task.
Modification in relation to the comment on your question.
Annotation Groups in your entities works for serialization and deserialization.
class Item
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"first", "second"})
*/
private $id;
/**
* #ORM\Column(type="string", name="name", length=100)
* #Groups({"first"})
*/
private $name;
/**
* #ORM\Column(type="string", name="name", length=200)
* #Groups({"second"})
*/
private $description;
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
}
If you had both "name" and "description" in your POST data, you could insert either into your entity with the following:
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['first']]);
Or
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['second']]);
In the first case, only the name property would be populated and only the description property in the second case.

Add dynamic property on entity to be serialized

I have this REST API. Whenever request comes to get a resource by id ( /resource/{id}) I want to add a permissions array on that object on the fly (entity itself does not have that field).
What I came up with is this event listener. It checks the result the controller has returned:
class PermissionFinderListener {
...
public function onKernelView(GetResponseForControllerResultEvent $event) {
$object = $event->getControllerResult();
if (!is_object($object) || !$this->isSupportedClass($object)) {
return;
}
$permissions = $this->permissionFinder->getPermissions($object);
$object->permissions = $permissions;
$event->setControllerResult($object);
}
....
}
The problem is that the JMS Serializer opts out this dynamic property on serialization. I tried making the onPostSerialize event subscriber on JMS serializer, but then there are no clear way to check if this is a GET ONE or GET COLLECTION request. I don't need this behaviour on GET COLLECTION and also it results a huge performance hit on collection serialization. Also I don't want to create any base entity class with permission property.
Maybe there is some other way to deal with this scenario?
What I could imagine is a combination of Virtual Property and Serialization Group:
Add a property to your entity like:
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"}) */
*
* #return string
*/
public function getPermissions()
{
return $permissionFinder->getPermissions($this);
}
Only thing you need to do then is to serialize 'includePermissions' group only in your special case (see http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies)
If you don't have access to $permissionFinder from your entity you could as well set the permission attribute of an entity from a Controller/Service before serializing it.
EDIT:
This is a bit more code to demonstrate what I mean by wrapping your entity and using VirtualProperty together with SerializationGroups. This code is not tested at all - it's basically a manually copied and stripped version of what we're using. So please use it just as an idea!
1) Create something like a wrapping class for your entity:
<?php
namespace Acquaim\ArcticBundle\Api;
use JMS\Serializer\Annotation as JMS;
/**
* Class MyEntityApi
*
* #package My\Package\Api
*/
class MyEntityApi
{
/**
* The entity which is wrapped
*
* #var MyEntity
* #JMS\Include()
*/
protected $entity;
protected $permissions;
/**
* #param MyEntity $entity
* #param Permission[] $permissions
*/
public function __construct(
MyEntity $entity,
$permissions = null)
{
$this->entity = $entity;
$this->permissions = $permissions;
}
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"})
*
* #return string
*/
public function getPermissions()
{
if ($this->permissions !== null && count($this->permissions) > 0) {
return $this->permissions;
} else {
return null;
}
}
/**
* #return object
*/
public function getEntity()
{
return $this->entity;
}
}
2) In your controller don't return your original Entity, but get your permissions and create your wrapped class with entity and permissions.
Set your Serialization Context to include permissions and let the ViewHandler return your serialized object.
If you don't set Serialization Context to includePermissions it will be excluded from the serialized result.
YourController:
$myEntity = new Entity();
$permissions = $this->get('permission_service')->getPermissions();
$context = SerializationContext::create()->setGroups(array('includePermissions'));
$myEntityApi = new MyEntityApi($myEntity,$permissions);
$view = $this->view($myEntityApi, 200);
$view->setSerializationContext($context);
return $this->handleView($view);

Symfony2 Form with custom FormType calls DataTransformer with same data in both directions

I have made a new FormType and it extends the entity type via
//...
public function getParent()
{
return 'entity';
}
Which lead my edit form to complain that an integer was not My/Entity/Type and I need a data transformer. So I created one. This is the abbreviated version (it's just the basic tutorial version)
//...
public function reverseTransform($val)
{
// Entity to int
return $val->getId();
}
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
//...
Then added it to my form type
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new IdToMyModelTransformer($this->em));
}
This fixed me viewing my form, but now when I submit the form with an entity picked from my custom widget it tries to call transform not reverseTransform with the $val as an int the ->getId() fails on a non-object.
I can't figure out the correct way of doing this. If I use 'choice' as my widget parent I get a different set of issues (choice default constraints triggered saying it is invalid data?)
I need an entity passed to my widget so it can extract the meta data for display, but I can't post an entity back of course. How do I tell the form that?
Tried setting 'data_class' => null but no joy. Checking network tab shows the value is sent correctly when posting the form.
Update 1
So I re-read the DataTransformer page and that diagram got me thinking, especially after rubber-duck programming above, I ask the form for Entity but expect it to receive ints.. so I actually need a unidirectional transformer, ViewTransformer -> Get entity for display, get posted an int from widget, don't transform it just pass straight through. Which works and I just get the "invalid data" error on update.
Now I have in my Transformer:
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Update 2
That seems to have fixed it now, although for some reason if I post int 2 in my form the string "2/" is sent to my transformer. Any ideas on that? FOr now I'm cleaning the string in transformer, but seems like it just shouldnt be happening.
By what I'm seeing in your transformer class you're not implementing the code right. This should be the correct implementation:
namespace App\YourBundle\Form\DataTransformer;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IdToMyModelTransformer implements DataTransformerInterface
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em) {
$this->em = $em;
}
/**
* Transforms a value from the original representation to a transformed representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the datasource (object or array).
* 2. When data from a request is submitted using {#link Form::submit()} to transform the new input data
* back into the renderable format. For example if you have a date field and submit '2009-10-10'
* you might accept this value because its easily parsed, but the transformer still writes back
* "2009/10/10" onto the form field (for further displaying or other purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* #param mixed $object The value in the original representation
*
* #return mixed The value in the transformed representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function transform($object) {
if (null === $object) {
return null;
}
return $object->getId();
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {#link Form::submit()} is called to transform the requests tainted data
* into an acceptable format for your data processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as empty strings). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* #param mixed $categoryId The value in the transformed representation
*
* #return mixed The value in the original representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($id) {
if (!$id || $id <= 0) {
return null;
}
if(!ctype_digit($id)){
throw new TransformationFailedException();
}
$repo = $this->em->getRepository('...');
$result = $repo->findOneBy(array('id' => $id));
if (null === $result) {
throw new TransformationFailedException(
sprintf(
'Entity with id does not exist!',
$id
)
);
}
return $result;
}
}
In the IdToMyIntType you would have something like this:
namespace App\YourBundle\Form\Type;
use App\YourBundle\Form\DataTransformer\IdToMyModelTransformer ;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IdToMyModelType extends AbstractType {
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct( EntityManager $em ) {
$this->em = $em;
}
public function buildForm( FormBuilderInterface $builder, array $options ) {
$transformer = new IdToMyModelTransformer ( $this->em );
$builder->addModelTransformer( $transformer );
}
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array('invalid_message' => 'Something went wrong message.'));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'id_to_model_type';
}
}
I would suggest you check out the DataTransformerInterface and read the documentation over the methods. It'll briefly explain what is that method expected to do. Also, in case you have problems implementing it, you can always check the official documentation, which contains a working example and build up from there.
As per my last update I realised, because I was only using my form data to display the currently saved entity relation (the rest is provided by ajax) and not in the same format the form would be receiving it in it lead to some confusion.
To follow the tutorials wording:
Model data
This was all to remain as-is (No model datatransformer needed)
Norm data
No changes
View data (unidirection transformation required)
Transform()
ID to Entity so widget can access other properties
ReverseTransform()
Posted ID is in correct format so we just return it
Code
Very simplified:
private $om;
public function __construct (ObjectManager om)
{
$this->om = $om;
}
public function transform($val)
{
// Int to Entity
return $om->getRepository('MyBundle:EntityName')->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Hopefully that helps anyone else who lets their requirements confuse them!

Resources