Description
The input worfklow miss support for reverseTransform (like it's done in symfony/form component).
ATM, it only works with POST method, not PUT.
To make it fully works, I had to manually add support for it.
The inner issue is that the DTO is not update with value from the entity before the request is bind to it. So the validation could not be done properly, and also it's impossible to know what has been updated.
To fix that, I added a normalizer, and some extra code to the data transformer
The data transformer
<?php
namespace AppBundle\Api\DataTransformer;
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use AppBundle\Api\Dto\PlannedCrawl as PlannedCrawlDto;
use AppBundle\Entity\PlannedCrawl;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class PlannedCrawlTransformer implements DataTransformerInterface
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function transform($plannedCrawlDto, string $to, array $context = [])
{
$errors = $this->validator->validate($plannedCrawlDto);
if (\count($errors)) {
throw new ValidationException($errors);
}
if ('item' === $context['operation_type'] && 'put' === $context['item_operation_name']) {
$entity = $context['object_to_populate'];
$entity->updateFromDto($plannedCrawlDto);
return $entity;
}
return PlannedCrawl::createFromDto($plannedCrawlDto);
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return PlannedCrawl::class === $to && PlannedCrawlDto::class === ($context['input']['class'] ?? null);
}
}
The normalizer
<?php
namespace AppBundle\Api\Normalizer;
use AppBundle\Api\Dto\PlannedCrawl;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PlannedCrawlNormalizer implements DenormalizerInterface, CacheableSupportsMethodInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if ($context['object_to_populate'] ?? false) {
// Swap object to populate to use a new DTO updated with real entity value
$context['object_to_populate'] = PlannedCrawl::createFromEntity($context['object_to_populate']);
}
return $this->normalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return PlannedCrawl::class === $type;
}
public function hasCacheableSupportsMethod(): bool
{
return true;
}
}
IMHO, if you add a reverseTransform, I will be able to remove my normalizer and to remove the following hack in the DataTranformer:
if ('item' === $context['operation_type'] && 'put' === $context['item_operation_name']) {
This issue struck me also. To say it a different way (though, @lyrixx's description really good already):
Using Product an an example entity, for PUT, we need a way to "initialize" the ProductInputDto object (based on the the actual entity's current database data) before it's passed to the transformer.
The process (which @lyrixx created manually above) looks like this:
A) We are somehow able to "initialize" the ProductInputDto object by using the Product database entity. This puts it into a "this is how the database currently looks" state. This is then used as the object_to_populate when deserializing the JSON into ProductInputDto.
B) The process continues like normal. But this means that the transformer is passed a ProductInputDto object that also contains the "current" data. This allows us to run validation (e.g. a field omitted by the user in the PUT request won't be null on the DTO - it will have the current database data) and also allows us to safely write all fields from ProductInputDto onto Product (currently we might accidentally set a property to null... because it's missing on ProductInputDto because the user didn't actually send it in the JSON).
Possible Solution
Add a new DataTransformerInitializerInterface:
interface DataTransformerInitializerInterface extends DataTransformerInterface
{
/**
* Creates a new DTO object that the data will then be serialized into (using object_to_populate).
*
* This is useful to "initialize" the DTO object based on the current resource's data. For example:
*
* $currentResource = $context[AbstractItemNormalizer::OBJECT_TO_POPULATE];
*
* if (!$currentResource) {
* // initialize an empty Dto
* return new ProductInputDto();
* }
*
* // create a ProductInputDto based on the current resource's data
* $dto = new ProductInputDto();
* $dto->title = $currentResource->getTitle();
*
* return $dto;
*/
public function initialize(string $to, array $context = []): ?object;
}
This would be used in AbstractItemNormalizer right before the DTO is denormalized: https://github.com/api-platform/core/blob/38d69e6fdeaa6d6d32d43b7467454acb17d11315/src/Serializer/AbstractItemNormalizer.php#L200
Any volunteers? :)
The proposed interface looks good to me! PR very welcome!
@soyuka could you also take a look to this proposal?
Fixed by #3701
Most helpful comment
This issue struck me also. To say it a different way (though, @lyrixx's description really good already):
Using
Productan an example entity, for PUT, we need a way to "initialize" theProductInputDtoobject (based on the the actual entity's current database data) before it's passed to the transformer.The process (which @lyrixx created manually above) looks like this:
A) We are somehow able to "initialize" the
ProductInputDtoobject by using theProductdatabase entity. This puts it into a "this is how the database currently looks" state. This is then used as theobject_to_populatewhen deserializing the JSON intoProductInputDto.B) The process continues like normal. But this means that the transformer is passed a
ProductInputDtoobject that also contains the "current" data. This allows us to run validation (e.g. a field omitted by the user in the PUT request won't benullon the DTO - it will have the current database data) and also allows us to safely write all fields fromProductInputDtoontoProduct(currently we might accidentally set a property tonull... because it's missing onProductInputDtobecause the user didn't actually send it in the JSON).Possible Solution
Add a new
DataTransformerInitializerInterface:This would be used in
AbstractItemNormalizerright before the DTO is denormalized: https://github.com/api-platform/core/blob/38d69e6fdeaa6d6d32d43b7467454acb17d11315/src/Serializer/AbstractItemNormalizer.php#L200Any volunteers? :)