Input Data Transformer
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 SubscribeAdding the @var User
above the owner
property was enough for the denormalizer to automatically convert the IRI string we're sending in our JSON into a proper User
object. Yay! And this also fixed something in our documentation. Go back to the docs tab... actually, I'll open a new tab so I don't lose my testing data.
On the original tab, until now, when we hit "Try it out", it only listed the description
field in the example JSON. The docs didn't think that title
, owner
and price
were fields that were allowed to be sent.
But now, on the new version of the docs, when we hit "Try it out"... it does now recognize that owner
is a field we can send.
So... what's going on? It looks like there's a little bug with input DTOs where the documentation doesn't notice that a field exists until it has some metadata on it. So as soon as we added the type to owner
, suddenly the documentation noticed it!
And... that's fine because we do want types on all of our properties. Back in the class, above title
, add @var string
, @var int
for price and above isPublished
, @var bool
:
// ... lines 1 - 8 | |
class CheeseListingInput | |
{ | |
/** | |
* @var string | |
// ... line 13 | |
*/ | |
public $title; | |
/** | |
* @var int | |
// ... line 19 | |
*/ | |
public $price; | |
// ... lines 22 - 28 | |
/** | |
* @var bool | |
// ... line 31 | |
*/ | |
public $isPublished = false; | |
// ... lines 34 - 48 | |
} |
By the way, if you're wondering why description
was always in the docs, remember that the description
field comes from the setTextDescription()
method, which does have metadata above it and an argument with a type-hint:
// ... lines 1 - 8 | |
class CheeseListingInput | |
{ | |
// ... lines 11 - 36 | |
/** | |
* The description of the cheese as raw text. | |
* | |
* @Groups({"cheese:write", "user:write"}) | |
* @SerializedName("description") | |
*/ | |
public function setTextDescription(string $description): self | |
{ | |
// ... lines 45 - 47 | |
} | |
} |
Let's check the docs now: refresh, go back to the POST endpoint, hit, "Try it out" and... yes! Now it sees all the fields.
Finishing the transform Logic
Ok: let's finish our data transformer. Instead of returning, say $cheeseListing = new CheeseListing()
and pass the title as the first argument: $input->title
:
// ... lines 1 - 8 | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
// ... lines 11 - 13 | |
public function transform($input, string $to, array $context = []) | |
{ | |
// ... lines 16 - 17 | |
$cheeseListing = new CheeseListing($input->title); | |
// ... lines 19 - 24 | |
} | |
// ... lines 26 - 35 | |
} |
Then, some good, boring work: $cheeseListing->setDescription($input->description)
, $cheeseListing->setPrice($input->price)
, $cheeseListing->setOwner($input->owner)
- which is a User
object - and $cheeseListing->setIsPublished($input->isPublished)
. Return $cheeseListing
at the bottom:
// ... lines 1 - 8 | |
class CheeseListingInputDataTransformer implements DataTransformerInterface | |
{ | |
// ... lines 11 - 13 | |
public function transform($input, string $to, array $context = []) | |
{ | |
// ... lines 16 - 17 | |
$cheeseListing = new CheeseListing($input->title); | |
$cheeseListing->setDescription($input->description); | |
$cheeseListing->setPrice($input->price); | |
$cheeseListing->setOwner($input->owner); | |
$cheeseListing->setIsPublished($input->isPublished); | |
return $cheeseListing; | |
} | |
// ... lines 26 - 35 | |
} |
Okay: moment of truth. I'll close the extra tab, go back to the original documentation tab, hit "Execute" and... it fails:
Argument 1 passed to
CheeseListing::setPrice()
must be of typeint
,null
given.
The problem is that I forgot to pass a price
field up in the JSON, which causes the type error. We're going to talk more about this later when we chat about validation, but for now, be sure to pass every field we need, like price: 2000
.
Try it again. And... bah! I get the same error for the setIsPublished()
method. I really meant to default isPublished
to false in CheeseListingInput
:
// ... lines 1 - 8 | |
class CheeseListingInput | |
{ | |
// ... lines 11 - 32 | |
public $isPublished = false; | |
// ... lines 34 - 48 | |
} |
Ok, one more time. And... yes! A 201 status code. It worked!
So using a DTO input is a 3-step process. First, API Platform deserializes the JSON we send into a CheeseListingInput
object. Second, we transform that CheeseListingInput
into a CheeseListing
in the data transformer. And third, the normal Doctrine data persister saves things. That's a really clean process!
Go back to the docs and look at the put operation that updates cheeses. Will this work? Well, we do have a data transformer... so... why wouldn't it? Well, it won't quite work yet. Why not? Because our data transformer always creates new CheeseListing
objects... which would cause Doctrine to make an INSERT query even though we're trying to update a record.
Next: let's make this work! It's... a bit trickier than it may seem at first.
Hey there,
For some reason this wasn't working for me.
I was getting a 500 error like this one.
<blockquote>A new entity was found through the relationship 'App\Entity\CheeseNotification#cheeseListing' that was not configured to cascade persist operations for entity: App\Entity\CheeseListing@000000000c8e85fb0000000038202b1e. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'App\Entity\CheeseListing#__toString()' to get a clue.</blockquote>
So I went in App\DataPersister\CheeseListingDataPersister and change the persist function.
Became
In short, I had to move up the $this->decoratedDataPersister->persist($data); so its already persisted before the persist and flush the notification.
Thought it could help.