DTO Quirks

This Chapter isn't
quite ready...

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

Browse Tutorials

The last field that we're missing on CheeseListingOutput is owner.

No worries: in CheeseListingOutput, add public $owner. Then copy the PHPDoc from price and paste that here. We know that this will be a User object and we'll put it in the cheese:read group.

Over in the data transformer, populate that with $output->owner = $cheeseListing->getOwner().

Easy enough! Try it: find your browser, refresh and it works! The owner field is an embedded object because the phoneNumber field is being exposed.

It turns out, this detail is important. Go into the User class and look at the phoneNumber property. This is actually in two groups: owner:read an admin:read.

Right now, I'm logged in as an admin... and we created special code in the last tutorial to always add the admin:read group in this situation. This is the reason why we're able to see the phoneNumber on every user.

Let's see what happens when we are not an admin. Open a new tab, go to the homepage and click log out. Perfect.

User Serialization Has Changed?

Now that we're anonymous, refresh the same endpoint. Error? Interesting:

The return value of UserNormalizer::normalize() - that's a class we created in a previous tutorial - must be type array, string returned.

Let's go check that out: src/Serializer/Normalizer/UserNormalizer.php. The purpose of this is to add an extra owner:read group if the User that's being serialized is the currently-authenticated user.

The error says that this method is returning a string but it's supposed to return an array. And... it's right. Look at my normalize() method: I gave it an array return type. But apparently, when we call $this->normalizer->normalize(), this returns a string.

And, hmm... that makes sense. Now that we're anonymous, the phoneNumber field will not be returned. And so, when the embedded User object is serialized, instead of returning an array of fields, it is now normalized into its IRI string.

Ok! So if you normalize a User object, sometimes it will be an object and sometimes it will be an IRI string. The fix is to remove the array return type. That was actually never needed... it's not on the normalize() method's interface. I added it simply because I thought this would always return an array.

When we refresh now... yep! The owner property is an IRI string.

But... wait a second. Why didn't we have this error before? Before we started working with the output DTO stuff... shouldn't we have had this same problem with UserNormalizer? Why wasn't it a problem until now?

DTO's Serialize Differently

Here's the answer, and it's important. When you use an output class like we're doing for CheeseListing, the object that's ultimately serialized is CheeseListingOutput. And because that class isn't technically an API Resource class, it's serialized in a slightly different way internally. For the serialization nerds out there, API resource classes are usually normalized using ItemNormalizer which extends AbstractItemNormalizer. But with a DTO object, it instead uses the simpler ObjectNormalizer.

Where Did @type Go?

This causes small, but important differences. For example, when you use an output DTO, the @type field is gone. We have @id... but not @type. This actually makes one of our tests fail.

Find your terminal and run:

symfony php bin/phpunit

Yep! One failure because one test is looking for @type. Let's open this test up: tests/Functional/CheeseListingResourceTest.php and then scroll down to testGetCheeseListingCollection(). Let's see... here it is: that @type is no longer being returned. For now, just delete it so that the tests will pass.

But good news! Thanks to the API Platform team, this bug has been fixed and should be released in API Platform 2.5.8. But since that hasn't been released yet at the time of this recording, we'll move on.

Run the tests again:

symfony php bin/phpunit

And... green! Next, I want to look a bit deeper at how the serialization of embedded objects is different with DTO classes and what to do about it.

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