Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Controlling Fields without Groups

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

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.

readable: false

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.

writable: false

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.

Leave a comment!

Login or Register to join the conversation
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.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