Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

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",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.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.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9