Filtering on Relations

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

Go directly to /api/users/5.jsonld. This user owns one CheeseListing... and we've decided to embed the title and price fields instead of just showing the IRI. Great!

Earlier, we talked about a really cool filter called PropertyFilter, which allows us to, for example, add ?properties[]=username to the URL if we only want to get back that one field. We added that to CheeseListing, but not User. Let's fix that!

Above User, add @ApiFilter(PropertyFilter::class). And remember, we need to manually add the use statement for filter classes: use PropertyFilter.

... lines 1 - 6
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
... lines 8 - 15
/**
... lines 17 - 20
* @ApiFilter(PropertyFilter::class)
... lines 22 - 24
*/
class User implements UserInterface
... lines 27 - 190

And... we're done! When we refresh, it works! Other than the standard JSON-LD properties, we only see username.

Selecting Embedded Relation Properties

But wait there's more! Remove the ?properties[]= part for a second so we can see the full response. What if we wanted to fetch only the username property and the title property of the embedded cheeseListings? Is that possible? Totally! You just need to know the syntax. Put back the ?properties[]=username. Now add &properties[, but inside of the square brackets, put cheeseListings. Then []= and the property name: title. Hit it! Nice! Well, the title is empty on this CheeseListing, but you get the idea. The point is this: PropertyFilter kicks butt and can be used to filter embedded data without any extra work.

Speaking of filters, we gave CheeseListing a bunch of them, including the ability to search by title or description and filter by price. Let's add another one.

Scroll to the top of CheeseListing to find SearchFilter. Let's break this onto multiple lines.

... lines 1 - 16
/**
... lines 18 - 34
* @ApiFilter(SearchFilter::class, properties={
* "title": "partial",
* "description": "partial"
* })
... lines 39 - 41
*/
class CheeseListing
... lines 44 - 202

Searching by title and description is great. But what if I want to search by owner: find all the CheeseListings owned by a specific User? Well, we can already do this a different way: fetch that user's data and look at its cheeseListings property. But having it as a filter might be super useful. Heck, then we could search for all cheese listings owned by a specific user and that match some title! And... if users start to have many cheeseListings, we might decide not to expose that property on User at all: the list might be too long. The advantage of a filter is that we can get all the cheese listings for a user in a paginated collection.

To do this... add owner set to exact.

... lines 1 - 16
/**
... lines 18 - 34
* @ApiFilter(SearchFilter::class, properties={
... lines 36 - 37
* "owner": "exact"
* })
... lines 40 - 42
*/
class CheeseListing
... lines 45 - 203

Go refresh the docs and try the GET endpoint. Hey! We've got a new filter box! We can even find by multiple owners. Inside the box, add the IRI - /api/users/4. You can also filter by id, but the IRI is recommended.

Execute and... yes! We get the one CheeseListing for that User. And the syntax on the URL is beautifully simple: ?owner= and the IRI... which only looks ugly because it's URL-encoded.

Searching Cheese Listings by Owner Username

But we can get even crazier! Add one more filter: owner.username set to partial.

... lines 1 - 16
/**
... lines 18 - 34
* @ApiFilter(SearchFilter::class, properties={
... lines 36 - 38
* "owner.username": "partial"
* })
... lines 41 - 43
*/
class CheeseListing
... lines 46 - 204

This is pretty sweet. Refresh the docs again and open up the collection operation. Here's our new filter box, for owner.username. Check this out: Search for "head" because we have a bunch of cheesehead usernames. Execute! This finds two cheese listings owned by users 4 and 5.

Let's fetch all the users... just to be sure and... yep! Users 4 and 5 match that username search. Let's try searching for this cheesehead3 exactly. Put that in the box and... Execute! Got it! The exact search works too. And, even though we're filtering across a relationship, the URL is pretty clean: owner.username=cheesehead3.

Ok just one more short topic for this part of our tutorial: subresources.

