Chapters
-
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!
Filtering Related Collections
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot 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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere are two places where our API returns a collection of cheese listings. The first is the GET
operation for /api/cheeses
... and our extension class takes care of filtering out unpublished listings. The second... is down here, when you fetch a single user. Remember - we decided to embed the collection of cheese listings that are owned by this user. But... surprise! Our query extension class does not filter this! Why? The extension class is only used when API Platform needs to make a direct query for a CheeseListing
. In practice, this means it's used for the CheeseListing
operations. But for a User
resource, API platform queries for the User
and then, to get the cheeseListings
field, it simply calls $user->getCheeseListings()
. And guess what? That method returns the full, unfiltered collection of related cheese listings.
Careful with Collections
When you decide to expose a collection relation like this in your API, I want you to keep something in mind: exposing a collection relationship is only practical if you know that the number of related items will always be... reasonably small. If a user could have hundreds of cheese listings... well... then API Platform will need to query, hydrate and return all of them whenever someone fetches that user's data. That's overkill and will really slow things down... if not eventually kill that API call entirely. In that case, it would be better to not expose a cheeseListings
property on User
... and instead instruct an API client to make a GET
request to /api/cheeses
& use the owner
filter. The response will be paginated, which will keep things at a reasonable size.
IRIs Instead of Embedded Data?
But if you do know that a collection will never become too huge and you do want to expose it like this... how can we hide the unpublished listings? There are two options. Well... the first is only a partial solution: instead of embedding these two properties... and potentially exposing the data of an unpublished CheeseListing
, you could configure API Platform to only return the IRI string.
As a reminder, each item under cheeseListings
contains two fields: title
and price
. Why only those two fields? Because, in the CheeseListing
entity, the title
property is in a group called user:read
... and the price
property is also in that group. When API Platform serializes a User
, we've configured it to use the user:read
normalization group. By putting these two properties into that group, we're telling API Platform to embed these fields.
If we removed the user:read
group from all the properties in CheeseListing
, the cheeseListings
field on User
would suddenly become an array or IRI strings... instead of embedded data.
Why does that help us? Well... it sort of doesn't. That field would still contain the IRI's for all cheese listings owned by this user... but if an API client made a request to the IRI of an unpublished listing, it would 404. They wouldn't be able to see the data of the unpublished listing... which is great... but the IRI would still show up here... which is kinda weird.
Truly Filtering the Collection
If you really want to filter this properly, if you really want the cheeseListings
property to only contain published listings, we can do that.
Let's modify our test a little to look for this. After we make a GET
request for our unpublished CheesesListing
and assert the 404
, let's also make a GET
request to /api/users/
and then $user->getId()
- the id of the $user
we created above that owns this CheeseListing
. Change that line to createUserAndLogIn()
and pass $client
... because you need to be authenticated to fetch a single user's data.
Show Lines
|
// ... lines 1 - 9 |
class CheeseListingResourceTest extends CustomApiTestCase | |
{ | |
Show Lines
|
// ... lines 12 - 107 |
public function testGetCheeseListingItem() | |
{ | |
Show Lines
|
// ... line 110 |
$user = $this->createUserAndLogIn($client, 'cheeseplese@example.com', 'foo'); | |
Show Lines
|
// ... lines 112 - 125 |
$client->request('GET', '/api/users/'.$user->getId()); | |
Show Lines
|
// ... lines 127 - 128 |
} | |
Show Lines
|
// ... lines 130 - 131 |
After the request, fetch the returned data with $data = $client->getResponse()->toArray()
. We want to assert that the cheeseListings
property is empty: this User
does have one CheeseListing
... but it's not published. Assert that with $this->assertEmpty($data['cheeseListings'])
.
Show Lines
|
// ... lines 1 - 9 |
class CheeseListingResourceTest extends CustomApiTestCase | |
{ | |
Show Lines
|
// ... lines 12 - 107 |
public function testGetCheeseListingItem() | |
{ | |
Show Lines
|
// ... lines 110 - 126 |
$data = $client->getResponse()->toArray(); | |
$this->assertEmpty($data['cheeseListings']); | |
} | |
Show Lines
|
// ... lines 130 - 131 |
Let's make sure this fails...
php bin/phpunit --filter=testGetCheeseListingItem
And... it does:
Failed asserting that an array is empty.
Adding getPublishedCheeseListings()
Great! So... how can we filter this collection? Let's think about it: we know that API Platform calls getCheeseListings()
to get the data for the cheeseListings
field. So... what if we made this method return only published cheese listings?
Yea... that's the key! Well, except... I don't want to modify that method: it's a getter method for the cheeseListings
property... so it really should return that property exactly. Instead, create a new method: public function getPublishedCheeseListings()
that will also return a Collection
. Inside, return $this->cheeseListings->filter()
, which is a method on Doctrine's collection object. Pass this a callback function(){}
with a single CheeseListing
argument. All that function needs is return $cheeseListing->getIsPublished()
.
Show Lines
|
// ... lines 1 - 37 |
class User implements UserInterface | |
{ | |
Show Lines
|
// ... lines 40 - 195 |
public function getPublishedCheeseListings(): Collection | |
{ | |
return $this->cheeseListings->filter(function(CheeseListing $cheeseListing) { | |
return $cheeseListing->getIsPublished(); | |
}); | |
} | |
Show Lines
|
// ... lines 202 - 248 |
} |
If you're not familiar with the filter()
method, that's ok - it's a bit more common in the JavaScript world... or "functional programming" in general. The filter()
method will loop over all of the CheeseListing
objects in the collection and execute the callback for each one. If our callback returns true, that CheeseListing
is added to a new collection... which is ultimately returned. If our callback returns false, it's not.
The end result is that this method returns a collection of only the published CheeseListing
objects... which is perfect! Side note: this method is inefficient because Doctrine will query for all of the related cheese listings... just so we can then filter that list and return only some of them. If the number of items in the collection will always be pretty small, no big deal. But if you're worried about this, there is a more efficient way to filter the collection at the database level, which we talk about in our Doctrine Relations tutorial.
But no matter how you filter the collection, you'll now have a new method that returns only the published listings. Let's make it part of our API! Find the $cheeseListings
property. Right now this is in the user:read
and user:write
groups. Copy that and take it out of the user:read
group. We still want to write directly to this field... by letting the serializer call our addCheeseListing()
and removeCheeseListing()
methods, but we won't use it for reading data.
Instead, above the new method, paste the @Groups
and put this in just user:read
. If we stopped now, this would give us a new publishedCheeseListings
property. We can improve that by adding @SerializedName("cheeseListings")
.
Show Lines
|
// ... lines 1 - 37 |
class User implements UserInterface | |
{ | |
Show Lines
|
// ... lines 40 - 183 |
/** | |
* @return Collection|CheeseListing[] | |
*/ | |
public function getCheeseListings(): Collection | |
{ | |
return $this->cheeseListings; | |
} | |
/** | |
* @Groups({"user:read"}) | |
* @SerializedName("cheeseListings") | |
*/ | |
public function getPublishedCheeseListings(): Collection | |
Show Lines
|
// ... lines 197 - 248 |
} |
I love it! Our API still exposes a cheeseListings
field... but it will now only contain published listings. But don't take my word for it, run that test!
php bin/phpunit --filter=testGetCheeseListingItem
Yes! It passes! To be safe, let's run all the tests:
php bin/phpunit
And... ooh - we do get one failure from testUpdateCheeseListing()
:
Failed asserting that Response status code is 403
It looks like we got a 404. Find testUpdateCheeseListing()
. The failure is coming from down here on line 67. We're testing that you can't update a CheeseListing
that's owned by a different user... but instead of getting a 403
, we're getting a 404
.
The problem is that this CheeseListing
is not published. This is awesome! Our query extension class is preventing us from fetching a single CheeseListing
for editing... because it's not published. I wasn't even thinking about this case, but API Platform acted intelligently. Sure, you'll probably want to tweak the query extension class to allow for an owner to fetch their own unpublished cheese listings... but I'll leave that step for you.
Let's set this to be published... and run the test again:
Show Lines
|
// ... lines 1 - 9 |
class CheeseListingResourceTest extends CustomApiTestCase | |
{ | |
Show Lines
|
// ... lines 12 - 46 |
public function testUpdateCheeseListing() | |
{ | |
Show Lines
|
// ... lines 49 - 56 |
$cheeseListing->setIsPublished(true); | |
Show Lines
|
// ... lines 58 - 74 |
} | |
Show Lines
|
// ... lines 76 - 130 |
} |
php bin/phpunit
All green! That's it friends! We made it! We added one type of API authentication - with a plan to discuss other types more in a future tutorial - and then customized access in every possible way I could think of: preventing access on an operation-by-operation basis, voters for more complex control, hiding fields based on the user, adding custom fields based on the user, validating data... again... based on who is logged in and even controlling database queries based on security. That... was awesome!
In an upcoming tutorial, we'll talk about custom operations, DTO objects and... any other customizations we can dream up. Are we still missing something you want covered? Let us know! In the mean time, go create some mean API endpoints and let us know what cool thing it's powering.
Alright friends, see ya next time!
43 Comments
Hey julien_bonnier!
Yes! That's absolutely possible. We'll talk about it in depth in an upcoming *third* tutorial, but basically what you're looking for is a DTO (their docs aren't super clear on this). The process is:
A) Create a normal, boring class. This will NOT be your "Helper" class - that's a service. You should create something that will just hold the data you need.Your Helper will help populate that data.
B) If you put this class in Entity/, then API Platform will see it instantly (give it the normal @ApiResource annotation). If you put it in some other directory, then add that path in the config file: https://github.com/symfony/...
C) Create a custom data provider and data persister that operate on this class. This is what makes your model/DTO class work even though it's not hooked up with Doctrine.
That should be it - but let me know if you run into trouble :).
Cheers!
Is there a way to set up the yaml config file with a wild card on the path?
something like `'%kernel.project_dir%/src/*/Infrastrocture/DTO'` ?
thanks
PS: Awesome stuff you guys do here :)
Hey Fernando,
> Is there a way to set up the yaml config file with a wild card on the path?
> something like `'%kernel.project_dir%/src/*/Infrastrocture/DTO'` ?
If you're talking about B) from the Ryan's answer - I'm not sure if it possible, wildcards might not work as it should be implemented in bundle's config... but I don't know for sure, you can try at least, who knows, maybe it already works :)
> PS: Awesome stuff you guys do here :)
Thank you for your feedback!
Cheers!
One thing that keeps me wondering is how I could expose some API operation that has nothing to do with a specific entity, like a operation that will call another operations, or just dispatch some command and return only a simple "OK" or "ERROR", something like that... I'm head to the part 3, hope to find it in there.. Thanks!!!!!!!! Great course, again
Ok, I first wrote this and then went to read the other comments... it's already answered to Julien Bonnier!! Perfect
Hey Carlos,
Happy to see you found an answer in the comments!
Cheers!
Esperando a que salga Symfony 5 + ApiPlatform + Auth2.0 + Voters
Hola Cesar C.
Entre este curso y el mas reciente de ApiPlatform (https://symfonycasts.com/sc... ) puedes encontrar casi todas esas tecnologias, solo hace falta Auth2.0 pero gracias por la sugerencia, las tomamos en serio para decidir que cursos desarrollar primero.
Saludos!
Hi team, before starting, thanks for these amazing courses :).
I'm woking on my api, containing a Category entity that get a oneToMany relation with cards. Cards entities got owner property that is related to one User entity.
In my case, i want to filter the cards collection into Category where cards->owner is null or is the logged user.
I'm using the collection-criteria approach. Here is my query :
public static function filteredCardsCriteria(User $user): Criteria
{
return Criteria::create()
->andWhere(Criteria::expr()->eq('parent', $user))
->orWhere(Criteria::expr()->eq('parent', null))
;
}
Here is the getter where i call it inside the category entity.
/**
* @Groups({"category:read"})
* @SerializedName("cards")
*
*/
public function getFilteredCards(): Collection
{
$criteria = CardRepository::filteredCardsCriteria();
return $this->cards->matching($criteria);
}
My problem is that i really don't know what is the safest way to inject authenticated user as parameters of the filteredCardsCriteria() method :/. I have no doubt that you'll be able to help me, thanks in advance.
Hey Romain S.
To get the logged in User in Symfony, you need to grab it from the Security service (Symfony\Component\Security\Core\Security), so you can inject that service into your class and use it, or, from a controller you can use the shortcut method `$this->getUser()`. Just be aware that that method may return null if there are no logged user
Cheers!
Thank you for answering Diego !
Indeed, I guess I have to use the security service. But the question I'm asking myself is how to insert it in my Criteria query? My Criteria query is supposed to filter "category->cards" that have an owner equal to the authenticated user.
As I call my criteria query in my entity (see code below), I can't use the security service to inject the authenticated user as a parameter to my "filteredCardsCriteria()" method. Just like I can't inject it in the categoryRepository because I'm not in an object context.
public function getFilteredCards(): Collection
{
$criteria = CardRepository::filteredCardsCriteria();
return $this->cards->matching($criteria);
}
So in this case, how do you do it?
Cheers !
Oh, in that case you can ask for the user in your getFilteredCards(User $user)
repository method, then it would be the job of the caller to fetch the user from the security service, e.g. A controller's action
Yeah but in api plateform we're not using any controller, are we ? Is there a way to filter an entity related collection by comparing an external parameters to it like, for exemple the authenticated user without using controller ?
Hey Romain S. !
Let me see if I can jump in and help out!
For full reference for anyone else reading this, usually this type of thing is done with a query extension - https://symfonycasts.com/screencast/api-platform-security/query-extension - however, that only applies to the "top-level" item. What I mean is, API Platform only makes a query (and thus, only calls your query extension) for the top-level resource that you're fetching. So, for you, the Category.
Filtering the cards relation property is a bit different, since API Platform just calls $category->getCards()
or ->getFilteredCards()
depending on how you have it set up. In other words, you're using the correct approach... and I totally see the problem.
And, I don't think there is any "magic" fix... and I'm debating a few approaches in my mind. I would try this approach: https://symfonycasts.com/screencast/api-platform-extending/data-provider
Basically, create a new, non-persisted, filteredCards
property on your entity, and make getFilteredCards() simply return that property. So what we're effectively doing is creating a "custom field" as described in the video I linked to. Then, via a custom data provider, you would do whatever filtering or query logic you need and then set the filteredCards property with your final, filtered list - I show that 2 chapters later - https://symfonycasts.com/screencast/api-platform-extending/is-me-field#codeblock-9bc8a3c601
Let me know if this makes sense! I agree that the approach shown in this chapter works... until you need a service to accomplish your task. At that moment, it stops just being a "filtered field" and really becomes more of a "custom field".
Cheers!
AAAAD done, this time I coded along, first listen to the full course, then code along... also 2x speed, I don't think I can ever listen to Ryan again without such velocity... this will invalidate the possibility of ever meeting him and be comfortable with velocity of speech xD
Things I dont like... I really hate the codding via annotations... I think it is messy and is not PHP... sorry guys... BUT recognize the value and maybe one day I understand them correctly and don't feel the same about them... I hate magic on y code...
Other than that I really like api platform, makes it really quick fast and reliable way of building APIs...
I went along and made my own changes to the thing... no sessions, sorry Ryan :P
I made a ApiTokenAuthenticator and am using that... just went to the security tutorial(Symfony4) for inspiration...
Also this made me like the Validators from Symfony, I didnt use them on a project because well... magic...(yeah Im not super fun sometimes xD) but now I am aiming on rebuilding most of that project with API platform and they will be used... thanks or the push guys...
Hey Fernando,
Congrats on finishing the course... twice! Sounds like you made a lot of extra work in your project, well done! And yeah, if you want to please Ryan - just tell him that you coded along with him! ;) Haha, I think Ryan can speak 2x faster in person, so you still can meet him one day and have a great conversation :D Maybe next year on SymfonyCon? ;)
About annotations. Well, annotations has a great advantage - you write code and config at the same spot. And thanks to Symfony Plugin you have autocomplete for it. I bet that's because it was set by default in Symfony, just easier to handle everything in one place, less files, etc. But I see your point, and using something different like XML or Yaml is totally valid for your configuration! Feel free to use whatever you're comfortable with. I think Yaml is a great option, clear and simple.. but I know a few guys who prefer to work with XML instead :)
And thanks for your feedback!
Cheers!
in the end, annotations are, like php or symfony, a tool and I should not get annoyed by them... I may take a look at yaml way of setting this thanks...
PS: I really cannot imagine myself doing anything with xml... if ever comes the day where my carer as a developer depends on doing something complex with xml, I'll become a gardener or barista or something... xD
Hey Fernando A.!
this will invalidate the possibility of ever meeting him and be comfortable with velocity of speech xD
No problem - I will talk at 2x speed if we ever meet ;)
Things I dont like... I really hate the codding via annotations... I think it is messy and is not PHP... sorry guys
I LOVE annotations, but I like them a different amount in different situations. For routing & Doctrine metadata, I love annotations. The API Platform annotations are my least favorite annotations out of anything. I would very much prefer a PHP configuration. The problem is that they're very complex. Doctrine metadata is simple - @ORM\Column(nullable=false)
. But the API Platform metadata is many times more complex and, I agree, i really don't love it. Using XML or YAML would probably be slightly better, but in the end, I think this type of config is complex enough that it should be done in PHP. I hope that's a feature they will add someday :). Here is an issue about it - https://github.com/api-platform/core/issues/3558
Cheers!
Hi !
First, thank you Ryan Weaver ! You're an awesome teacher. I followed a lot of SymfonyCasts these past few weeks, and each time, you achieve to make it look easy, logic, and fun, with great progression through the chapters, and with great and reusable final code. SymfonyCasts is by far the best site to learn Symfony and many other web-related subjects. Beside, each time you say "error" - and it happens...hmmm...a lot... - I always think of that and I don't know why :)
Now back to this course.
- sometimes, for those of us who don't use PhpStorm, it can be hard to know which Symfony Components to import/use, and it's not always written in the course script. It's no big deal, though, as it forces us to find the right documentation, but it's quite frustrating when I run phpunit, expect that it works like in the video, but fail because of some forgotten statement. By the way, I had never used PHPUnit before, and a few words for introduction could have been useful.
- about ApiPlatform : I'm quite surprised by the complexity of the management of the fields visibility (user vs. admin). The first part of this tutorial is quite simple, and suddenly you have to override several methods and create your own listeners and normalizers, with possible performance issues. For something that seems so basic and needed - almost every entity I ever created need that - , I was expecting some simple anotations like "@Visibility({"admin"|"all"|"user"|"owner"})". I must admit that I fear the day I'll have to do all that by myself for my next app...and that day will come very soon.
- Honestly, when I started this course, I was hoping to find a concrete use of APIs with Symfony. I started learning API for my next - huge - project, and now, great!, I know how to do it. But now, I need help to actually use it on regular views, on a regular project. I watched the (also great) React course with Ron_Furgandy, but this API_Platform tutorial could use a chapter where you learn how to create simple cheeses or users views (and not solely through the API docs!). For example, I have yet no idea how to use API_Platform from the controller, or even if It should be used - or if everything must be AJAX-called from the frontend pages via React or Vue ; and, if both can be done, which method to chose and why. Where should I go next to learn that ?
Anyway, thank you SymfonyCasts, and see you soon for technical questions about APIPlatform ;)
Hi Jean-tilapin!
> First, thank you Ryan Weaver ! You're an awesome teacher...
❤️❤️❤️ - I will pass this feedback to the entire team!
> Beside, each time you say "error" - and it happens...hmmm...a lot... - I always think of that and I don't know why :)
OMG, 🤣Actually, for whatever reason, this is a word that I'm *aware* of when I say it. Even I think, "this sound kinda funny" :P
> sometimes, for those of us who don't use PhpStorm, it can be hard to know which Symfony Components to import/use, and it's not always written in the course script. It's no big deal, though, as it forces us to find the right documentation
Yes, we get this feedback occasionally. It's had because if I show the use statement every time, most people would want to kill me :). But... the use statements *should* be included in the course script (the code blocks) ALWAYS. Well, you can always "expand" a code block to find it. But what I mean is, if something we do adds a "use" statement, then the code block *should* show that use statement. If it is NOT, then that's really "our mistake". If you have an example if this, I'd greatly appreciate it.
> about ApiPlatform : I'm quite surprised by the complexity of the management of the fields visibility (user vs. admin). The first part of this tutorial is quite simple, and suddenly you have to override several methods and create your own listeners and normalizers, with possible performance issues
There is a lot going on here :). Groups are SO powerful with Symfony's serializer, but can also be a lot to keep track of. I also like what your code looks like better than all the @Groups stuff! Part of the complexity is that the groups also determine whether or not a related resource is serialized as an IRI or an embedded object (e.g. the "category" property of a Product is "/api/categories/5" or it's an embedded object). That little fact prevents us from using simple "read" and "write" groups on every entity. I don't think there's much we can do in the tutorial about this, but I agree and would love if there were an easier "layer" put on top of groups to make this more manageable. I *will* say, however, that we added a lot of groups so that we could make our system *massively* flexible. You may or may not really need that. I would start with simple groups like "user:read" and "user:write" and then only add more stuff if you need it.
Now, about the normalizers, I hate these things! What I "want" to do is create a normalizer class and easily "tweak" the way that some object is normalized. Instead, we need worry about service decoration and setting a special flag into context to avoid calling ourselves recursively. Yikes! I hope that API Platform (or the serializer) will add a cleaner hook to all of this.
> For example, I have yet no idea how to use API_Platform from the controller, or even if It should be used - or if everything must be AJAX-called from the frontend pages via React or Vue ; and, if both can be done, which method to chose and why. Where should I go next to learn that
Good question and good timing. Watch the Vue tutorial - https://symfonycasts.com/sc... - it uses API Platform. We make AJAX calls around chapter 24 but then *replace* an AJAX call with direct data from the server on chapter 30 (which, sorry, won't be released for a few more days - but you can go to chapter 29 and click "next chapter" to cheat and see the script). I am a huge fan of using things like React & Vue only on the parts of your site where you need it. I'm also a fan of sending data directly from your server into JavaScript to avoid AJAX calls in some cases. In chapter 30, we serialize something to JSONLD in a controller (well, actually Twig) so that we can use that data instead of an AJAX call.
> Anyway, thank you SymfonyCasts, and see you soon for technical questions about APIPlatform ;)
Ha! See you there ;).
Cheers!
Hi there,
I am already searching for a whole while but cant get my head around the following problem. Any advice could be helpfull! :)
let me explain:
I have an entity 'Post', This entity has other entities link to it ('Documents', 'Images', ...). This 'posts' is linked to the entity 'User'.
I have two use cases where i ask for Posts, but both need a different filter.
case 1: On my main page, non authenticated visitors can see all posts with the linked entities' data which have 'isActive' marked as true. The admin makes this mark.
case 2: registered user can ask to see ALL their OWN posts with the linked entities' data (so as well those with isActive marked as false).
I tried already in many ways but I can't figure out how to have the ability to have these filters live next to each other in a single api endpoint :)
All the best!
Wannes
Hey Wannes V.!
Ah, yea, I totally understand the issue. The key thing with both situations is that you want the filter to be mandatory. What I mean is (and I think you understand this already), you don't want (in case 1) for the API request to be /api/posts?isActive=true
... because that would mean that a smart user could just take off the ?isActive
part to see all posts. In both case 1 and case 2, you want the URL to just be /api/posts
but for some invisible & dynamic filtering to filter this down to the appropriate set.
The solution for this type of automatic & invisible filtering is a Query Extension - it's what we talk about here - https://symfonycasts.com/screencast/api-platform-security/query-extension - that allows the URL to be /api/posts
, but really, we are filtering in whatever way we want. What really makes this work is your logic in the class. It will be something like:
A) If user is anonymous, add (pseudo-code) andWhere('isActive=true')
B) If user is authenticated, add (pseudo-code) andWhere('owner=:user')
and maybe also you add "OR isActive = true" so that I can see ALL of my posts (active or not) AND also active posts from anyone else.
Does that cover it? Or did I miss a detail? By the way, let's suppose that you did this, but on the homepage (to avoid confusion), you want to AVOID listing any non-active posts for the currently authenticated user (to avoid the user saying "Hey! Why is my non-active Post showing up on the homepage!?). To do this, you would do exactly what I have above. But also, on the homepage, you would change the API URL to be /api/posts?isActive=1
. The end result is that ONLY active posts are shown on the homepage. But if a "bad user" removes the ?isActive
part, they will still only see their OWN non-active posts thanks to the QueryExtension.
Here's another way to think about this:
A) User a Query Extension to limit a collection to ALL possible items that a user should be allowed to view from a security perspective
B) Use "filters" as a way to further filter that collection for UI / business reasons. Another example might be a "See my non-active posts" page - you would send a request /api/posts?isActive=false
to see MY non-active posts (or you could also use /api/posts?isActive=false&user=/api/users/5
where 5 is the currently-authenticated user id... but really, the &user= part is more for clarity... because if someone removed this filter, the query extension would still prohibit seeing other users' non-active posts).
Let me know if this helps!
Cheers!
Goodmorning Ryan!
Once more: thank you! :)
It did the job in a few minutes while I was before already struggling with it for a whole while!
All the best !
Wannes
Hey there, great tutorial! I'm kind of forced (by requirements) to create an API and deploy my fronted application somewhere else. So basically i am writing my own third-party React-app that will be communicating with my API. So I'm in need of JWT which will be handled by the lexikbundle but the whole process of JWT authentication and refresh tokens is still a bit of a mystery to me. Especially when they have to work in symbiosis with api platform. Do you guys have any good resources where I can find more answers on this topic?
Hey juantreses
If you still can decide whether or not to use JWT you may want to watch this chapter first so you can really tell if you need them or not. https://symfonycasts.com/sc...
In case you want to implement JWT for your app's authentication, you can watch this tutorial where Ryan demonstrates how to work with JWT and Symfony. The only gotcha is that the tutorial is build on Symfony3 but the main concepts, especially those related to create a JWT and store it on your frontend are still relevant, and, you can leave us questions in the comments.
https://symfonycasts.com/sc...
Cheers!
Very nice tutorials :)
When do you expect to have part 3 online?
Hey Daniel S.
Thanks for reaching us. We are very glad that you appreciate our courses. Unfortunately I can't give you any ETA on Api platform part 3 so stay in touch and looking for updates. While waiting it I'd recommend to check out our newest Symfony 5 tutorials.
Cheers!
Hi! First of all thanks for this great course!
I wonder how to handle a case like this:
Let's say a have one entity, for example User entity with properties:
- username
- firstName
- lastName
- password
- addressStreet
- addressZipCode
- addressCity
I want to split this User entity to 3 independent API resources. I want to achieve situation like this:
User:
DELETE /api/users/{id} [delete User]
GET /api/users/main-data [get collection of User main data (only username, firstName, lastName properties)]
GET /api/users/{id}/main-data [get User main data (only username, firstName, lastName properties)]
PUT /api/users/{id}/main-data [update User main data (only username, firstName, lastName properties)]
GET /api/users/address [get collection of User addres (only addressZip, addresZipCode, addressCity properties)]
GET /api/users/{id}/address [get User addres (only addressZip, addresZipCode, addressCity properties)]
PUT /api/users/{id}/address [udpdate User address (only addressZip, addresZipCode, addressCity properties)]
PUT /api/users/{id}/password [update User password (only password property)]
So, I have two questions:
1) Can I do that in easy way (with all of automation which API Platform provides)?
2) Should I do that like this? (I mean, is that good approach).
I think that is common case.
To achieve that, Should I create 3 independent model class, for example UserMainData, UserAddress and UserPassword with properties I want and expose this models in API Platform and finnaly create own DataProviders and DataPersisters for this models class?
Maybe exists easier way to achieve my case?
Thanks a lot!
Hey @Marcin!
Interesting situation :).
1) Can I do that in easy way (with all of automation which API Platform provides)?
Yes-ish... ;) See below
2) Should I do that like this? (I mean, is that good approach).
Hmmmmm, I'm less sure. Generally-speaking, if you have a use-case to break your User resource down into small "resources" - I have no problem with that. And the whole idea certainly isn't wrong - it's just parts of it that look weird to me. So let's look at a few pieces:
- A) /api/users/main-data
It feels weird to have basically a "user main data" resource - which is what this tells me (this would return the "collection of user main data" resources). I'd prefer /api/users... where you simply return the "main data" from that :). Or you return ALL data from that - and then if a client purposely wants less data, allow them to use sparse fieldsets https://symfonycasts.com/screencast/api-platform/property-filter
- B) /api/users/address
I don't really like this for the same reason - but it bothers me less. You may in fact have a use-case where an API client needs to get a list of all the addresses in the system - and maybe filter them. If so, this probably makes sense. I would do this with a custom model class and data persister / data provider as you mentioned.
- C) /api/users/{id}/address
This does make some sense to me - at least from a RESTful standpoint. What I don't like is that, when I get a User resource, it doesn't normally have an "address" property. This makes it looks like it should... and that you're fetching that one property. I'm also not sure about is if this is reasonably possible with API Platform. The same is true for the PUT version of this - I don't love... and if you implemented it, it would almost certainly be via a custom operation.
- D) PUT /api/users/{id}/password
This one doesn't bother me much from a RESTful standpoint... but I think you'd need a custom operation to accomplish it... and though I try to avoid this, this is a pretty decent example of when it might make sense. But again, you could also just send a PUT request to /api/users/{id} and send {"password": "foo"}
... so it depends on how much trouble you want to get into.
I hope this helps! It's not always easy to "bend" API Platform like this... and it's not often a good idea to even try - because it means you might be doing things that aren't very RESTful... or don't really offer any advantages over the "normal" way anyways.
Cheers!
Let's say that the cheeselisting title/name has to be unique. And the user tries to POST a cheeselisting with the same name twice, I have it in my doctrine set to unique, and api platform returns a 500 server error response. Is this the correct response code to be sending? Is there a better way to handle these post requests?
Maybe 400, and a message "Already Exists"? Is that possible to do with api platform?
Edit:
Seems there already is something of the sort in api platform for when a specific attribute of an entity is to be unique. But in my case I have a unique constraint on my entity as follows:
```
* @ORM\Table(
* uniqueConstraints={
* @ORM\UniqueConstraint(name="vote_unique", columns={"user_id", "post_id"})
* }
* )
```
This only creates a 500 error not 400 like when I put user to unique only for example.
Hey gabb
It returns a 500 because the constraint you added it's at the database level. What you have to do is to add another "unique" constraint to your entity, so it fails at validations level. Check at this piece of documentation https://symfony.com/doc/cur...
Cheers!
Should there be a separate register endpoint or should I use POST /api/users/ but then is it possible to automatically log people in after using this endpoint?
Hey gabb!
Hmm, interesting question. From a practical perspective - where your API is being used only by your own JavaScript frontend - it is indeed very practical to automatically authenticate the user after registration. You should be able to do that in API Platform by using an event listener - there's a good example of sending an email whenever a book is created in this section - https://api-platform.com/docs/core/events/#custom-event-listeners - which you would replace with logic to authenticate the user.
Sio, if you want to do this, I don't see any problem with using POST /api/users
instead of duplicating that functionality over to some other register endpoint. IF you have some use-case where sometimes you DO need something to create new users without authenticating, then you could choose to activate the "login" feature with a flag - e.g. ?auto_authenticate=1 to "activate" that feature. That's maybe not perfectly RESTful, but I think it's a fine thing to do.
Let me know how it goes!
Cheers!
Hello! First of all great course!
I wonder if there is any recommendation on having different User entities. I have two roles Clinician and Patient each of them have different properties and interactions with the system.
Should I create something like `ClinitianProfile` and `PatientProfile` and relate it to the User entity nullable in a "one or another" way.
Should I extend User into `ClinitianUser` and `PatientUser`. If so, how it's done?? via two user providers??? I can create different firewalls I guess.
Thanks in advance!
Hey Qcho!
Ah yes, great question :). The answer is... of course... "it depends" :D. But let me try to give you a better answer. The correct answer is probably what you suggested: ClinitianProfile and PatientProfile. You can have two totally different "user" classes, but this has two limitations (which, if you have the right requirements might be "good limitations", but usually you don't want these limitations): (1) there cannot even be one page on the site that both a "clinician" and "patient" can access (this is not truly a limitation, but in practical terms it is) and (2) a single user account can NEVER be both a clinician and a patient.
To say it differently: creating 2 totally different User classes makes sense if you... almost have 2 totally independent "sites": a clinician site where only clinicians log in and use and a "patient" site where only patients log in. If you DO have this, having 2 different user classes, and 2 different "login forms" can keep things clean. But the parts of your site really need to be separate. If you, for example, even try to "share" part of a template between the two parts of the site, you might accidentally write code like app.user.primaryCarePhysician
which is a method that lives only on the Patient
user class . but not Clinician
. In that situation, the code would work on the patient pages but break on the clinician pages. It can get tricky ;).
That's why usually I recommend having a single User class and then extending them with the "profile" idea that your proposed. In this model, you probably only have 1 login page (sure, you could have multiple login pages if you want, but really, they all work the same - a clinician could technically log in to the login page that you "intended" for physicians). Once a user logs in, you would probably check to see if they have a ClinitianProfile record to know if that are a Clinitian or check to see if they have a PatientProfile to know if they are a patient. And in theory, you could be both (you could write code to prevent this if you want, but from a database structure, it would be possible to be both a clinitian and a patient).
Btw, your idea of having a ClinitianUser and PatientUser that both extend a shared User class is also not a bad idea... but it's basically what I explained first: it is "effectively" 2 different "User" classes, even though they extend the same base User class. You would STILL need to be very careful with any shared code: it would be ok to use any methods inside shared code that are on the shared User class, but not on either specific user class. One advantage to this model versus the "Profile" idea is that a user would naturally ONLY be a clinician OR a patient, but never both (because if you use Doctrine inheritance to accomplish this, then each record will have a "discriminator" column that says which "type" they are - and this would be automatically used when they log in to return the correct ClinitianUser or PatientUser object.
Phew! So, that reply got long because a lot depends on your app. If you use the "profile" way of doing things, the authentication part will not need anything special: you are always logging in as a "User"... and then your app code does different things depending on whether they have a joined ClinitianProfile or PatientProfile. If you do the ClinitianUser idea with Doctrine inheritance, you also shouldn't need to do anything special: I think you can use the normal "entity" user and point it at the parent "User" class. I believe Doctrine is then smart enough to actually return a ClinitianUser or PatientUser automatically based on that "discriminator column".
Let me know if this helps... or confused further ;)
Cheers!
Great tutorial!!
please, consider add in future tutorial JWT access and filter results in forms.
Thank's a lot
Thanks Alberto rafael P.!
I'm pretty sure I know what you mean about JWT access, but can you tell me more about "filter results in forms"? I want to make sure we don't miss a good topic!
Cheers!
Wow, thank's for answers me Ryan.
I mean apply filter with forms in a list of resources.
How build these queries?
Thanks again
Hey Alberto rafael P.!
> Wow, thank's for answers me Ryan
Of course!
> I mean apply filter with forms in a list of resources
Do you mean: how would we build a search form with field for "filtering" that is then used to make a request to an API Platform endpoint with filters applies (e.g. /api/cheeses?is_published=1)? I think you mean something different... but I don't quite understand yet. Please tell me more!
Cheers!
if you want to make a filter form with several options for a list of resources.
I have a list of cheeses and in the filters I want to be able to do it by:
e.g. those over $ 5, created today, and that the owner is Ryan.
I would have to do a much more complex query. How to drive is logical?
Hey Alberto rafael P.!
I think I understand :). The key to making this information available via the API is all about adding the correct API filters: https://symfonycasts.com/screencast/api-platform/filters - basically, you want to first make it possible to use API Platform to make requests and filter with this information. For example, in your case, I would set up several filters that would allow me to do something like GET /api/cheeses?price[gt]=5&createdAt[after]=2019-10-03&owner=/api/users/5
.
Once your API Platform resource is setup with these filters, the "form" itself should just be a way to "build" this URL for the user and then make the AJAX request. For example, they might enter "5" into a "price" field, and when they hit submit, you add the ?price[gt]=5
query parameter to the URL.
Does that make sense? Or am I misunderstanding? :)
Cheers!
"Houston: no signs of life"
Start the conversation!
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
}
}
Hi there,
Great course!
I was wondering, all this course is about interacting with entities, but is there a way to use API Platform on classes that do stuff? I mean, let's say I have a Helper with a bunch of methods for calculations is there anyway to add API Platform endpoints for that as well?