Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Normalizer: Object-by-Object Dynamic Fields

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 now know how to add dynamic groups: we added admin:read above phoneNumber and then, via our context builder, we're dynamically adding that group to the serialization context if the authenticated user has ROLE_ADMIN.

So... we're pretty cool! We can easily run around and expose input our output fields only to admin users by using these two groups.

But... the context builder - and also the more advanced resource metadata factory - has a tragic flaw! We can only change the context globally. What I mean is, we're deciding which groups should be used for normalizing or denormalizing a specific class... no matter how many different objects we might be working with. It does not allow us to change the groups on an object-by-object basis.

Let me give you a concrete example: in addition to making the $phoneNumber readable by admin users, I now want a user to also be able to read their own phoneNumber: if I make a request and the response will contain data for my own User object, it should include the phoneNumber field.

You might think:

Ok, let's put phoneNumber in some new group, like owner:read... and add that group dynamically in the context builder.

That's great thinking! But... look in the context builder, look at what's passed to the createFromRequest() method... or really, what's not passed: it does not pass us the specific object that's being serialized. Nope, this method is called just once per request.

Creating a Normalizer

Ok, no worries. Context builders are a great way to add or remove groups on a global or class-by-class basis. But they are not the way to dynamically add or remove groups on an object-by-object basis. Nope, for that we need a custom normalizer. Let's convince MakerBundle to create one for us. Run:

php bin/console make:serializer:normalizer

Call this UserNormalizer. When an object is being transformed into JSON, XML or any format, it goes through two steps. First, a "normalizer" transforms the object into an array. And second, an "encoder" transforms that array into whatever format you want - like JSON or XML.

When a User object is serialized, it's already going through a core normalizer that looks at our normalization groups & reads the data via the getter methods. We're now going to hook into that process so that we can change the normalization groups before that core normalizer does its job.

Go check out the new class: src/Serializer/Normalizer/UserNormalizer.php.

... lines 1 - 8
class UserNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function normalize($object, $format = null, array $context = array()): array
{
$data = $this->normalizer->normalize($object, $format, $context);
// Here: add, edit, or delete some data
return $data;
}
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof \App\Entity\BlogPost;
}
public function hasCacheableSupportsMethod(): bool
{
return true;
}
}

This works a bit differently than the context builder - it works more like the voter system. The serializer doesn't have just one normalizer, it has many normalizers. Each time it needs to normalize something, it loops over all the normalizers, calls supportsNormalization() and passes us the data that it needs to normalize. If we return true from supportsNormalization(), it means that we know how to normalize this data. And so, the serializer will call our normalize() method. Our normalizer is then the only normalizer that will be called for this data: we are 100% responsible for transforming the object into an array.

Normalizer Logic

Of course... we don't really want to completely take over the normalization process. What we really want to do is change the normalization groups... and then call the core normalizer so it can do its normal work. That's why the class was generated with a constructor where we're autowiring a class called ObjectNormalizer. This is the main, core, normalizer for objects: it's the one that's responsible for reading the data via our getter methods. So... cool! Our custom normalizer is basically... just offloading all the work to the core normalizer!

Let's start customizing this! For supportsNormalization(), return $data instanceof User. So if the thing that's being normalized is a User object, we handle that.

... lines 1 - 9
class UserNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
... lines 12 - 34
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof User;
}
... lines 39 - 48
}

Now we know that normalize() will only be called if $object is a User. Let's add some PHPDoc above this to help my editor.

... lines 1 - 18
/**
* @param User $object
*/
public function normalize($object, $format = null, array $context = array()): array
... lines 23 - 50

The goal here is to check to see if the User object that's being normalized is the same as the currently-authenticated User. If it is, we'll add that owner:read group. Add that check on top: if $this->userIsOwner($object) - that's a method we'll create in a minute - then, add the group. The $context is passed as the third argument... and we're passing it to the core normalizer below. Let's modify it first! Use $context['groups'][] = 'owner:read.

That's lovely! A normalizer is only used for... um... normalizing an object to an array - it is not used for denormalizing an array back into an object. That's why we're always adding owner:read here. If you wanted to create this same feature for denormalization... and add an owner:write group.. you'll need to create a separate denormalizer class. There's no MakerBundle command to generate it, but the logic will be almost identical to this... and you can even make your one normalizer class implement both NormalizerInterface and DenormalizerInterface.

Oh, also, we don't need to check for the existence of a groups key on the array because, in our system, we are always setting at least one group.

Let's add that missing method: private function userIsOwner(). This will take a User object and return a bool. For now, fake it: return rand(0, 10) > 5.

... lines 1 - 35
private function userIsOwner(User $user): bool
{
return mt_rand(0, 10) > 5;
}
... lines 40 - 46

And... I think that's it! Like with voters, this is a situation where we don't need to add any configuration: as soon as we create a class and make it implement NormalizerInterface, the serializer will see it and start using it.

So... let's take this for a test drive! Back on the docs, I'm currently not logged in. Let's refresh the page... and create a new user. How about email goudadude@example.com, password foo, same username, no cheeseListings, but with a phoneNumber. Execute and... perfect! A 201 status code. Copy that email... go back to the homepage.. and log in: goudadude@example.com, password foo and... go!

Cool! Now that we're authenticated, head back to /api. Yep, the web debug toolbar confirms that I'm a "gouda dude". Let's try the GET operation to fetch a collection of users. Because of our random logic, I'd expect some results to show the phoneNumber and some not. Execute and... hey! The first user has a phoneNumber field! It's null... because apparently we didn't set a phoneNumber for that user, but the field is there. And, thanks to the randomness, there is no phoneNumber for the second and third users.

Tip

If you start a new API Platform project, instead of seeing phoneNumber: null, the field is missing. This is due to a change in API Platform 2.5: if your resource supports the PATCH operation (which is on by default in 2.5), then null fields are "omitted". It's no big deal - just don't let it surprise you!

If you try the operation again... yes! This time the first and second users have that field, but not the third. Hey! We're now dynamically adding the owner:read group on an object-by-object basis! Normalizers rock!

But... wait a second. Something is wrong! We're missing the JSON-LD fields for these users. Well, ok, we have them on the top-level for the collection itself... and even the embedded CheeseListing data has them... but each user is missing @id and @type. Something in our new normalizer killed the JSON-LD stuff!

Next, let's figure out what's going on, find this bug, then crush it!

Leave a comment!

55
Login or Register to join the conversation
Kiuega Avatar

Hello, when I put the 'owner: read' group on phoneNumber, it no longer appears when creating a user, exactly like http://disq.us/p/24ddhmr

I tried, as you ask http://disq.us/p/24e22de to always return 'true' on the 'userIsOwner' function. It hasn't changed anything.

However, if on the 'phoneNumber' property, I put the 'user: read' group back, it works. So I do not understand at all where the problem is coming from. My code seems compliant though:

User entity : https://gist.github.com/bas...
Normalizer : https://gist.github.com/bas...
AutoGroupResourceMetadataFactory : https://gist.github.com/bas...

Do you know where the problem can come from?

When in doubt, I cleared the cache, but nothing changes
I'm using Symfony 5.2.9 and API Platform 2.5

Reply

Hey again Kiuega!

You can see that I'm catching up on the tough questions today ;).

Hmm. Ok, I have some questions!

> when I put the 'owner: read' group on phoneNumber, it no longer appears when creating a user

By "no longer appears", you mean in the API response, correct? You're not talking about whether or not the field shows up in the documentation. I'm 95% sure this is what you meant (it matches the other issue), but I'm just checking :).

And, does the phoneNumber field show up if you simply *fetch* a user (i.e. phoneNumber is not in the response after POST /api/users to create a user, but it DOES show up if you GET /api/users/1 to fetch your user). The answer to this will, I think, help discover the issue.

> I tried, as you ask http://disq.us/p/24e22de to always return 'true' on the 'userIsOwner' function.

Just to make sure, have you verified that userIsOwner() *is* being called? Or, another way to ask this is: have you verified that the normalize() method in your UserNormalizer is being called?

Otherwise, I don't see any obvious problems with your code. But I'm sure we can debug it!

Cheers!

1 Reply
André P. Avatar
André P. Avatar André P. | weaverryan | posted 1 year ago | edited

Hey, weaverryan!

The exact same thing was happening to me and I just spent hours trying to figure it out.

The phoneNumber field was not showing no matter what. Tried every group and endpoint and nothing.
So I tested with another field, username, and it was working as expected, which was sooo confusing.

After a lot of debugging I found out that the phoneNumber was simply not showing because it was NULL in the database. If I set a value, it works as expected.

Is this the expected behaviour? It seems weird to me that a field does not appear in a response if it is NULL.
Or maybe I'm missing something.

I'm using Symfony 5.3 and API Platform 2.6.5.

Thank you!

UPDATE
Upon further investigation, I did a clean install to check if the same thing was happening, and it is.
So, it appears that any nullable field, if NULL, is not included in any response by default, only when it has a value.

Is this a bug?

Reply

Hey André P. !

Thanks for the detailed information! This is pretty interesting. So, I am not aware of anything in the Symfony serializer itself (ignoring API Platform for a moment) that would do this. In fact, you can see in the docs that null values SHOULD be used, unless you tell it to NOT output null values: https://symfony.com/doc/cur... - and here is the code that looks for that, in case you want to put some debugging code there to see if it's the cause: https://github.com/symfony/...

So... I decided to try things for myself - one more than one person has an issue, it's usually legit. But... I can't reproduce it! I went backwards in the code to right where we fill in the userIsOwner() with real logic - so right at this moment - https://symfonycasts.com/sc... - I also upgraded to Symfony 5.3 and 2.6.5 of API Platform. The code is here: https://github.com/SymfonyC...

And... things worked perfectly! I did NOT set a phoneNumber on my user (so it's null in the database). And, when I request my user data (either via /api/users or /api/users/2 to get the exact record), I DO have phoneNumber: null in the response.

This means that that bad behavior here is a bit of a mystery - I can't explain what you're seeing or why. If you notice anything that looks different in my code vs your code, let me know :).

Cheers!

Reply
André P. Avatar

Hey, @weaverryan !

Thank you for you reply!

But... this is so weird.

Just to be clear, when I spoke of a clean install, I meant just creating a new Symfony project and then composer require api and composer require maker.

I created an entity with some fields (including nullable), POSTed some records and then GET them, so the problem can be reproduced.

But I started to dug deeper. The problem seems to lie in here https://github.com/api-platform/core/blob/6336f6446722cd6728683c36fb70e0074553296d/src/Serializer/SerializerContextBuilder.php#L108-L115

It appears that if there is a PATCH operation, skip_null_values is set to true.
Since the PATCH operation is now enabled by default (contrary to the time this tutorial was created), this end up being the default behaviour on most recent versions.

If the PATCH operation is disabled, it works as expected.

Can you confirm this?

Thank you!

Reply

Hey André P.!

Ahhhhh! That's really excellent digging! It seems they added that code, being careful not to break backwards compatibility... but they sort of DID change the behavior a bit :). I can confirm that you are 100% correct: as soon as I enable PATCH on the User resource, that phoneNumber: null disappeared.

Sooooo, I'll add a note about this to the tutorial so that it doesn't surprise people. Beyond that, is this a problem for you - or was is it more that you just wanted to figure out why our codes were behaving differently (a worthy pursuit on its own!)

Cheers - and thanks for the follow-up - we really like to add notes when needed to keep the experience smooth for everyone!

Reply
André P. Avatar

Hey!

Thank you for the follow-up as well!

At first, I just wanted to figure out what was happening and really understand the reason behind it, but now that I know what is going on, I'm thinking about it.

So, imagine that I'm developing an API that will also be consumed by me (this is, it is a private API that will be used to "feed" a website and/or an app).

It is important for me, who is going to consume the API, to know how I'll get an endpoint response since it is different to check if a property is null or if a property exists.

Since I wanted to use PATCH operations (because, according to specs, as far as I know, it is the right operation for partial updates) I guess I would have to use the latter way, which is no problem, but what is the approach of API Platform? Will it change the behavior of the PATCH operation and start including null properties? Or all other operations will start not including them? It would be nice to know just to avoid eventual breaks in the code. I know there is always a way to prevent this but the problem, I think, lies in not knowing the consistency of the responses in the next version/future.

I actually went searching for what were the specs regarding null properties in API responses and, guess what, there's a big discussion about it, hahah.

I guess that, ideally, null properties should behave exactly the same across all operations and, maybe, have a global configuration option to skip, or not, null values?

Once again, thank you!

Cheers!

Reply

Hey André P.!

> It is important for me, who is going to consume the API, to know how I'll get an endpoint response since it is different to check if a property is null or if a property exists

Definitely :)

> Will it change the behavior of the PATCH operation and start including null properties? Or all other operations will start not including them?

I think that this change - when PATCH is included as an operation, suddenly null values on non-patch operations are treated differently - was likely a mistake by API Platform. In the future, it seems that they want to go in the direction of "all other operations start not including null values". However, unless they make some accidental change, this would not be done in a minor version - it would wait until API Platform 3.0 (possibly in some 2.x version, they might start forcing you to explicitly set some config to "opt into" not including null values. But the point is, it wouldn't happen suddenly - it would be your choice.

> I guess that, ideally, null properties should behave exactly the same across all operations and, maybe, have a global configuration option to skip, or not, null values?

Totally - and my guess is that this is the direction API Platform will go (using PATCH as the new "correct" behavior at some point).

Cheers!

Reply
André P. Avatar
André P. Avatar André P. | weaverryan | posted 1 year ago | edited

Hey, weaverryan.

Thank you for your reply and insight.

Let's see how things go. It certainly is as you say, if there are going to be breaking changes it will probably be in a major version release.

Keep the amazing work!

Thank you!

Reply
Kiuega Avatar

Hello @weaverryan !

Thank you for answering all my questions, it's great!
So, for this problem, Since then, I have advanced (and finished) in API Platform training, suddenly the code is not the same.

But off the top of my head, I can tell you the following:

>By "no longer appears", you mean in the API response, correct? You're not talking about whether or not the field shows up in the documentation. I'm 95% sure this is what you meant (it matches the other issue), but I'm just checking :).

Well, the phoneNumber field did not even appear in the documentation (just like the password field but you had explained why on another video)

>And, does the phoneNumber field show up if you simply *fetch* a user (i.e. phoneNumber is not in the response after POST /api/users to create a user, but it DOES show up if you GET /api/users/1 to fetch your user). The answer to this will, I think, help discover the issue.

Since my current code matches the one we have at the end of part 3 training, I don't know if that will affect, but since we still have the 'owner: read' group for the phoneNumber, I think it won't change anything, so here's what I can get:

GET '/api/users' : http://image.noelshack.com/... (no phoneNumber field in schema with authenticated user)
On the other hand, in the response, I get the phoneNumber field only on the logged in user, which is correct : http://image.noelshack.com/...

And we have the exact same result when I GET '/api/users/{uuid}'.

In the end, the phoneNumber field will never appear in the documentation, but will appear in the response if it is needed. I no longer know if this was the behavior we expected or if there is indeed a problem.

>Just to make sure, have you verified that userIsOwner() *is* being called? Or, another way to ask this is: have you verified that the normalize() method in your UserNormalizer is being called?

Yes of course! :)

To finish, and here we will only have to trust my memory (very selective), but it seems to me that in the rest of the training (part 3), I thought I saw that the same thing was happening on your side, namely that the phoneNumber field did not appear in the documentation but was present in the response when it was necessary.

In the end it will remain a mystery

Reply
Edgar Avatar

Hello,

I have a problem with my own project. Everything was working good with User entity, I was using GET operation as expected, but just after creating UserNormalizer with maker with default code, it's throwing an error:


"hydra:description": "No collection route associated with the type \"App\\Entity\\User\"."

,

I'm using symfony 5.1.8 and ApiPlatform 2.5

itemOperation GET is there as always.

Any advice is appreciated!

UPDATE:

User Entity has a collection


/**
* @ORM\OneToMany(targetEntity="ClientUserRole", mappedBy="user", fetch="EAGER")
*/
private $clientUserRoles;

/**
* @Groups({"user:read"})
* @SerializedName("roles")
*/
public function getClientUserRoles()
{
return $this->clientUserRoles;
}

ClientUserRoles is not an ApiResource, it only has a serialization groups on specific relations


/**
* @ORM\ManyToOne(targetEntity="Role", fetch="EAGER")
* @ORM\JoinColumn(name="role_id", referencedColumnName="role_id")
* @SerializedName("role")
* @Groups("user:read")
*/
private $role;

/**
* @ORM\ManyToOne(targetEntity="Client", fetch="EAGER")
* @ORM\JoinColumn(name="client_id", referencedColumnName="client_id")
* @SerializedName("client")
* @Groups("user:read")
*/
private $client;

Client entity is not an ApiResource, it only has a serialization groups on specific attribute


/**
* @ORM\Column(type="string", length=100, nullable=true)
* @Groups({"user:read"})
*/
private $name;

Role entity is not an ApiResource, it only has a serialization groups on specific attribute


/**
* @ORM\Column(type="string", length=50)
* @Groups("user:read")
*/
private $name;

I noticed that the error is related to clientUserRoles collection on User entity. This doesn't cause problems if a remove the new normalizer. I don't have any custom code on normalizer, just what is created with maker:



namespace App\Serializer\Normalizer;

use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

class UserNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
private $normalizer;

public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}

public function normalize($object, $format = null, array $context = []): array
{
$data = $this->normalizer->normalize($object, $format, $context);

// Here: add, edit, or delete some data

return $data;
}

public function supportsNormalization($data, $format = null): bool
{
return $data instanceof \App\Entity\User;
}

public function hasCacheableSupportsMethod(): bool
{
return true;
}
}
Reply

Hey @Edgar!

This is a super weird error! These custom normalizers in api platform are tricky, because you need to make sure (after you do whatever modifications you need) that you call the Serializer system again so that the normal process can take place. The make:serializer:normalizer command in MakerBundle is a bit generic to the Serializer component in general: it’s designed as a starting point to create a normalizer... but api platform is a bit special.

Basically, here’s my guess at what’s happening: in api platform (as I mentioned), you need to do your work in the normalizer and then call the normalization system again so that api platform can do its normal work. The generated normalizer doesn’t do this - it just calls (iirc) the ObjectNormalizer. So, i would modify the code on that normalizer to do what we do in the next couple of chapters. My guess is that there’s something different enough with your entities and relations that using the object normalizer causes this explosion. I could also be totally wrong - it’s a very odd error. But let me know.

Cheers!

Reply
Eric G. Avatar

I'm just trying to see if I can track it down but I just ran into something similar.. at the end of this chapter we refresh and my entire User section/object/api disapeared... I started with the course files and updated a couple things but was still on symfony 4... definitely something strange around the normalizer. If I remove it things appear to work (though it won't get the new user aware permissions.. I'll update here if I can figure it out.

Reply

Hey Eric G.!

Thanks for sharing! It does make me think that, indeed, something weird is going on. For the entire User section in the API to disappear from a normalizer... I can't even imagine how that would happen. Let me know if you find anything :).

Cheers!

Reply
Eric G. Avatar

It was pretty strange. If I removed the normalizer file and cleared the cache the User api would appear again. Of course I managed to break the whole thing trying to figure it out. I was able to ue the "finish" code from the module and got everything working there so it was either something I did by mistake or a quirk of the exact versions I ended up on

Reply

Ha! Yea, that is SUPER weird. Thanks for following up. If you repeat this again later, let me know.

Cheers!

Reply
akincer Avatar
akincer Avatar akincer | posted 2 years ago

I'm getting 401 "Full authentication is required to access this resource." at the end when trying to do a POST. So far I'm unable to figure out what I might have missed. I compared the code for the User resource and the UserNormalizer to the code in the script and can't find any differences except the code for UserNormalizer at the end of the script here differs significantly with the NormalizerAwareInterface and NormalizerAwareTrait stuff and I don't see any discussion of that change. But regardless it fails the same way in both UserNormalizer instances.

Any hints where I might check?

Reply

Hey Aaron,

"Full authentication is required" means that you're not authenticated fully, for example, you might be authenticated via "remember me" feature. Literally, log out first, then log back in, and try again that endpoint. Do you have access to it now? Symfony has "remember me" feature that allow to logging in users via cookie even if session is expired. But to access some resources you may require full authentication. In web interface when you're trying to access such resource - it will redirect you to login form. But with an API endpoint - it will show you the error, and you have to log in yourself.

I hope it's clearer for you now. Does log out and log in again help?

Cheers!

Reply
akincer Avatar

I understand but in the tutorial Ryan explicitly says:

"So... let's take this for a test drive! Back on the docs, I'm currently not logged in. Let's refresh the page... and create a new user. How about email goudadude@example.com, password foo, same username, no cheeseListings, but with a phoneNumber. Execute and... perfect! A 201 status code. Copy that email... go back to the homepage.. and log in: goudadude@example.com, password foo and... go!"

When he refreshes you can see he's still not logged in and is able to perform the operation anonymously as defined in the POST operation under collectionOperations for the ApiResource declaration as both the course script shows and my code reflects yet it's not being honored.

Reply

Hey @Aaron Kincer!

Hmm. So if you're getting a 401 when you try to make a POST to /users then it means that - for *some* reason - something is fully denying access to this operation. What I mean is, I don't think it's related to your custom normalizer... it seems more that something is fully denying access to the entire operation. This can mostly likely happen due to your security rules inside your @ApiResource annotation on User or via access_control inside of security.yaml - it would most likely be the first, since we haven't really touched any access_control stuff in security.yaml in this tutorial.

So make sure an anonymous user can access the POST collection operation, we explicitly set its security - you can see it in this code block - https://symfonycasts.com/sc... - does your operation have this? Let me know - it's the first place I would look :).

Cheers!

Reply
akincer Avatar

Yes it does indeed have that. I copied and pasted the code just to make sure I didn't have a typo. I'm quite confused what's going on here.

Reply

Hey @Aaron!

Ok, here is how we can debug *where* the access denied is coming from:

1) Make the request that is giving you the 401
2) Go to https://localhost:8000/_profiler
3) Find the request that gave you the 401 - it should be the top request or maybe the 2nd to top request. Click the little "token" link on the right
4) You'll now be on the profiler for that request. Click into the "Security" section
5) Somewhere on this page, you'll see an "Access decision log". What do you see here? Can you take a screenshot? What we're looking for is *which* decision was DENIED, which should help us figure out where that is coming from.

Cheers!

Reply
akincer Avatar

Based on the output it's RoleVoter that's denying. I've gone back quite a few chapters to make sure I didn't miss anything copying the entirety of code for any and every file I could find to make sure I had the latest up to this point. I've compared to code in the "finish" portion of the course code. I cannot get past this so I'm not sure what's going on. Any other suggestions?

Reply
Default user avatar

Here's what it says:

# Result Attributes Object
1 DENIED ROLE_USER
null
"Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter"
ACCESS ABSTAIN
"Symfony\Component\Security\Core\Authorization\Voter\RoleVoter"
ACCESS DENIED
"Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter"
ACCESS ABSTAIN
"App\Security\Voter\CheeseListingVoter"
ACCESS ABSTAIN

Reply

Hey @Aaron Kincer!

Sorry for my slow reply - busy week :p.

So yes, it's the RoleVoter, which means that *something* is denying access based on a "role" - e.g. ROLE_USER or ROLE_ADMIN. But the question is.... where is that coming from? Many things could be checking access for a role - like access_control in security.yaml or even your security expression inside your @ApiPlatform annotations.

Here is a hacky way that we could figure this out:

1) Open the core RoleVoter - https://github.com/symfony/... - in your project

2) Right before the return statement, add this:


if ($result === VoterInterface::ACCESS_DENIED) {
throw new \Exception('Access is being denied!');
}

Now try the endpoint again. When you do, use the same trick to go into the profiler for that request, but this time go into the Exception section on the left. You should now be able to see a stacktrace that shows *who* is calling the RoleVoter. This will hopefully be enough for you to see what code is running this check - but if it's still not clear, take a screenshot and send it over :). I would be looking to see if one of the classes in the stack is ResourceAccessChecker from API Platform (which would point to this coming from some security you have on the @ApiPlatform annotation) or AccessListener (which would mean it is coming from access_control in security.yaml) or something else.

Cheers!

Reply
akincer Avatar

I grabbed the course Finish code and created a new project. I'm not getting the same error so clearly I've done something wrong somewhere so I'll just compare code to figure out what I've done wrong and report back. Thank you for your help.

I never doubted Symfony for a moment. Well, maybe for a moment :)

Reply

Hey @Aaron!

Ah, excellent idea! I also took a look at the deeper exceptions, and I can tell you that the access denied IS coming from API Platform. So, specifically, API Platform is looking at the "security" option of the current "operation" inside of the @ApiResource annotation (this can be specified at the root level of the resource, or overridden on the specific operation) and determining that access should be denied thanks to that expression. Or, you may be using the "access_control" option, which we use in this tutorial because the newer and equivalent "security" wasn't created yet. So, double-check these options in your annotation to help see whaat's going on :).

Cheers!

Reply
akincer Avatar

OK, I've figured out the general gist of what's going on here. After extensive examination of project files with a diff tool I simply could not find demonstrable differences between the Finish code and my WIP training code. This left me scratching my head and I spent more time than I care to admit trying to think of a way to isolate the issue.

Finally I decided to do something very basic -- compare package versions. They were quite different so I decided to do a composer update on the Finish code. This required me to change the repository class construct methods from referencing the RegistryInterface to ManagerRegistry class. But, more importantly, it broke the POST operation with the same error! So clearly there's a behavioral change somewhere. Here are the package changes the update process made. The change that breaks the operation is in there somewhere:

- Removing doctrine/doctrine-cache-bundle (1.3.5)
- Removing fzaninotto/faker (v1.8.0)
- Removing jdorn/sql-formatter (v1.2.17)
- Removing symfony/contracts (v1.1.5)
- Removing symfony/test-pack (v1.0.6)
- Removing zendframework/zend-code (3.3.1)
- Removing zendframework/zend-eventmanager (3.2.1)
- Upgrading api-platform/api-pack (v1.2.0 => v1.3.0)
- Upgrading api-platform/core (v2.4.5 => v2.5.7)
- Upgrading composer/package-versions-deprecated (1.11.99 => 1.11.99.1)
- Upgrading doctrine/annotations (1.10.2 => 1.11.1)
- Upgrading doctrine/cache (1.10.0 => 1.10.2)
- Upgrading doctrine/collections (1.6.4 => 1.6.7)
- Upgrading doctrine/common (2.12.0 => 2.13.3)
- Upgrading doctrine/data-fixtures (v1.3.2 => 1.4.4)
- Upgrading doctrine/dbal (2.10.2 => 2.12.1)
- Upgrading doctrine/doctrine-bundle (1.11.2 => 2.2.1)
- Upgrading doctrine/doctrine-migrations-bundle (v2.0.0 => 2.2.1)
- Upgrading doctrine/event-manager (1.1.0 => 1.1.1)
- Upgrading doctrine/inflector (1.3.1 => 1.4.3)
- Upgrading doctrine/instantiator (1.3.0 => 1.4.0)
- Upgrading doctrine/lexer (1.2.0 => 1.2.1)
- Upgrading doctrine/migrations (v2.1.0 => 2.3.0)
- Upgrading doctrine/orm (v2.7.2 => 2.7.4)
- Upgrading doctrine/persistence (1.3.7 => 1.3.8)
- Upgrading doctrine/reflection (1.2.1 => 1.2.2)
- Locking doctrine/sql-formatter (1.1.1)
- Locking fakerphp/faker (v1.12.0)
- Upgrading fig/link-util (1.0.0 => 1.1.1)
- Upgrading hautelook/alice-bundle (v2.5.1 => 2.8.0)
- Locking laminas/laminas-code (3.5.0)
- Locking laminas/laminas-eventmanager (3.3.0)
- Locking laminas/laminas-zendframework-bridge (1.1.1)
- Upgrading monolog/monolog (1.24.0 => 1.25.5)
- Upgrading myclabs/deep-copy (1.9.1 => 1.10.2)
- Upgrading nelmio/alice (v3.5.7 => 3.7.4)
- Upgrading nelmio/cors-bundle (1.5.6 => 2.1.0)
- Upgrading nesbot/carbon (2.21.3 => 2.41.5)
- Upgrading nikic/php-parser (v4.2.2 => v4.10.2)
- Upgrading ocramius/proxy-manager (2.2.2 => 2.10.0)
- Upgrading phpdocumentor/reflection-common (1.0.1 => 2.2.0)
- Upgrading phpdocumentor/reflection-docblock (4.3.1 => 5.2.2)
- Upgrading phpdocumentor/type-resolver (0.4.0 => 1.4.0)
- Upgrading sebastian/comparator (3.0.2 => 4.0.6)
- Upgrading sebastian/diff (3.0.2 => 4.0.4)
- Upgrading sebastian/exporter (3.1.0 => 4.0.3)
- Upgrading sebastian/recursion-context (3.0.0 => 4.0.4)
- Upgrading symfony/asset (v4.3.2 => v4.3.11)
- Upgrading symfony/browser-kit (v4.3.3 => v4.3.11)
- Upgrading symfony/cache (v4.3.2 => v4.3.11)
- Locking symfony/cache-contracts (v1.1.10)
- Upgrading symfony/config (v4.3.2 => v4.3.11)
- Upgrading symfony/console (v4.3.2 => v4.3.11)
- Upgrading symfony/css-selector (v4.3.3 => v4.3.11)
- Upgrading symfony/debug (v4.3.2 => v4.3.11)
- Upgrading symfony/dependency-injection (v4.3.2 => v4.3.11)
- Locking symfony/deprecation-contracts (v2.2.0)
- Upgrading symfony/doctrine-bridge (v4.3.2 => v4.3.11)
- Upgrading symfony/dom-crawler (v4.3.3 => v4.3.11)
- Upgrading symfony/dotenv (v4.3.2 => v4.3.11)
- Upgrading symfony/event-dispatcher (v4.3.2 => v4.3.11)
- Locking symfony/event-dispatcher-contracts (v1.1.9)
- Upgrading symfony/expression-language (v4.3.2 => v4.3.11)
- Upgrading symfony/filesystem (v4.3.2 => v4.3.11)
- Upgrading symfony/finder (v4.3.2 => v4.3.11)
- Upgrading symfony/flex (v1.9.10 => v1.10.0)
- Upgrading symfony/framework-bundle (v4.3.2 => v4.3.11)
- Upgrading symfony/http-client (v4.3.3 => v4.3.11)
- Locking symfony/http-client-contracts (v1.1.10)
- Upgrading symfony/http-foundation (v4.3.2 => v4.3.11)
- Upgrading symfony/http-kernel (v4.3.2 => v4.3.11)
- Upgrading symfony/inflector (v4.3.2 => v4.3.11)
- Upgrading symfony/maker-bundle (v1.12.0 => v1.24.1)
- Upgrading symfony/mime (v4.3.2 => v4.3.11)
- Upgrading symfony/monolog-bridge (v4.3.3 => v4.3.11)
- Upgrading symfony/monolog-bundle (v3.4.0 => v3.6.0)
- Locking symfony/orm-pack (v2.0.0)
- Upgrading symfony/phpunit-bridge (v4.3.3 => v5.1.8)
- Upgrading symfony/polyfill-intl-idn (v1.11.0 => v1.20.0)
- Locking symfony/polyfill-intl-normalizer (v1.20.0)
- Upgrading symfony/polyfill-mbstring (v1.11.0 => v1.20.0)
- Upgrading symfony/polyfill-php72 (v1.11.0 => v1.20.0)
- Upgrading symfony/polyfill-php73 (v1.11.0 => v1.20.0)
- Upgrading symfony/profiler-pack (v1.0.4 => v1.0.5)
- Upgrading symfony/property-access (v4.3.2 => v4.3.11)
- Upgrading symfony/property-info (v4.3.2 => v4.3.11)
- Upgrading symfony/routing (v4.3.2 => v4.3.11)
- Upgrading symfony/security-bundle (v4.3.2 => v4.3.11)
- Upgrading symfony/security-core (v4.3.2 => v4.3.11)
- Upgrading symfony/security-csrf (v4.3.2 => v4.3.11)
- Upgrading symfony/security-guard (v4.3.2 => v4.3.11)
- Upgrading symfony/security-http (v4.3.2 => v4.3.11)
- Upgrading symfony/serializer (v4.3.2 => v4.3.11)
- Locking symfony/serializer-pack (v1.0.4)
- Locking symfony/service-contracts (v1.1.9)
- Upgrading symfony/stopwatch (v4.3.2 => v4.3.11)
- Upgrading symfony/translation (v4.3.2 => v4.3.11)
- Locking symfony/translation-contracts (v1.1.10)
- Upgrading symfony/twig-bridge (v4.3.2 => v4.3.11)
- Upgrading symfony/twig-bundle (v4.3.2 => v4.3.11)
- Upgrading symfony/validator (v4.3.2 => v4.3.11)
- Upgrading symfony/var-dumper (v4.3.2 => v4.3.11)
- Upgrading symfony/var-exporter (v4.3.2 => v4.3.11)
- Upgrading symfony/web-link (v4.3.2 => v4.3.11)
- Upgrading symfony/web-profiler-bundle (v4.3.2 => v4.3.11)
- Upgrading symfony/webpack-encore-bundle (v1.6.2 => v1.8.0)
- Upgrading symfony/yaml (v4.3.2 => v4.3.11)
- Upgrading theofidry/alice-data-fixtures (v1.1.1 => 1.3.0)
- Upgrading twig/twig (v2.11.3 => v2.14.1)
- Locking webimpress/safe-writer (2.1.0)
- Upgrading webmozart/assert (1.4.0 => 1.9.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 15 installs, 89 updates, 7 removals
- Downloading symfony/maker-bundle (v1.24.1)
- Removing zendframework/zend-eventmanager (3.2.1)
- Removing zendframework/zend-code (3.3.1)
- Removing symfony/test-pack (v1.0.6)
- Removing symfony/contracts (v1.1.5)
- Removing jdorn/sql-formatter (v1.2.17)
- Removing fzaninotto/faker (v1.8.0)
- Removing doctrine/doctrine-cache-bundle (1.3.5)
- Upgrading composer/package-versions-deprecated (1.11.99 => 1.11.99.1): Extracting archive
- Upgrading symfony/flex (v1.9.10 => v1.10.0): Extracting archive
- Installing symfony/translation-contracts (v1.1.10): Extracting archive
- Upgrading symfony/polyfill-mbstring (v1.11.0 => v1.20.0): Extracting archive
- Upgrading symfony/validator (v4.3.2 => v4.3.11): Extracting archive
- Upgrading twig/twig (v2.11.3 => v2.14.1): Extracting archive
- Upgrading symfony/twig-bridge (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/polyfill-php73 (v1.11.0 => v1.20.0): Extracting archive
- Upgrading symfony/polyfill-php72 (v1.11.0 => v1.20.0): Extracting archive
- Installing symfony/polyfill-intl-normalizer (v1.20.0): Extracting archive
- Upgrading symfony/polyfill-intl-idn (v1.11.0 => v1.20.0): Extracting archive
- Upgrading symfony/mime (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/http-foundation (v4.3.2 => v4.3.11): Extracting archive
- Installing symfony/event-dispatcher-contracts (v1.1.9): Extracting archive
- Upgrading symfony/event-dispatcher (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/debug (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/http-kernel (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/filesystem (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/config (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/twig-bundle (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/serializer (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/inflector (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/property-info (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/property-access (v4.3.2 => v4.3.11): Extracting archive
- Upgrading webmozart/assert (1.4.0 => 1.9.1): Extracting archive
- Upgrading phpdocumentor/reflection-common (1.0.1 => 2.2.0): Extracting archive
- Upgrading phpdocumentor/type-resolver (0.4.0 => 1.4.0): Extracting archive
- Upgrading phpdocumentor/reflection-docblock (4.3.1 => 5.2.2): Extracting archive
- Upgrading doctrine/lexer (1.2.0 => 1.2.1): Extracting archive
- Upgrading doctrine/annotations (1.10.2 => 1.11.1): Extracting archive
- Installing symfony/serializer-pack (v1.0.4): Extracting archive
- Installing symfony/service-contracts (v1.1.9): Extracting archive
- Upgrading symfony/security-core (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/security-http (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/security-guard (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/security-csrf (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/dependency-injection (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/security-bundle (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/console (v4.3.2 => v4.3.11): Extracting archive
- Upgrading doctrine/reflection (1.2.1 => 1.2.2): Extracting archive
- Upgrading doctrine/event-manager (1.1.0 => 1.1.1): Extracting archive
- Upgrading doctrine/collections (1.6.4 => 1.6.7): Extracting archive
- Upgrading doctrine/cache (1.10.0 => 1.10.2): Extracting archive
- Upgrading doctrine/persistence (1.3.7 => 1.3.8): Extracting archive
- Upgrading doctrine/instantiator (1.3.0 => 1.4.0): Extracting archive
- Upgrading doctrine/inflector (1.3.1 => 1.4.3): Extracting archive
- Upgrading doctrine/dbal (2.10.2 => 2.12.1): Extracting archive
- Upgrading doctrine/common (2.12.0 => 2.13.3): Extracting archive
- Upgrading doctrine/orm (v2.7.2 => 2.7.4): Extracting archive
- Upgrading symfony/routing (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/finder (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/var-exporter (v4.3.2 => v4.3.11): Extracting archive
- Installing symfony/cache-contracts (v1.1.10): Extracting archive
- Upgrading symfony/cache (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/framework-bundle (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/stopwatch (v4.3.2 => v4.3.11): Extracting archive
- Installing webimpress/safe-writer (2.1.0): Extracting archive
- Installing laminas/laminas-zendframework-bridge (1.1.1): Extracting archive
- Installing laminas/laminas-eventmanager (3.3.0): Extracting archive
- Installing laminas/laminas-code (3.5.0): Extracting archive
- Upgrading ocramius/proxy-manager (2.2.2 => 2.10.0): Extracting archive
- Upgrading doctrine/migrations (v2.1.0 => 2.3.0): Extracting archive
- Upgrading symfony/doctrine-bridge (v4.3.2 => v4.3.11): Extracting archive
- Installing doctrine/sql-formatter (1.1.1): Extracting archive
- Upgrading doctrine/doctrine-bundle (1.11.2 => 2.2.1): Extracting archive
- Upgrading doctrine/doctrine-migrations-bundle (v2.0.0 => 2.2.1): Extracting archive
- Installing symfony/orm-pack (v2.0.0): Extracting archive
- Upgrading symfony/expression-language (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/asset (v4.3.2 => v4.3.11): Extracting archive
- Upgrading nelmio/cors-bundle (1.5.6 => 2.1.0): Extracting archive
- Upgrading fig/link-util (1.0.0 => 1.1.1): Extracting archive
- Upgrading symfony/web-link (v4.3.2 => v4.3.11): Extracting archive
- Upgrading api-platform/core (v2.4.5 => v2.5.7): Extracting archive
- Upgrading api-platform/api-pack (v1.2.0 => v1.3.0): Extracting archive
- Installing fakerphp/faker (v1.12.0): Extracting archive
- Upgrading symfony/yaml (v4.3.2 => v4.3.11): Extracting archive
- Upgrading sebastian/recursion-context (3.0.0 => 4.0.4): Extracting archive
- Upgrading sebastian/exporter (3.1.0 => 4.0.3): Extracting archive
- Upgrading sebastian/diff (3.0.2 => 4.0.4): Extracting archive
- Upgrading sebastian/comparator (3.0.2 => 4.0.6): Extracting archive
- Upgrading myclabs/deep-copy (1.9.1 => 1.10.2): Extracting archive
- Upgrading nelmio/alice (v3.5.7 => 3.7.4): Extracting archive
- Upgrading theofidry/alice-data-fixtures (v1.1.1 => 1.3.0): Extracting archive
- Upgrading doctrine/data-fixtures (v1.3.2 => 1.4.4): Extracting archive
- Upgrading hautelook/alice-bundle (v2.5.1 => 2.8.0): Extracting archive
- Upgrading symfony/translation (v4.3.2 => v4.3.11): Extracting archive
- Upgrading nesbot/carbon (2.21.3 => 2.41.5): Extracting archive
- Upgrading symfony/dom-crawler (v4.3.3 => v4.3.11): Extracting archive
- Upgrading symfony/browser-kit (v4.3.3 => v4.3.11): Extracting archive
- Upgrading symfony/css-selector (v4.3.3 => v4.3.11): Extracting archive
- Upgrading symfony/dotenv (v4.3.2 => v4.3.11): Extracting archive
- Installing symfony/http-client-contracts (v1.1.10): Extracting archive
- Upgrading symfony/http-client (v4.3.3 => v4.3.11): Extracting archive
- Installing symfony/deprecation-contracts (v2.2.0): Extracting archive
- Upgrading nikic/php-parser (v4.2.2 => v4.10.2): Extracting archive
- Upgrading symfony/maker-bundle (v1.12.0 => v1.24.1): Extracting archive
- Upgrading monolog/monolog (1.24.0 => 1.25.5): Extracting archive
- Upgrading symfony/monolog-bridge (v4.3.3 => v4.3.11): Extracting archive
- Upgrading symfony/monolog-bundle (v3.4.0 => v3.6.0): Extracting archive
- Upgrading symfony/phpunit-bridge (v4.3.3 => v5.1.8): Extracting archive
- Upgrading symfony/var-dumper (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/web-profiler-bundle (v4.3.2 => v4.3.11): Extracting archive
- Upgrading symfony/profiler-pack (v1.0.4 => v1.0.5): Extracting archive
- Upgrading symfony/webpack-encore-bundle (v1.6.2 => v1.8.0): Extracting archive

Reply

Hey @Aaron!

Ah! Excellent detective work! My immediate guess would be this guy: Upgrading api-platform/core (v2.4.5 => v2.5.7).

You could try running adding (if it's not there already directly) api-platform/core to your composer.json and setting its version explicitly to "2.4.5". Then run a composer update to downgrade to that version. Then you can see if you the error "goes away". I still can't think of why this would happen, but this seems like the likely cause. There *is* a security difference between API Platform 2.4 and 2.5... but it's just a new feature. What I mean is, in 2.4, you used access_control inside your @ApiResource config to control security. In 2.5, that still exists, but is deprecated. A new security was added (we helped push for that change, the security works a bit better). The point is, if you have access_control, it should work identically in both versions. However, if you have security=, then that would work in 2.5, but do nothing in 2.4.... so... that might explain things? :)

Cheers!

Reply
Default user avatar
Default user avatar Aaron Kincer | weaverryan | posted 1 year ago

It was in fact the 2.5 upgrade. Here's what works:

2.4.5 -> access_control
2.5.x -> security

Having access_control in 2.5 doesn't work.

Once again the problem was in fact me because at some point I obviously ran composer update for reasons. Thanks for your help!

Reply

Hey @Aaron!

Awesome! Good job working through that :). access_control "should" work on 2.5 (it's just deprecated), but it's always possible that some behavior changed between those version. At the very least, there *is* a small difference between access_control and security: access_control is run AFTER your object is deserialized but security is run BEFORE it's deserialized. access_control is really equivalent to security_post_denormalize in 2.5. It's actually nice in 2.5 because you can choose whether you want to run your security check before or after deserialization.

Cheers!

Reply
akincer Avatar

Also I sent screenshots I put together showing the stack traces and emailed them to the email address on the contact page.

Reply
akincer Avatar

I just appreciate your help and love learning new debug tips along the way. Here are the top entries in the stack traces:

HttpException: ExceptionListener
InsufficientAuthenticationException: ExceptionListener
AccessDeniedException: DenyAccessListener

There's nothing under access_control in security.yaml.

Reply
Titoine Avatar
Titoine Avatar Titoine | posted 2 years ago

Isn't it a bit weird to add group in a normalizer? Or is it a common thing?
There is multiple way to add group (Context builder, meta data factory), but it seems to be the only way to add a group to one specific entity.

Reply

Hey @keuwa!

You nailed it :). The three different ways are used based on 3 different needs:

A) Metadata factory: use this if you need to add groups and the logic for adding the groups isn't dependent on the current request (or authenticated user) or the individual object being serialized. The advantage of a context builder is that the result of this is cached.

B) Context builder: use this if you need to add groups based on info from the current request (or authenticated user), but not based on data for an individual object.

C) Normalizer: use this if you need to add groups for an individual object

3 different ways for 3 different situations :). I know, it's a bit complex - especially because creating normalizes is kind of annoying (with the whole calling the inner normalizer and avoiding recursion).

Cheers!

Reply
hacktic Avatar
hacktic Avatar hacktic | posted 3 years ago

Could you please provide the owner:write code?

Reply

Hiya hacktic !

Absolutely :). It's basically the same process, but you *do* need to implement a few interfaces, and instead of working with objects and turning them into something else, you're working with data and a "type" - so the whole process is literally backwards :).

Here's a finished example to try: https://gist.github.com/wea... - I just hacked this together, so please let me know if something doesn't work. The most interesting part might be the "diff" of how I transformed the finished normalizer from this project and added the denormalizer stuff - https://gist.github.com/wea...

Let me know if it helps! Cheers!

Reply
Hannah R. Avatar
Hannah R. Avatar Hannah R. | weaverryan | posted 3 years ago | edited

Thanks a lot weaverryan ,

i used your example and tried to update the phoneNumber field as owner with owner:write set.
I get Statuscode 200, but the field is not updating.
When changing to user:write everything is working as expected and the field is updating.

Any Ideas?

Reply

Hey Hannah R.!

Hmm. What do the groups look like above the phoneNumnber property? Is owner:write one of the groups above it?

Cheers!

Reply
hacktic Avatar
hacktic Avatar hacktic | weaverryan | posted 3 years ago | edited

Hey weaverryan,

ok -> Updating phoneNumber as expected:
@Groups({"admin:read", "owner:read", "user:write"})

nok -> Status Code 200 but not updating value of phoneNumber:
@Groups({"admin:read", "owner:read", "owner:write"})

Reply

Hey hacktic!

Sorry for my slow reply! It's SymfonyCon week. I've just (thanks to your report) realized my mistake, and it's a tricky one that, to be honest, hadn't occurred to me before. The problem is (and you'll see if it you look in the denormalizer code I sent above) the object is deserialized and THEN the group is added. At that point, it's too late - the deserialization has already happened. Adding the group then has (duh Ryan!) no effect.

So, adding the owner:write is much tricker than I suspected. What we *need* to know (even before deserializing) is whether the User/entity that ApiPlatform queried from the database (the one whose "id" is in the URL) is owned by the current user. We can fetch the "current API resource data" out of the Request object. I've updated the gist to reflect this. The last revision will show you the changes - https://gist.github.com/wea... - and this time I fully verified that the code works on the project :).

Let me know if it works for you!

Cheers!

Reply
Hannah R. Avatar
Hannah R. Avatar Hannah R. | weaverryan | posted 2 years ago | edited

Hey weaverryan ,
when using the latest revision of UserNormalizer, it is not possible to create an User anymore.


"hydra:description": "Argument 1 passed to App\\Serializer\\Normalizer\\UserNormalizer::userIsOwner() must be an instance of App\\Entity\\User, null given, called in /Users/hannah/PhpstormProjects/api-platform-security/src/Serializer/Normalizer/UserNormalizer.php on line 104"

UserResourceTest::testCreateUser also fails with the same Error.

I'm confused :)

Reply

Hi Hannah R.!

Sorry for my slow response! Hmm that's really interesting! Am I correct to assume that the method that's calling userIsOwner is the normalize method? Or is it something else? I'm just not sure what "stage" of the tutorial your UserNormalizer is at - I don't think we get to a line 104 in our tutorial (but I could be looking at the wrong spot!).

If normalize() is calling this method with a "null" value for the $object argument (which is what we're passing to $this->userIsOwner(), then yes, something is very wrong :). The normalize method is called when we're taking an object and turning it *into* an array. And normalize() should only be called if supportsNormalization() returns true... and THAT method is checking to make sure that we're dealing with a User object.

Because that doesn't make a lot of senes, my guess is that I'm making some incorrect assumptions about how your UserNormalizer looks. Could you post the code here so I can see what the context around what is calling userIsOwner()?

Cheers!

Reply
Hannah R. Avatar
Hannah R. Avatar Hannah R. | weaverryan | posted 2 years ago | edited

Hi weaverryan,
sorry for the confusion.
I am talking about the version from your last comment, where you implemented owner:write.

https://gist.github.com/wea...

And 1000 thanks for this course!

Reply

Hey Hannah R. !

Ah, duh! I should have checked the context of your message! And sorry for my slow reply again!

Based on your error, it means that this value is null:


$apiResource = $this->requestStack->getCurrentRequest()
->attributes->get('data');

For a POST (create) that actually WILL be null: the "data" key is set by a class called ReadListener when it queries for the object that you are "editing". So, I think my gist is actually mistaken: we need to check if that is null. If it is, then this is a "CREATE" and we should probably *always* add the owner:write group because... whoever is creating this, IS the owner. I've just updated the gist - https://gist.github.com/wea... - let me know if that works!

Cheers!

Reply
Default user avatar
Default user avatar Hans Grinwis | weaverryan | posted 2 years ago | edited

Hello weaverryan ,

Thanks for this course and the owner:write gist. However, I didn't try your approach to getting the object via
$apiResource = $this->requestStack->getCurrentRequest()->attributes->get('data');

The object is already present in the context:

if (
!isset($context['object_to_populate']) ||
$context['object_to_populate']->getOwner() === $this->security->getUser()
) {
$context['groups'][] = 'owner:write';
}

Reply

Hey Hans Grinwis!

Ah! I didn't realize that! That $context holds a lot of treasures and... unfortunately, because it's just an array - I'm not aware of everything that's in there. So yes, my guess is that your way as an easier way to grab this. I'll make a note of it in the gist!

Thanks!

Reply

Hello,

I'm confused because this owner:user thing doesn't work with me.
I don't see any phoneUser at all, when I enable xdebug, I see that's it's going into this line:
$context['groups'][] = 'owner:read';
But there is no any phoneNumber in the GET /users endpoint
maybe because I don't use the same composer.lock?
https://gist.github.com/rak...

Reply

Hey Rakodev!

Hmmm. I don't think the vendors version should make a difference - this functionality is pretty "core" to Symfony's serializer. Can you post your custom normalizer and also your entity class? It might be some small detail that's causing the problem. You could also add a debug right *after* the $this->normalizer->normalize() line to see if the $data contains the phoneNumber field or not (my guess is that it does *not* because you don't see it in the API response, but it's something to check).

Cheers!

Reply

Hello weaverryan ,

When I put the debug point after $this->normalizer->normalize() line I can see that $data doesn't contain a phoneNumber field, but it is present in the $object properties.

My UserNormalizer class:
https://gist.github.com/rak...

My Entity:
https://gist.github.com/rak...

My AutoGroupResourceMetadataFactory:
https://gist.github.com/rak...

1 Reply
Cat in space

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

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}