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 SubscribeWe'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 | |
'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
.
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 | |
'description') ( | |
'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
.
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.
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 parametername
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
// 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
}
}
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:
Great course nonetheless!