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

Earlier, we added a bunch of nice filters to DragonTreasure. Let's add a few more - starting with User - so we can show off some filtering superpowers for relations.

Using PropertyFilter Across Relations

Start like normal: ApiFilter and let's first use PropertyFilter::class. Remember: this is kind of a fake filter that allows our API client to select which fields they want. And this is all pretty familiar so far.

... lines 1 - 4
use ApiPlatform\Metadata\ApiFilter;
... line 6
use ApiPlatform\Serializer\Filter\PropertyFilter;
... lines 8 - 22
#[ApiFilter(PropertyFilter::class)]
... lines 24 - 25
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 28 - 174
}

When we head over, refresh, and go to the GET collection endpoint... we see a new properties[] field. We could choose to return just username... or username and dragonTreasures.

When we hit "Execute"... perfect! We see the two fields... where dragonTreasures is an array of objects, each containing the fields we chose to embedded.

Again, this is super duper normal. So let's try something more interesting. In fact, what we're going to try isn't supported directly in the interactive docs.

So, copy this URL... paste and add .jsonld to the end.

Here's the goal: I want to return the username field and then only the name field of each dragon treasure. The syntax is a bit ugly: it's [dragonTreasures], followed by []=name.

And just like that... it only shows name! So right out of the box, PropertyFilter allows us to reach across relationships.

Searching Relation Fields

Let's do something else. Head back to DragonTreasure. It might be handy to filter by the $owner: we could quickly get a list of all treasures for a specific user.

No sweat! Just add ApiFilter above the $owner property, passing in the trusty SearchFilter::class followed by strategy: 'exact'.

... lines 1 - 55
class DragonTreasure
{
... lines 58 - 101
#[ApiFilter(SearchFilter::class, strategy: 'exact')]
private ?User $owner = null;
... lines 104 - 215
}

Back over on the docs, if we open up the GET collection endpoint for treasures and give it a whirl... let's see... here we go - "owner". Enter something like /api/users/4... assuming that's actually a real user in our database, and... yes! Here are the five treasures owned by that user!

But I want to get crazier: I want to find all treasures that are owned by a user matching a specific username. So instead of filtering on owner, we need to filter on owner.username.

How? Well, when we want to filter simply by owner, we can put the ApiFilter right above that property. But since we want to filter on owner.username, we can't put that above a property... because owner.username isn't a property. This is one of the cases where we need to put the filter above the class. And... that also means we need to add a properties option set to an array. Inside, say 'owner.username' and set that to the partial strategy.

... lines 1 - 55
#[ApiFilter(SearchFilter::class, properties: [
'owner.username' => 'partial',
])]
class DragonTreasure
{
... lines 61 - 218
}

Ok! Head back over and refresh. We know we have an owner whose username is "Smaug"... so let's go back to the GET collection endpoint and... here in owner.username, search for "maug"... and hit "Execute".

Let's see... That worked! This shows all treasures owned by any user whose username contains maug. Pretty cool!

Ok squad: get ready for the grand finale - Subresources. These have seriously changed in API Platform 3. Let's dive into them next.

Leave a comment!

7
Login or Register to join the conversation
David-S Avatar
David-S Avatar David-S | posted 13 days ago | edited

Hi everyone,

so I have an interesting error. Instead of integer ids I use uuids (as primary key).
When searching "DreagonTreasures" with a specific user e.g. /api/users/0189d986-1e5c-7205-8e1c-466b139ceda1
I get a 200 response with

"hydra:totalItems": 0,
"hydra:member": [],

Here is what id looks like in my class:

 #[ORM\Id]
 #[ORM\Column(type: UuidType::NAME, unique: true)]
 #[ORM\GeneratedValue(strategy: 'CUSTOM')]
 #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
 private ?UUid $id;

I'm using MySQL. The id (with uuid value) apparently is of type binary(16) in MySQL. Does it have something to do with that?
What am I missing / how can I fix this?

(When I use integer ids it works as it should.)

Thanks for your help.

Reply

Hey @David-S

I think that's unexpected. Have you watched this chapter? You may find it helpful https://symfonycasts.com/screencast/api-platform-extending/uuid-identifier
By the way, I don't recommend using UUIDs as the primary key because it will slow down your queries. It's better to keep a traditional integer id as the primary key and add a UUID as your "public id"

Cheers!

Reply
David-S Avatar

Hey @MolloKhan,

thanks for your reply. I wasn't sure that "slowing down" is a real world problem (at least for my application).
So, to keep it simple, I wanted to use the uuid instead of integer id. But Ok, I will use int as primary and add uuids as identifiers.

I used Symfony\Component\Uid\Uuid . In that Symfony 5 API Tutorial Ramsey\Uuid\Uuid is used. Which one shpuld be uses now with Symfony 6 and API Platform 3?

Reply

Good question. Since Symfony 5.2 there's a UID component that basically replaces Ramsey's library. You can read more about it here https://symfony.com/doc/current/components/uid.html#storing-uuids-in-databases

Cheers!

Reply
David-G Avatar
David-G Avatar David-G | posted 3 months ago | edited

Hi everyone,

I am working on an application with API Platform 3. I have an Entity called "Users" and another Entity called "Prospects". I have created a route: /api/users/{partner_id}/prospects/{id}, where {partner_id} represents the custom id of users and {id} represents the id of prospects.

This setup is functioning correctly, but I want to apply a filter on the subresource "prospects," and I am having trouble understanding how to do this. I have encountered some issues, but I haven't found a solution yet.

Here is a portion of my code:

#[ApiResource(
    operations: [ 
        new Get(
            shortName: "Users",
            uriTemplate: '/users/{partner_id}/prospects/{id}',
            uriVariables: [
                'partner_id' => new Link(fromClass: Users::class, fromProperty: 'prospects'),
                'id' => new Link(fromClass: Prospects::class)       
            ],             
        ) 
    ],    
)]
#[ApiFilter(SearchFilter::class, properties:[
    'status',
    'lastname' => 'ipartial',
    'upline'
])]

Please let me know if you have any suggestions or solutions. Thank you!

Reply

Hey @David-G!

This is not a use-case I had thought of before! You said this isn't working. What does it do? An error? Just not work at all?

Cheers!

Reply
David-G Avatar

Hi,
The bug was between the chair and the screen because I didn't realize that I was trying to apply a filter on a Get operation when filters only work on GetCollection operations.
Thanks !

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice