Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Serialization Tricks

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

We've sort of tricked the system to allow a textDescription field when we send data. This is made possible thanks to our setTextDescription() method, which runs nl2br() on the description that's sent to our API. This means that the user sends a textDescription field when editing or creating a treasure... but they receive a description field when reading.

... lines 1 - 34
class DragonTreasure
{
... lines 37 - 93
#[Groups(['treasure:write'])]
public function setTextDescription(string $description): self
{
$this->description = nl2br($description);
return $this;
}
... lines 101 - 150
}

And that's totally fine: you're allowed to have different input fields versus output fields. But it would be a bit cooler if, in this case, both were just called description.

SerializedName: Controlling the Field Name

So... can we control the name of a field? Absolutely! We do this, as you may have predicted, via another wonderful attribute. This one is called SerializedName. Pass it description:

... lines 1 - 15
use Symfony\Component\Serializer\Annotation\SerializedName;
... lines 17 - 35
class DragonTreasure
{
... lines 38 - 101
#[SerializedName('description')]
#[Groups(['treasure:write'])]
public function setTextDescription(string $description): self
{
$this->description = nl2br($description);
return $this;
}
... lines 110 - 166
}

This won't change how the field is read, but if we refresh the docs... and look at the PUT endpoint... yep! We can now send a field called description.

Constructor Arguments

What about constructor arguments in our entity? When we make a POST request, for example, we know it uses the setter methods to write the data onto the properties.

Now try this: find setName() and remove it. Then go to the constructor and add a string $name argument there instead. Below, say $this->name = $name.

... lines 1 - 35
class DragonTreasure
{
... lines 38 - 67
public function __construct(string $name)
{
$this->name = $name;
$this->plunderedAt = new \DateTimeImmutable();
}
... lines 73 - 160
}

From an object-oriented perspective, the field can be passed when the object is created, but after that, it's read-only. Heck, if you wanted to get fancy, you could add readonly to the property.

Let's see what this looks like in our documentation. Open up the POST endpoint. It looks like we can still send a name field! Test by hitting "Try it out"... and let's add a Giant slinky we won from a real-life giant in... a rather tense poker match. It's pretty valuable, has a coolFactor of 8, and give it a description. Let's see what happens. Hit "Execute" and... it worked! And we can see in the response that the name was set. How is that possible?

Well, if you go down and look at the PUT endpoint, you'll see that it also advertises name here. But... go up find the id of the treasure we just created - its 4 for me, put 4 in here to edit... then send just the name field to change it. And... it didn't change! Yup, just like with our code, once a DragonTreasure is created, the name can't be changed.

But... how did the POST request set the name... if there's no setter? The answer is that the serializer is smart enough to set constructor arguments... if the argument name matches the property name. Yup, the fact that the arg is called name and the property is also called name is what makes this work.

Watch: change the argument to treasureName in both places:

... lines 1 - 35
class DragonTreasure
{
... lines 38 - 67
public function __construct(string $treasureName)
{
$this->name = $treasureName;
$this->plunderedAt = new \DateTimeImmutable();
}
... lines 73 - 160
}

Now, spin over, refresh, and check out the POST endpoint. The field is gone. API Platform sees that we have a treasureName argument that could be sent, but since treasureName doesn't correspond to any property, that field doesn't have any serialization groups. So it's not used. I'll change that back to name:

... lines 1 - 35
class DragonTreasure
{
... lines 38 - 67
public function __construct(string $name)
{
$this->name = $name;
$this->plunderedAt = new \DateTimeImmutable();
}
... lines 73 - 160
}

By using name, it looks at the name property, and reads its serialization groups.

Optional Vs Required Constructor Args

However, there is still one problem with constructor arguments that you should be aware of. Refresh the docs.

What would happen if our user doesn't pass a name at all? Hit "Execute" to find out. Ok! We get an error with a 400 status code... but it's not a very good error. It says:

Cannot create an instance of App\Entity\DragonTreasure from serialized data because its constructor requires parameter name to be present.

That's... actually too technical. What we really want is to allow validation to take care of this... and we'll talk about validation soon. But in order for validation to work, the serializer needs to be able to do its job: it needs to be able to instantiate the object:

... lines 1 - 35
class DragonTreasure
{
... lines 38 - 67
public function __construct(string $name = null)
{
$this->name = $name;
$this->plunderedAt = new \DateTimeImmutable();
}
... lines 73 - 160
}

Ok, try this now... better! Ok, it's worse - a 500 error - but we'll fix that with validation in a few minutes. The point is: the serializer was able to create our object.

Next: To help us while we're developing, let's add a rich set of data fixtures. Then we'll play with a great feature that API Platform gives us for free: pagination

Leave a comment!

2
Login or Register to join the conversation
Julien-M Avatar
Julien-M Avatar Julien-M | posted 6 months ago

The code in the SerializedName part doesn't seem to be the right one since this attribute is nowhere to be seen.
I suppose it should be like:

    #[Groups(['treasure:write'])]
    #[SerializedName('description')]
    public function setTextDescription(string $description): self

Great course nonetheless!

Reply

Hey,

Whoops. Sorry for that, somehow several code blocks on that page were duplicated. I already fixed them. Thanks for the report!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice