Chapters
39 Chapters
|
4:45:13
|
Login to bookmark this video
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
27.
Resource Metadata Factory: Dynamic ApiResource Options
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Subscribe to jump to this part in the video!
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.
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.21.6
"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
}
}
37 Comments
Hello,
Since API Platorm 2.7 ResourceMetadataFactoryInterface is deprecated and has been replaced with ResourceMetadataCollectionFactoryInterface.
I havent come arround to explore the new Interface, but it seems to be more than just a "switch interface and yeet" kind of update.
Are there any plans to update this course to news inside API Platform?
Created a working update for the Metadata changes for 3.0 while coding along, slightly refactored to work with the new Collection interface. It may help someone else too. Differences = Class name & implementation, HTTP verb from the operation method (the operation name is not really usable unless you play with regex), also changed method name of the helper to align with the API Platform naming convention.
@Micoto THANK YOU! This was SUPER helpful! I expanded on your logic a little.
I added another private function so it was easy to modify any of the
Operationsettings. As well added a custom exception class.This is awesome! Thank you for posting it! ❤️
Hey @ties8
I think so too!
Yup :). It's high on my list to get a new series re-recorded using fresh API Platform 3!
Cheers!
Hi,
I totally get confused. Why I need context builder file if all logic now in metadata factory?
I add extra code to check admin role and docs and implementation work well without context builder
Hey @Александр Хохлачёв!
Hmm, that's interesting! So the issue is (or "should be" and was when I built this tutorial) that the metadata factory is called while your container is compiling. In other words, in production, this code would be run just ONCE (probably during "bin/console cache:warmup") and its results are stored in the container statically. This is not a method that is called on every request.
So, either that has changed. OR, you're getting lucky: when you refresh (or make a request), the container is rebuilding on that request, which reads that you are currently an admin, and builds the cache using that data. If that's the case, after getting it to work once, I would then try to make a request (or refresh) as an anonymous user to see if you are *still* seeing the fields from admin:read/admin:write.
Let me know what you find out :).
Cheers!
Hi,
yes it work on refresh and rebuild showed docs
Hey @Александр Хохлачёв!
Cool! Then do it the way you've done it :). When I originally coded this, the metadata factory was not called on every request, and so couldn't contain any logic about the request of the user.
Cheers!
thx
Hi!
I am adding groups dynamicaly depending of user roles with help of ContextBuilder. For example:
user:read:admin
user:read:editor
user:read:manager
Every thing works fine except docs. I thought I could have correct documentation for logged in user.
I added session based authentication for the docs page, and I see logged in user in debug pannel.
I thought I could use same trick with Resource Metadata Factory as with ContextBuilder. I injected Security sevice and call isGranted method. But I got error:
When I dumped $this->security->getUser(), I got null :(
Is there any possibility to get logged user in Resource Metadata Factory class?
Hey php-programmist!
This is a really excellent question :). The problem is that the "resource metadata" (i.e. the collection of all of the API Platform annotations, property information, groups that are on each property, etc) is built during "compile" time of the container. So, it's built just *once* "per deploy" and it's built before it ever handles the first request. This is by design for performance: there's no reason to be re-parsing metadata on every request.
So... this means that we can't make the Resource Metadata Factory dynamic based on anything related to the request of the user. Instead, the way to customize the documentation based on the user is to "decorate" that class that builds the documentation. The process looks like this:
A) To build the documentation, it actually builds a JSON representation of the "documentation". In the same way that you might have a CheeseListing, which goes through the normalization a& serialization process to be turned into JSON, the documentation itself goes a normalization process - specifically it uses a class called DocumentationNormalizer - https://github.com/api-plat...
B) Ultimately, the DocumentationNormalizer reads information from the resource metadata factory and eventually returns a huge array of Open API data (which is later turned into JSON). The Swagger UI you see is built on this.
So by hooking into the DocumentationNormalizer, you can change the OpenAPI JSON that's returned based on the user... and thus "affect" the Swagger documentation. I've never personally done this - and my guess is that it's non-trivial, but this is the path :).
Let me know if you have any success.
Cheers!
Sorry, something might have gone wrong with me previous post... it doesn't show up. I think it is possible to keep the docs correct at least in terms of user dependent fields which is also mentioned in the second paragraph of the next chapter <a href="https://symfonycasts.com/screencast/api-platform-security/uncached-metadata#making-our-resource-metadata-factory-not-cached">"Making our Resource Metadata Factory Not Cached"</a>.
I have implemented a getRoleSpecificGroups() function in the AutoGroupResourceMetadataFactory which first checks the tokenStorage because the function is called during compilation and no token exists. If a user is authenticated it adds the role prefix (admin:, controller:, etc) to the default groups. The schema and example values are now correct in the swaggerUi and docs.json for users with different roles. Nevertheless im not shure if the hydra-documentation docs.jsonld is correct (i am not so familliar with it) because the admin-client is still broken but i am not sure if its functionality is realy dynamic or just generated at compilation time when no user is logged in. In <a href="https://github.com/api-platform/core/issues/2719#issuecomment-493445142">https://github.com/api-platform/core/issues/2719#issuecomment-493445142</a>) the discussion is about completely hide api endpoints for users without privileges which is not supported at the moment and as allready mentioned non-trivial.
Hey Jascha Lukas Gugat !
Ah, sorry about that - I'm not sure what happened. My guess is that you're referring to the comment that starts with "But I have got another problem". If I'm correct, that IS showing up now - it's possible it was flagged temporarily for some reason.
Duh, of course! I had honestly completely forgotten about this fact even though it's from my own course. This level of API Platform is very deep :).
Hmm. I'm not sure. I believe (and you could temporarily hack a
die()into this class to be sure) that when the Hydra documentation is generated, the DocumentationNormalizer::normalize() method is called - https://github.com/api-platform/core/blob/86a2a4d1724324cb888f00ba0570dc6072cdb715/src/Hydra/Serializer/DocumentationNormalizer.php#L72 - identical to what happens when you generate the OpenAPI documentation (that happens in a class with the same name - https://github.com/api-platform/core/blob/2.6/src/Swagger/Serializer/DocumentationNormalizer.php).If I'm correct, then, in theory, you should be able to generate user-specific documentation - that class uses the "resource metadata factory" - https://github.com/api-platform/core/blob/86a2a4d1724324cb888f00ba0570dc6072cdb715/src/Hydra/Serializer/DocumentationNormalizer.php#L78 - just like open api's DocumentationNormalizer does.
So, I don't have an answer for you - but I do have some spots that you could dig into to find out more :).
Cheers!
But i have got another problem:
I am using superclasses and single table inheritance for my entitiies and by using the previously described method with the uncached Ressource Metadata Factory make my documentation works perfectly for all normalization operations (consider all normalization groups from parants, grandparents, etc) but the denormalization groups are only considered from the final entity. It is so curious because the inherited denormalization groups are working as expected for the functionality but not for the generation of the docs and for the normalization groups both works as expected. Is there something i am missing on mapped superclass, inhertiance of desirializaition operations groups, during the documentation generation?
Hey, everyone!
So, I'm developing an API and I was hoping if you could help me make a decision.
When it comes to authorization, I wanted to create groups related to user roles, so I could easily, and intuitively, give or take access to a property/operation depending on the user role, even though it may require a lot of groups per property, but I think it is a "necessary evil" if I want to have this kind of versatility. Something like
role-billing:readorrole-owner:write.I think that way because, as an example, there may be a ROLE_BILLING that has access only to billing information, a ROLE_MANAGER that can only manage ad campaigns and a ROLE_OWNER that can do both.
The authentication method is through a Bearer Token and it is stateless. Because of this, the documentation will never reflect the "logged in" user, because there is no session and authentication happens via every request. So if I created the groups via the Resource Metadata Factory it would be a mess, basically.
So I was thinking that I could create the role groups via a Context Builder and then groups just for documentation purposes via the Resource Metadata Factory, something like
docs:user:read.That way I could control how I want to show the documentation, and let authorization do the rest, but I fear that I'm overcomplicating things and just creating a mess.
Another way would be to create re-usable Voters for each role and attach them to the properties, but I'm not sure if that is a better solution.
I'm new to Symfony and API Platform, so I still struggle with this decisions because I may not be thinking of the trade-offs in the long term due to my inexperience.
What is your opinion regarding this?
Thank you!
Hi André P.!
Nice to chat with you! Overall, I like your idea and, indeed, it is quite possibly a "necessary evil" if you want to have this level of flexibility.
One quite note before I answer your other questions: when you add the role groups via the ContextBuilder, IF you've decided to leverage role_hierarchy in security.yaml, then don't forget that you'll need to take that into account. For example, if ROLE_OWNER "inherits" both ROLE_BILLING and ROLE_MANAGER, then you'll want to make sure you "resolve" this so that if I ONLY have ROLE_OWNER, we also correct add ROLE_BILLING and ROLE_MANAGER to the groups. There's a service that can help with this: https://stackoverflow.com/questions/8928085/how-to-retrieve-full-role-hierarchy-in-symfony/49192974#answer-49192974
I like the idea of adding the correct roles to the context (based on the user) in the ContextBuilder. 👍 for that. So it sounds like the only/main problem is how to display the documentation. Is your goal to display ALL the possible fields in the documentation? If so, hmm. Yea, the only - and cleanest way - that I can think to do this is your idea: (A) make sure that every operation has some group - like
docs:user:read(which you could do manually, but simpler to do via a resource metadata factory), (B) make sure every field that is EVERY potentially exposed in the API also has this group (I think you would need to do this by hand on each property - you could potentially decorate the serializer's "class metadata factory" service, but you would still need a way to know if a property should EVER be exposed vs is internal... potentially you could do that automatically by checking to see if the property has at LEAST one other group...) and (C) remove thedocs:user:readclass via a context builder so that this group doesn't actually cause access to the operations and properties.So, my thinking is that (1) this is indeed pretty complex but that (2) at least I can't think of a better way of doing it :). Do your best to keep it simpler along the way when you can - but this seems like a good direction.
Cheers!
Hi weaverryan ,
Sorry for the late reply, but I was quite busy this last few days.
First of all, thank you for noting me about how the role hierarchy inheritance works. It already helped me dealing with voters and leveraging a lot of code. Awesome!
Regarding my initial question, your opinion made me think more about it.
My intention was to show **only** the documentation with the endpoints and data I wanted the "public" to see, but then I realized (like you said in some of your videos) that what I was doing was kind of "security through obscurity", and that makes no sense.
Since the documentation is targeted for development purposes, if I really don't want it to be public I'll just make it accessible in the dev environment.
I'll stick with the role groups created in the ContextBuilder and let the documentation be everything the API has to offer.
Once again, thank you very much for the help!
PS: Love the new dark theme!
Hey André P.
We're happy to know you are liking our dark theme. It required more effort than it may appear :)
Great work, really.
Not sure how to describe it, but feels really smooth, hahah.
I even like the attention to detail of changing the font weight, not just the color.
Once again, great work!
Hey Ryan! Is there a way to get rid of this model-noise from swagger. I mean, when I offer a company a enterprise level custom API that displays exact their business logic and have nice and exact documentation - this would not be something they would accept so easy in their documentation.
Hey Michael B.!
Sorry for the slow reply! Yea, that's a great question. I'd say 2 things:
1) The Swagger UI is ultimately generated from your OpenAPI spec. So, in a pinch, you could generate your OpenAPI spec, modify it, then use Swagger UI to read that new spec. I'm leaving out the details, but you probably get the idea on a high level. However, you'd probably need to be careful, if you're removing "excess models", to update other parts of the document that refer to them.
2) The proper fix would be to tell ApiPlatform to stop doing this :). I have NO idea if this is possible - and the logic in this area is pretty complex. I believe the definitions are added here - https://github.com/api-platform/core/blob/a9178865c2074b7db1dcbc5c78cb4fa39f4ada54/src/JsonSchema/SchemaFactory.php#L115 - and to remove the duplicates, you could modify this function - https://github.com/api-platform/core/blob/a9178865c2074b7db1dcbc5c78cb4fa39f4ada54/src/JsonSchema/SchemaFactory.php#L242
Buuuuut, that is (A) just a guess (you could verify by hacking in that function) and (B) that's a private property anyways! Though, it looks like you may be able to leverage this line - https://github.com/api-platform/core/blob/a9178865c2074b7db1dcbc5c78cb4fa39f4ada54/src/JsonSchema/SchemaFactory.php#L256 - by adding a
openapi_definition_nameoption or (swagger_definition_name) as described here - https://api-platform.com/docs/core/openapi/#changing-the-name-of-a-definition - though, looking at the code, I kind of don't see how that all connects (but hopefully I'm missing something).So, unless I'm missing something, it's not a super great answer. But let me know if this helps.
Cheers!
Hello ! Do you recommend working with this "generator" on every project with API Platform? Do you ?
Does it cover all the needs that we might have during development? I have heard of "SubResources", I never used them, but if I were to use it, would the generator work?
Or is it still better to do everything by hand?
Thank you !
Hey Kiuega!
I can't advice you on this with... too much certainty because I haven't used the generator in any significant way. But, my impression is that "yes", it does cover all cases. And of course, it's basically just generating a SDK - a set of tools to help make your life simpler. And so, if it doesn't work for some case, you can always just not use it.
For a bit more context, we have a small microservice architecture here at SymfonyCasts with about 3 microservices that expose an API and are consumed by SymfonyCasts.com and the other microservices. Buuut, all of this predates ApiPlatform (or at least, predates Api Platform v2), and so they don't use ApiPlatform. If I were building them today, I would use ApiPlatform and I would use the code generator. That would give me an SDK & model classes for each API that I could re-use in all the other services that need to talk to it, which is pretty darn convenient. And if I update my API (it's a private API, so I don't have backwards-compatibility concerns), I'd just update the generated SDK!
Anyways, I hope this helps. My advice would be to give it a shot :).
Cheers!
Hello,
I have been working with AutoGroup which looks great but I feel it lacks features.
There is no automatic group with the SubResource
If we define groups in normalizationContext and denormalizationContext, these are no longer taken into account, which is managed for example in the case of inheritance or the use of Traits
Hey Philippe Bauwens!
Yea, this was a bit of an experiment for me - it has its pros and cons.
Are you referring to if you added a
normalizationContext=""option above the class inside a@ApiResourceannotation? If so, I can't remember for sure, but I think it should still work - the resource metadata factory we create - https://symfonycasts.com/screencast/api-platform-security/resource-metadata-factory#codeblock-b610d31864 - is checking for the existence of this config, and then merging the "default" groups into any existing config. But, I think I may not be fully understanding your situation - can you tell me more about how you're using inheritance and traits related to normalizationContext and denormalizationContext.Can you tell me more about what behavior your expecting with sub resource vs what behavior you're seeing? An example would be ideal :).
Cheers!
I am referring to normalizationContext in the annotation indeed. I have done tests and it does not work.
I worked yesterday to review the AutoGroupResourceMetadataFactory class so that it gives me satisfaction. Here is the result.
With these changes, I support the normalizationContext annotation as well as subresourceOperations, in addition I have added the "read" and "write" groups to allow the use of generic cases.
In addition, I personally removed the lowercase setting because I admit that I am not interested in adding a shortname in all of my entities and if I have a "PrivateMessage" entity, have the group all in lowercase is not very readable (question of choice)
The only thing I don't have is an automatic context for subresourceOperations. I don't know how to determine that "api_foo_bar_get_subresource" will be the Foo:read group.
</code
(I don't know why my previous comment was marked as spam)
Hi Philippe Bauwens!
Sweet! Thanks for sharing your improved version of the auto groups class :). Unfortunately, I don't work with sub-resources too often, so I'm also not sure about that part either :/.
Cheers!
Hi I am following the tutorial and applying my own logic, based on what I am building, but when I configure the service in services.yaml and it looks like this
and mi AutoGroupResourceMetadataFactory class looks like this
but when I run my unit tests they all give me the following error:
Error: Object of class ApiPlatform\Core\Metadata\Resource\Factory\FormatsResourceMetadataFactory could not be converted to stringand if I unconfigure the service (that is, I comment it on service.yaml) all the tests run fine... Any advice :( Regards and thanks in advance...!!!
Hi Toshiro!
Nice to chat with you! Oh, and I see the error and it's SO subtle. In your class, here it is:
See it? You have an extra $ sign. Unfortunately, PHP is so friendly that this is allowed. But it looks like you're trying to convert the
$decoratedargument into a string... so that you can then call a dynamic method that matches that strings name on$this. That's why you see that exact error 🙃Anyways, get rid of that extra $, and you're going to be very happy :).
Cheers!
OMG!!! ( 0 _ 0 ) wow... thanks!! what a silly mistake !! God for these things I love programming!! lol ─=≡Σ((( つ◕ل͜◕)つ well beginner stuff, haha lol. Thank you for such a kind answer, sorry for stealing some time with something so silly!! Cheers!!
Hahaha, my pleasure! I'm happy it was something simple ;)
there Hi, in AutoGroupResourceMetadataFactory::getDefaultGroups, in return line, I think we should return $operationName in lowercase, since we are using "cheese:collection:post" (post is in lowercase). I'm not sure why there was no error in the video, but I got it. Maybe it's because I am using newer version both symfony and apiPlatform.
Hey Greta L.
Ohh, that's sound likely, ApiPlatform may have capitalized the
$operationName. If it works for you after lowercasing it, then I think that's what happened. Thanks for reporting this.Cheers!
Hey MolloKhan!
Hmmm. So I checked into this further to see if there might be some change in API Platform. By using the exact same code as this tutorial,
$operationNameinsidegetDefaultGroups()is always (already) in lowercase - like "get", "post" or "delete". I think upgraded to API Platform 2.5, and got the same result. It certainly doesn't hurt to lowercase$operationName- but as far as I can tell, it's already always lowercase. Are you seeing something different?Cheers!
Hello,
You said:
But there is no code block on this page, or did I miss something?
Hey Rakodev
You are right. Code blocks for this page haven't been added yet. They will be tomorrow. Sorry for the delay!
"Houston: no signs of life"
Start the conversation!