Input DTO: Denormalizing IRI Strings
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeStatus update: we created a CheeseListingInput
class with all the properties and groups that we need, we've configured CheeseListing
to use that and we've started creating a data transformer that will take the CheeseListingInput
and convert it into a CheeseListing
whenever somebody sends JSON to a POST or PUT endpoint.
supportsTransformation()
In supportsTransformation()
, we're dumping $data
, $to
and $context
:
// ... lines 1 - 6 | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
// ... lines 9 - 12 | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
dump($data, $to, $context); | |
// ... lines 16 - 17 | |
} | |
} |
When we look over at the profiler... it's slightly different than what we saw with our output transformer. This time, the data is an array: it's the decoded JSON that we sent. The $to
is the target class - CheeseListing
- and the $context
is also interesting: it has an input
key with class set to CheeseListingInput
.
Let's use this to fill in supportsTransformation()
. Start by checking if $data
is an instanceOf CheeseListing
, which in our dump, it is not. But if it is, then return false
:
// ... lines 1 - 6 | |
use App\Entity\CheeseListing; | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
// ... lines 11 - 17 | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
if ($data instanceof CheeseListing) { | |
// already transformed | |
return false; | |
} | |
// ... lines 24 - 25 | |
} | |
} |
This would mean that the object has already been transformed.
To be honest... I'm not sure if this is needed... but it's shown on the docs. There might be some case where a transformer is called multiple times.
The real important part is the return
down here: we support this transformation if $to === CheeseListing::class
and $context['input']['class']
- or null
if it's not set - equals CheeseListingInput::class
:
// ... lines 1 - 5 | |
use App\Dto\CheeseListingInput; | |
use App\Entity\CheeseListing; | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
// ... lines 11 - 17 | |
public function supportsTransformation($data, string $to, array $context = []): bool | |
{ | |
if ($data instanceof CheeseListing) { | |
// already transformed | |
return false; | |
} | |
return $to === CheeseListing::class && ($context['input']['class'] ?? null) === CheeseListingInput::class; | |
} | |
} |
The first part makes sense: we want to return true if we are converting into a CheeseListing
. And the second part isn't needed in our app because we know that CheeseListing
will always use CheeseListingInput
as its input class. But since you can have different input classes on different operations, the $context
tells us which input class should be used for this operation. We're using that to double-check.
Anyways, let's see if this is working! Dump all three arguments again inside of transform()
. And, to avoid an error, return an empty CheeseListing
object at the bottom:
// ... lines 1 - 8 | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
public function transform($object, string $to, array $context = []) | |
{ | |
dump($object, $to, $context); | |
return new CheeseListing(); | |
} | |
// ... lines 17 - 26 | |
} |
Now spin over to the browser - I'll leave the profiler open - and hit "Execute" to try it again. Let's see: it's the same 400 validation error as before... which makes sense! We're returning a new, empty CheeseListing
.
Back on the profiler tab, hit "Latest", which should take us to the profiler for the latest request. Yep! This dump is coming from line 13, which proves our supportsTransformation()
is working!
Let's rename $object
to $input
and add some PHPDoc: we know this will be a CheeseListingInput
: the result of deserializing the JSON:
// ... lines 1 - 8 | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
/** | |
* @param CheeseListingInput $input | |
*/ | |
public function transform($input, string $to, array $context = []) | |
{ | |
dump($input, $to, $context); | |
// ... lines 17 - 18 | |
} | |
// ... lines 20 - 29 | |
} |
So... now our job is simple, right? Take this CheeseListingInput
object, move the data over onto a CheeseListing
and return it. What could go wrong?
Denormalizing IRIs into Objects
But... wait a second: check out the dump again in the profiler. The JSON has been deserialized into this CheeseListingInput
object, which is cool, but look at the owner
field. It's a string! I mean... that makes sense... because we are sending this string in our JSON. But... does that mean we're supposed to query for the User
object manually using this IRI?
Nope! One of the jobs of the deserializer is to take each piece of data in the JSON that we're sending and figure out what type it should be, like a string DateTime
object or User
object. Then it does whatever work is needed to change the raw data into that type - like creating a DateTime
object from a date string. There are various normalizers that are good at doing this.
And... fortunately API Platform comes with an ItemNormalizer
whose job is to change IRI strings into objects by querying the database... or... more accurately, by calling the item data provider.
So why isn't that happening here? Why isn't the IRI string being changed into a User
object? Check out the CheeseListingInput
class and look at the owner
property:
// ... lines 1 - 7 | |
class CheeseListingInput | |
{ | |
// ... lines 10 - 19 | |
/** | |
* @Groups({"cheese:collection:post"}) | |
*/ | |
public $owner; | |
// ... lines 24 - 43 | |
} |
The deserializer has no idea that this property is supposed to hold a User
object! And so, that ItemNormalizer
doesn't know it should do its work!
Let's help it: above this add @var User
:
// ... lines 1 - 4 | |
use App\Entity\User; | |
// ... lines 6 - 8 | |
class CheeseListingInput | |
{ | |
// ... lines 11 - 20 | |
/** | |
* @var User | |
// ... line 23 | |
*/ | |
public $owner; | |
// ... lines 26 - 45 | |
} |
Head back to the browser, hit Execute and... once that finishes, go to the Profiler, hit Latest and tada! The owner
is now a User
object! Adding proper types to your properties is very important for deserialization.
Next, there's a small bug in the documentation that we actually just found a way to work around. Let's see what it is and finish our data transformer.
DataTransformerInterface is deprecated with API Platform 3:
https://api-platform.com/docs/core/upgrade-guide/#summary-of-the-changes-between-26-and-2730