Leave a comment!

  • 2020-06-09 Stefan

    Yes, it makes sense to me, thanks :)

    Unfortunately I have to use this query on a list, so /api/colors?car=/api/cars/5&color=blue would only work for 1 item and there would also be cars with no color but they have to be visible too.

    I am trying to solve it with a custom field, thank you very much :)

  • 2020-06-08 weaverryan

    Hey Stefan!

    Yea, I believe also that this would filter the main resource, not the colors property. So, the overall issue is how API Platform loads the colors property. The logic looks like this:

    A) API Platform makes an initial query for the main resource. In this example, let's pretend the main resource is for "cars". So, when you GET /api/cars, it queries for all the car resources. If you have any filters applied (e.g. ?brand=audi) then those are used to *modify* the query.

    B) To return the "colors" property for each car, API Platform simple calls $car->getColors() on each Car object. And so, you can see why you would *always* get *all* the colors returned.

    So if you want to filter that sub-collection, you probably need to do it (more or less) as a custom field. You *could* still expose your "colors" field as a normal field. However, you would then probably need a custom normalizer for the Car resource so that you could *modify* this field dynamically. The process would be similar to when we add a completely custom field in the next tutorial - https://symfonycasts.com/sc... - the difference would be that you aren't really *adding* a new field. Instead, you would read the Request query parameter and, if it exists, you would change the "colors" property to a different value.

    Let me know if that makes sense! Often, the more natural way to do something like this is to make a request to /api/colors?car=/api/cars/5&color=blue. In other words, make a direct query to the resource you want to filter. However, I realize that under certain situations, this isn't ideal - so what you're trying to do isn't wrong - just showing how it might work more easily in some situations.

    Cheers!

  • 2020-06-05 Stefan

    Isn't this only for filtering the main Resource, so for example it will return all brands where a specific color is included, but it will filter the brands, not the subresource colors?

  • 2020-06-05 Diego Aguiar

    Hey Stefan

    Have you tried configuring the filter like this?


    @ApiFilter(SearchFilter::class, properties={"brand.colors": "exact"})

    Cheers!

  • 2020-06-05 Stefan

    hi, is there a way to filter the subresources while fetching the main resource

    For example:
    All Items:


    {
    [
    brand: 'audi',
    colors: [
    {name: blue},
    {name: green}
    ]
    }

    Is there a way to filter the results of a subresource, so the result would be following?


    {
    [
    brand: 'audi',
    colors: [
    {name: blue}
    ]
    }
  • 2020-03-05 weaverryan

    Hi Daniel Klimowicz!

    Interesting. I don't know the answer to this, but I know where to look. API Platform (I'm guessing you know this part, but just in case - it's not something we talked about in the tutorial) automatically fetches relationships eagerly. The class that does that is this one: https://github.com/api-plat...

    I would add some debug code to this class and figure out *where* & why the eager loading is not happening. I can't see anything with your code or that class that would make me expect this behavior.

    Let me know what you find out - I'd really be interested!

    Cheers!

  • 2020-03-04 Daniel Klimowicz

    Hi, i have this config in my Entities


    * @ApiResource(
    * collectionOperations={
    * "get",
    * },
    * itemOperations={
    * "get",
    * "delete"={
    * "controller"=NotFoundAction::class,
    * "read"=false,
    * "output"=false,
    * },
    * "enhancement"={
    * "method"="GET",
    * "normalization_context"={"groups"={"enhancement:read"}},
    * },
    * }
    * )

    and i have 'enhancement:read' in almost all fields and in relation also, and i thought that API platform will query DB with joins, but i see in my symfony debugger that to get 5 entities and corresponding related entities api platform is making 9 queries, maybe i'm doing something wrong

  • 2019-07-01 Diego Aguiar

    NP Jerome! and thanks for sharing your solution :)

  • 2019-06-28 Jérôme 

    Thanks Diego! Sorry, I removed by comment because I didn't see your answer! :/ I managed to find a solution before seeing your comment and it seems that you were right. I posted the whole solution here: https://stackoverflow.com/q... Maybe it will help others. ;)

  • 2019-06-28 Diego Aguiar

    Hey Jérôme 

    I didn't try this but looks like you have to declare your filter service first like so:


    services:
    #...
    someEntity.search_filter:
    parent: 'api_platform.doctrine.orm.search_filter'
    arguments: [ { someProperty: 'strategy' }, {...} ]
    ...

    Then you have to bind that service to your ApiResource (entity), and then it should work. You can find more info about configuring filters here: https://api-platform.com/do...

    I hope this helps. Cheers!