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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Adding 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 type int, 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.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.7
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.4
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.4.5", // v2.7.3
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.4
        "ramsey/uuid-doctrine": "^1.6", // 1.6.0
        "symfony/asset": "5.1.*", // v5.1.5
        "symfony/console": "5.1.*", // v5.1.5
        "symfony/debug-bundle": "5.1.*", // v5.1.5
        "symfony/dotenv": "5.1.*", // v5.1.5
        "symfony/expression-language": "5.1.*", // v5.1.5
        "symfony/flex": "^1.1", // v1.9.10
        "symfony/framework-bundle": "5.1.*", // v5.1.5
        "symfony/http-client": "5.1.*", // v5.1.5
        "symfony/monolog-bundle": "^3.4", // v3.5.0
        "symfony/security-bundle": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/validator": "5.1.*", // v5.1.5
        "symfony/webpack-encore-bundle": "^1.6", // v1.7.3
        "symfony/yaml": "5.1.*" // v5.1.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
        "symfony/browser-kit": "5.1.*", // v5.1.5
        "symfony/css-selector": "5.1.*", // v5.1.5
        "symfony/maker-bundle": "^1.11", // v1.21.1
        "symfony/phpunit-bridge": "5.1.*", // v5.1.5
        "symfony/stopwatch": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.5
        "zenstruck/foundry": "^1.1" // v1.1.2
    }
}