Other Conditional Field Strategies
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet's keep playing with how we can hide or show fields. Remove the #[ApiProperty]
attribute. Then, on top, set the normalizationContext
option. We used this in previous tutorials... but this time, instead of groups
, set a key called AbstractNormalizer::IGNORED_ATTRIBUTES
and then set that to an array. Inside, put flameThrowingDistance
.
// ... lines 1 - 24 | |
class UserApi | |
{ | |
// ... lines 27 - 44 | |
readable: false, writable: false) | (|
public int $flameThrowingDistance = 0; | |
} |
Whether a field is readable or writable really comes down to the serializer. This tells the serializer:
Yo! When you're normalizing - so going to JSON - ignore this property.
This should make it writable, but not readable. When we try it...
symfony php bin/phpunit --filter=testPostToCreateUser
That's exactly what happens! To wrap it in a "do not write" sign, duplicate this move with denormalizationContext
.
// ... lines 1 - 10 | |
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | |
// ... lines 12 - 15 | |
( | |
// ... line 17 | |
normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['flameThrowingDistance']], | |
denormalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['flameThrowingDistance']], | |
// ... lines 20 - 23 | |
) | |
// ... lines 25 - 27 | |
class UserApi | |
{ | |
// ... lines 30 - 48 | |
} |
Copy that, put a "de" on the front of it, and now when we try it:
symfony php bin/phpunit --filter=testPostToCreateUser
Yup! flameThrowingDistance
is "1" - so it is not writable, and down here... it's not readable either. Sweet.
So this is just a different option that should work the same as ApiProperty
... though I have seen complex cases where this context option worked when the ApiProperty
solution did not. Anyway, delete those.
The #[Ignore] Attribute
The last way to ignore a field - if you want to ignore it completely - is to add an attribute called... #[Ignore]
! This comes from Symfony's serializer system.
// ... lines 1 - 10 | |
use Symfony\Component\Serializer\Annotation\Ignore; | |
// ... lines 12 - 25 | |
class UserApi | |
{ | |
// ... lines 28 - 45 | |
public int $flameThrowingDistance = 0; | |
} |
When we try the test:
symfony php bin/phpunit --filter=testPostToCreateUser
Perfect: It is not writable nor readable. Cool!
Alrighty, let's hit the reset button on all that dummy code. Get rid of the #[Ignore]
... and let's see if we have any extra use
statements up here. Then, over in our processor, remove the ->dump()
... and in our test, get rid of that extra field and the other ->dump()
. All clean!
Avoiding Writable on the Identifier
On this topic of readable and writable, right now, we can actually change the id
field in a PATCH
request. Watch: set this to 47
... which I just made up, and... it fails with a 500 error!
Open up the error:
Entity 47 not found.
That's coming from our state processor. It's coming from down here... it reads the id
up here and tries to find that in the database... but it's not there. If we had used a valid id
, it would have queried for that other User
entity... then we would have updated the properties on that!. That's a big no-no. At least with how our code is written, by making id
writable, we're allowing the user to change which user is being modified.
Let's look at the full flow. First, our provider found the original User
entity with the id
from the URL... and mapped that over to a UserApi
object. Good so far. Then, during deserialization, the id
on the UserApi
object was changed to 47
. Finally, in the state processor, we tried to query for an entity with id=47
... which is ultimately what we would have saved to the database.
Over in UserApi
, to fix this, above id
, add writable: false
.
// ... lines 1 - 24 | |
class UserApi | |
{ | |
readable: false, writable: false, identifier: true) | (|
public ?int $id = null; | |
// ... lines 29 - 45 | |
} |
Or we could use the #[Ignore]
attribute that we saw a second ago... since we don't want this to be readable or writable. The id
property helps generate the IRI... but it's not really part of our API.
If we run that test now... it passes because it's ignoring the new id
field in the JSON. Life is good.
While we're here, in UserApi
, there are two other properties that, for now, I want to make read-only. Above $dragonTreasures
, make this writable: false
... though we are going to make this writable later.
// ... lines 1 - 24 | |
class UserApi | |
{ | |
// ... lines 27 - 42 | |
writable: false) | (|
public array $dragonTreasures = []; | |
// ... lines 45 - 47 | |
} |
Below, do the same for $flameThrowingDistance
... because this is a fake property that we're generating as a random number.
// ... lines 1 - 24 | |
class UserApi | |
{ | |
// ... lines 27 - 42 | |
writable: false) | (|
public array $dragonTreasures = []; | |
// ... line 45 | |
writable: false) | (|
public int $flameThrowingDistance = 0; | |
} |
Using "security" to hide/show a field
Oh, and another way to control whether a field is readable or writable is the security
attribute. For example, if $flameThrowingDistance
were only readable or writable if you had a certain role, you could use the security
attribute to check for that. We'll see this a bit later.
Different Input/Output Classes?
Finally, I want to mention one last strategy for conditional fields... even though we won't do it. If the input JSON and output JSON for your API resource start to look really different, it is possible to have separate classes for your input and your output. You could have something like a UserApiRead
and a separate UserApiWrite
. The UserApiRead
would be used for the read operations like GET
and GET
collection. And UserApiWrite
would be used for PUT
, PATCH
, and POST
operations.
Though, full disclosure: I haven't actually played with this yet. It should work, but there are probably some road bumps and details along the way. One other thing to keep in mind is that, on UserApiWrite
, you could, in theory, set the output
to UserApiRead
. That would allow the user to send data in the format of UserApiWrite
, but be returned JSON from UserApiRead
. But, to make this work, after saving the UserApiWrite
in your state processor, you would need to turn it into a UserApiRead
and return that.
Anyway, that's definitely more advanced, but if it's interesting, and you try it, let me know!
Next up: Let's polish our new API resource by re-adding validation and security.
Actually, trying to separate reading and writing didn't work for me, because when I add
stateOptions: new Options(entityClass: SomeClass::class)
,api platform serialization listener sets force_resource_class context parameter to operation's class value and it would be dto for writing. Then I'm getting a serialization error, saying something like 'Cannot access x field on dto object' on non-intersecting fields. But if I try old-fashioned way without dtos, I cat use custom controller getting entity of one class and returning entity of another one without problems, because in this case there is no need in stateOptions