Input Data Transformer

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

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.

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.

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. 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.

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.

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
        "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.6
        "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
    }
}