Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Filtering Related Collections

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

There 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.

... lines 1 - 9
class CheeseListingResourceTest extends CustomApiTestCase
{
... lines 12 - 107
public function testGetCheeseListingItem()
{
... line 110
$user = $this->createUserAndLogIn($client, 'cheeseplese@example.com', 'foo');
... lines 112 - 125
$client->request('GET', '/api/users/'.$user->getId());
... lines 127 - 128
}
... 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']).

... lines 1 - 9
class CheeseListingResourceTest extends CustomApiTestCase
{
... lines 12 - 107
public function testGetCheeseListingItem()
{
... lines 110 - 126
$data = $client->getResponse()->toArray();
$this->assertEmpty($data['cheeseListings']);
}
... 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().

... lines 1 - 37
class User implements UserInterface
{
... lines 40 - 195
public function getPublishedCheeseListings(): Collection
{
return $this->cheeseListings->filter(function(CheeseListing $cheeseListing) {
return $cheeseListing->getIsPublished();
});
}
... 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").

... lines 1 - 37
class User implements UserInterface
{
... lines 40 - 183
/**
* @return Collection|CheeseListing[]
*/
public function getCheeseListings(): Collection
{
return $this->cheeseListings;
}
/**
* @Groups({"user:read"})
* @SerializedName("cheeseListings")
*/
public function getPublishedCheeseListings(): Collection
... 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:

... lines 1 - 9
class CheeseListingResourceTest extends CustomApiTestCase
{
... lines 12 - 46
public function testUpdateCheeseListing()
{
... lines 49 - 56
$cheeseListing->setIsPublished(true);
... lines 58 - 74
}
... 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!

Leave a comment!

43
Login or Register to join the conversation

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?

1 Reply

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!

2 Reply
Fernando A. Avatar

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 :)

Reply

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!

Reply

Thank you for that reply! I will look into it for sure. Keep up the good work!

Best regards

Reply
Carlos Avatar

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

Reply
Carlos Avatar

Ok, I first wrote this and then went to read the other comments... it's already answered to Julien Bonnier!! Perfect

Reply

Hey Carlos,

Happy to see you found an answer in the comments!

Cheers!

Reply
csc Avatar

Esperando a que salga Symfony 5 + ApiPlatform + Auth2.0 + Voters

Reply
MolloKhan Avatar MolloKhan | SFCASTS | csc | posted 2 years ago | edited

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!

Reply
Romain S. Avatar
Romain S. Avatar Romain S. | posted 2 years ago

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.

Reply

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!

Reply
Romain S. Avatar

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 !

Reply

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

Reply
Romain S. Avatar

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 ?

Reply

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!

Reply
Fernando A. Avatar
Fernando A. Avatar Fernando A. | posted 3 years ago

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...

Reply

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!

1 Reply
Fernando A. Avatar
Fernando A. Avatar Fernando A. | Victor | posted 3 years ago

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

Reply

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!

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | posted 3 years ago

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 ;)

Reply

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!

Reply
Wannes V. Avatar
Wannes V. Avatar Wannes V. | posted 3 years ago

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

Reply

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!

Reply
Wannes V. Avatar

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

Reply

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?

Reply

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!

Reply
Daniel S. Avatar
Daniel S. Avatar Daniel S. | posted 3 years ago

Very nice tutorials :)
When do you expect to have part 3 online?

Reply

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!

Reply
Default user avatar

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!

Reply

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!

Reply

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.

Reply

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!

Reply

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?

Reply

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!

Reply
Qcho Avatar

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!

Reply

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!

1 Reply
Alberto rafael P. Avatar
Alberto rafael P. Avatar Alberto rafael P. | posted 3 years ago

Great tutorial!!
please, consider add in future tutorial JWT access and filter results in forms.

Thank's a lot

Reply

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!

Reply
Alberto rafael P. Avatar
Alberto rafael P. Avatar Alberto rafael P. | weaverryan | posted 3 years ago

Wow, thank's for answers me Ryan.
I mean apply filter with forms in a list of resources.
How build these queries?
Thanks again

Reply

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!

Reply
Alberto rafael P. Avatar
Alberto rafael P. Avatar Alberto rafael P. | weaverryan | posted 3 years ago

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?

Reply

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!

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
    }
}
userVoice