I have a DTO class looking like:
class ParamsDto
{
#[AssertType(ArrayCollection::class)]
#[AssertAll([
new AssertType('digit'),
new AssertPositive(),
])]
private ?ArrayCollection $tagIds = null;
public function getTagIds(): ?ArrayCollection
{
return $this->tagIds;
}
public function setTagIds(?ArrayCollection $tagIds): self
{
$this->tagIds = $tagIds;
return $this;
}
}
Given a request to a url like https://domain.php?tag-ids[]=2
, I’d like to parse the tag-ids
request param into this DTO’s tagIds
property.
First step I did, I created a name converter, so I can convert between tag-ids
and tagIds
, so my serializer instantiation looks like:
$nameConverter = new EducationEntrySearchParamNameConverter();
$serializer = new Serializer([
new ArrayDenormalizer(),
new ObjectNormalizer(null, $nameConverter, null, new ReflectionExtractor()),
], [new JsonEncoder()]);
$params = $serializer->denormalize($requestParams, ParamsDto::class);
where $params
shows as:
^ AppDataTransferObjectParamsDto {#749
-tagIds: DoctrineCommonCollectionsArrayCollection {#753
-elements: []
}
}
So it is instantiated but it is empty.
Most likely because my request does not include the elements
key in it.
If I do a bit of preprocessing, like:
$requestParams = [];
foreach ($request->query->all() as $key => $value) {
if (in_array($key, ['tag-ids'])) {
$requestParams[$key] = ['elements' => $value];
} else {
$requestParams[$key] = $value;
}
}
$params = $serializer->denormalize($requestParams, ParamsDto::class);
then I get the right output:
^ AppDataTransferObjectParamsDto {#749
-tagIds: DoctrineCommonCollectionsArrayCollection {#757
-elements: array:1 [
0 => "2"
]
}
}
How do I do it in a way that the serializer translate the request into the DTO in a way where I don’t have to do this pre-processing?
L.E: No need for using a custom name converter, I am now using SerializedName
2
Answers
I'll add this in order to help others struggling with same problem, but I was able to come up with it only after @Jakumi's answer, this is also why I am going to award them the bounty.
Therefore, after some research, it seems that indeed, the simplest way to go about this is to just use array as the type hint. This way, things will work out of the box, without you doing anything else, but you won't be using collections, so:
However, if you want to keep using collections, then you need to do some extra work:
So beside the setter and getter, you'll need to add a adder and a remover as well.
This will deserialize into a ArrayCollection of integers.
Bonus, if your object property is a collection of DTO's, not just integers, and you want to denormalize those as well, then you'd do something like:
This will denormalize the tags property into a ArrayCollection instance (TagCollection extends it) and each item in the collection will be a TagDto in this case.
Short answer: I would strongly suggest to replace the collection with just a standard php built-in
array
instead – at least on the setter! -, I bet that would help.Long answer:
My guess is, since your DTO isn’t an entity, that the serializer won’t use something doctrine-related, which would tell it to convert arrays to collections.
IMHO, Collections are object-internal solutions that should never be exposed directly to the outside, the interface to the outside world are arrays. Very opinionated maybe.
From a non-doctrine Serializer’s perspective, a collection is an object. Objects have properties. To map an array onto an object is to use the keys of the array and match them to the properties of the object, using reflection: Using the property directly, if it is public, else using adders/removers, else using setters/getters. (IIRC in that order) – That’s why adding
elements
as a key to your input with the array of ids "works".Using an array as the interface in the setter/getter pair will help the property accessor greatly to just set the array. You can still have the property internally as an ArrayCollection, if that’s what you want/need.
This is just a guess: adding docblock type hint
/** @param array<int> $tagIds */
to your setter might even trigger conversion from"2"
to2
. HavingaddTagId
+removeTagId
+getTagIds
instead with a php type hint might also work instead.