If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
When your API resource is on an entity, serialization groups are a must because you'll definitely have some properties that you want to show or not show. But serialization groups add complexity. One of the big benefits of having a separate class for your API is not needing serialization groups. Because... the whole point of your API class is to represent your API... so, in theory, you'll want every property to be part of your API.
But, in the real world, that's not always true. And we just ran into one case:
password
should be a write-only field. Let's try to replicate some of the complexity
that our User
entity originally had, but by avoiding serialization groups.
In UserResourceTest
, down here, remove the ->dump()
... and after we
->assertStatus(201)
, assert that the password
property is not returned. To
do that, we can say ->use(function(Json $json))
. The use()
function comes
from browser and there are a few different objects - like Json
- that you can
ask it to pass you via the type-hint. In this case, browser takes the JSON from
the last response, puts it into a Json
object and passes it to us. Use it by
saying $json->assertMissing('password')
.
If we try that now:
symfony php bin/phpunit --filter=testPostToCreateUser
It fails because password
does exist.
Okay, let's take a tour of how we can customize our API fields without groups.
One of the easiest, (and, coincidentally, my favorite) is to use #[ApiProperty()]
with readable: false
.
We want this to be writable, but not readable.
symfony php bin/phpunit --filter=testPostToCreateUser
And... that fixes things! Beautiful.
Let's repeat this for id
... because id
is pretty useless since we have @id
.
When we run that... it fails because id
is being returned. So now, copy...
just the readable: false
part... add #[ApiProperty]
above id
, paste, and
I'll also add identifier: true
... just to be explicit.
And now...
symfony php bin/phpunit --filter=testPostToCreateUser
That passes.
Let's keep going. Copy the next test name - testPatchToUpdateUser
- and run
it:
symfony php bin/phpunit --filter=testPatchToUpdateUser
It passes immediately! Yay! ->patch()
is already working. To dive deeper into
other ways we can hide or show fields, also send a flameThrowingDistance
field
in the JSON set to 999. And down here, ->dump()
the response.
Before we try this, find EntityClassDtoStateProcessor
. Right after we
set the id
, dump($data)
. Those two dumps will help us understand
exactly how this all works.
Now run the test:
symfony php bin/phpunit --filter=testPatchToUpdateUser
And... awesome. The first dump on top - from the state processor - shows
flameThrowingDistance
999, which means the field is writable. And below,
the response returned 999, which means the field is also readable. Yup...
this is a normal, boring field. If the user sends the field in JSON, that new
value is deserialized onto the object.
Ok, experimentation time! In UserApi
, above the property, start with the same
#[ApiProperty()]
and readable: false
. We've already seen this.
When we run the test, on top, the "999" was written onto the UserApi
,
but it doesn't show up in the response. It's writable, but not readable.
If we also pass writable: false
... and try again. On top, the value is
just "10". The field is not writable, so the field in the JSON was ignored.
It's also not in the response: it's not readable or writable.
The readable/writable options alone are probably going to solve most situations. But next, let's learn some other tricks and see why you probably want to make sure that your identifier is not writable.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "3.1.x-dev", // 3.1.x-dev
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.10.2
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
"doctrine/orm": "^2.14", // 2.16.1
"nelmio/cors-bundle": "^2.2", // 2.3.1
"nesbot/carbon": "^2.64", // 2.69.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.23.1
"symfony/asset": "6.3.*", // v6.3.0
"symfony/console": "6.3.*", // v6.3.2
"symfony/dotenv": "6.3.*", // v6.3.0
"symfony/expression-language": "6.3.*", // v6.3.0
"symfony/flex": "^2", // v2.3.3
"symfony/framework-bundle": "6.3.*", // v6.3.2
"symfony/property-access": "6.3.*", // v6.3.2
"symfony/property-info": "6.3.*", // v6.3.0
"symfony/runtime": "6.3.*", // v6.3.2
"symfony/security-bundle": "6.3.*", // v6.3.3
"symfony/serializer": "6.3.*", // v6.3.3
"symfony/stimulus-bundle": "^2.9", // v2.10.0
"symfony/string": "6.3.*", // v6.3.2
"symfony/twig-bundle": "6.3.*", // v6.3.0
"symfony/ux-react": "^2.6", // v2.10.0
"symfony/ux-vue": "^2.7", // v2.10.0
"symfony/validator": "6.3.*", // v6.3.2
"symfony/webpack-encore-bundle": "^2.0", // v2.0.1
"symfony/yaml": "6.3.*", // v6.3.3
"symfonycasts/micro-mapper": "^0.1.0" // v0.1.1
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.11
"symfony/browser-kit": "6.3.*", // v6.3.2
"symfony/css-selector": "6.3.*", // v6.3.2
"symfony/debug-bundle": "6.3.*", // v6.3.2
"symfony/maker-bundle": "^1.48", // v1.50.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.3.2
"symfony/stopwatch": "6.3.*", // v6.3.0
"symfony/web-profiler-bundle": "6.3.*", // v6.3.2
"zenstruck/browser": "^1.2", // v1.4.0
"zenstruck/foundry": "^1.26" // v1.35.0
}
}