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 SubscribeOn CheeseListing
, thanks to SearchFilter
and also BooleanFilter
and RangeFilter
, when we use the collection endpoint, there are a number of different ways that we can filter, like searching the title
, description
and owner
fields:
... lines 1 - 18 | |
/** | |
... lines 20 - 45 | |
* @ApiFilter(BooleanFilter::class, properties={"isPublished"}) | |
* @ApiFilter(SearchFilter::class, properties={ | |
* "title": "partial", | |
* "description": "partial", | |
* "owner": "exact", | |
* "owner.username": "partial" | |
* }) | |
* @ApiFilter(RangeFilter::class, properties={"price"}) | |
... lines 54 - 57 | |
*/ | |
class CheeseListing | |
{ | |
... lines 61 - 217 | |
} |
That's pretty cool!
Except, we can only search by one field at a time. Like, we can search for something in the title
or something in the description
but we can't, for example, say ?search=
and have that automatically search across multiple fields.
But hey! That's no huge issue: when the built-in filters aren't enough, just make your own. Creating custom filters is both fun and... weird! Let's go!
Over in src/
, how about in ApiPlatform
, create a new PHP class called CheeseSearchFilter
. As usual, this will need to implement an interface or extend some base class. In this case, we need to extend AbstractFilter
. Make sure you get the one from Doctrine ORM
... it's actually impossible to see which one we have here... so I'll randomly guess:
... lines 1 - 2 | |
namespace App\ApiPlatform; | |
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter; | |
... lines 6 - 8 | |
class CheeseSearchFilter extends AbstractFilter | |
{ | |
... lines 11 - 17 | |
} |
Hey! I got it! If you chose the wrong one, you'll have to delete your project and start over. Or... just delete this one line and manually say use AbstractFilter
to get the right one.
It turns out, creating a filter is different based on the underlying data source. If your underlying data source is Doctrine ORM, creating a filter will look one way. But if your underlying data source is, for example, ElasticSearch
- which is something API Platform has built-in support for - then a custom filter will look different. And if you have a completely custom API resource like DailyStats
, creating a custom filter looks even another way.
We'll talk about how to create a custom filter for DailyStats
in a few minutes.
When we extend AbstractFilter
, as you can see, we need to implement a couple of methods. Go to "Code"->"Generate" - or Command
+ N
on a Mac - and select "Implement Methods" to generate the two methods we need:
... lines 1 - 5 | |
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; | |
use Doctrine\ORM\QueryBuilder; | |
class CheeseSearchFilter extends AbstractFilter | |
{ | |
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) | |
{ | |
} | |
public function getDescription(string $resourceClass): array | |
{ | |
} | |
} |
Filters are ultimately two parts: they're the logic that does the filtering, which is filterProperty()
, and then the logic that describes how the filter works, which is used in the documentation. That's the job of getDescription()
.
Let's start there. To help fill in this method, let's cheat and look at some core classes. Hit Shift
+Shift
and look for PropertyFilter.php
. Make sure to include non-project items. Open that up.
As a reminder, PropertyFilter
is an odd filter that is used to return less fields on each result. That's different than most filters whose job is to return less items in the collection.
In PropertyFilter
, search for getDescription()
.
Excellent! So what getDescription()
returns is an array of the query parameters that can be used to activate this filter. And under each one, we set a bunch of array keys that are ultimately used to generate documentation for that query parameter.
Unfortunately, nobody likes random arrays like this. And honestly, the best way to see what keys we should fill-in is by checking out these core classes.
Close this filter and let's open one more: hit Shift
+Shift
and look for SearchFilter.php
... and make sure to include non-project items.
In this case, getDescription()
actually lives in SearchFilterTrait
. Hold Command or Control
and click to open up that.
This returns the exact same structure, except that it's a bit more complex. It says $properties = $this->getProperties();
and loops over those to create the same array structure we saw earlier - with one query param per property.
In CheeseListing
, when you use SearchFilter
, you can pass it a properties
option that says:
I want this
SearchFilter
to work on all 4 of these properties
... lines 1 - 18 | |
/** | |
... lines 20 - 46 | |
* @ApiFilter(SearchFilter::class, properties={ | |
* "title": "partial", | |
* "description": "partial", | |
* "owner": "exact", | |
* "owner.username": "partial" | |
* }) | |
... lines 53 - 58 | |
class CheeseListing | |
{ | |
... lines 61 - 217 | |
} |
What that effectively does is create four different possible query parameters that you can use. And so the getDescription()
method returns an array with 4 items in it.
Close up those core classes.
Now because we're extending AbstractFilter
, one of the properties we magically have access to is $this->properties
. Let's dd()
that here so we can see what it looks like: dd($this->properties)
:
... lines 1 - 8 | |
class CheeseSearchFilter extends AbstractFilter | |
{ | |
... lines 11 - 14 | |
public function getDescription(string $resourceClass): array | |
{ | |
dd($this->properties); | |
} | |
} |
Back at the browser, open a new tab and go to /api/cheeses.jsonld
. But we won't hit the dump yet. Why? Because we haven't told API Platform that we want our CheeseListing
to use this filter.
How do we do that? We already know how! Back in CheeseListing
, we've done this before with other filters. Anywhere in here, add @ApiFilter()
with CheeseSearchFilter::class
:
... lines 1 - 19 | |
/** | |
... lines 21 - 55 | |
* @ApiFilter(CheeseSearchFilter::class) | |
... lines 57 - 59 | |
*/ | |
class CheeseListing | |
{ | |
... lines 63 - 219 | |
} |
Unfortunately, PhpStorm doesn't auto-complete that for me... so I'll copy the class name and add it on top manually: use App\ApiPlatform\CheeseSearchFilter
:
... lines 1 - 10 | |
use App\ApiPlatform\CheeseSearchFilter; | |
... lines 12 - 221 |
As soon as we do this, when we refresh the endpoint, even though I don't have any query parameters in the URL... it does hit getDescription()
! Why? Because in JSON-LD Hydra, the filter documentation is part of the response.
Apparently, $this->properties
is null. Here is where - and when - $this->properties
comes into play. Often - and as we've seen - you can pass which properties you want a filter to operate on.
For example, if we wanted our filter to be configurable, we could say properties =
and pass it price
... or an array of fields:
... lines 1 - 19 | |
/** | |
... lines 21 - 55 | |
* @ApiFilter(CheeseSearchFilter::class, properties={"price"}) | |
... lines 57 - 59 | |
*/ | |
class CheeseListing | |
{ | |
... lines 63 - 219 | |
} |
As soon as we have this, when we refresh: we get price
in the array! So if you want the properties on your filter to be configurable, that is the purpose of $this->properties
.
Now, in our case, we're creating this filter entirely for a single class for our application. So we don't need to worry about making it configurable. Remove the properties
option from the annotation:
... lines 1 - 19 | |
/** | |
... lines 21 - 55 | |
* @ApiFilter(CheeseSearchFilter::class) | |
... lines 57 - 59 | |
*/ | |
class CheeseListing | |
{ | |
... lines 63 - 219 | |
} |
We're going to completely ignore $this->properties
... and do whatever we want.
Next, we now know enough to fill in getDescription()
for our filter. Once we've done that, we'll bring it to life!
Hey there,
That's a good question, and after looking at the ApiPlatform documentation, I didn't find a way to configure filter names. I think the only way to do that is by creating a custom filter, unless I missed something
Cheers!
Hi,
i'm trying to filter on computed values, i created a custom filter
$queryBuilder
->addSelect('onHand - onHold) AS stockAvailable ')
->andHaving('stockAvailable > 0')
;
There is no property stockAvailable just a method :
/**
* @Groups({"stock:read"})
*/
public function getStockAvailable(): int
{
return $this->onHand - $this->onHold;
}
but the filter seems not working, i wonder if my case is possible
Hey Joel L.
First, you will have to create a custom filter and hook it into the ApiPlatform system
https://api-platform.com/do...
Then, you'll have to code the right query to perform the filtering you require. I believe the code you wrote will work, the only thing you're missing is the query identifier you set at the moment of creating a `QueryBuilder`
Let me know if it worked. Cheers!
Actually i already try based on this example.
BTW the API Platform doc example is slightly different from the SFCast course. One is extending AbstractContextAwareFilter and the other one AbstractFilter, does it matter ?
I forgot to mention i'm using GraphQL
Those two abstract classes are almost the same, the only thing that changes is on the apply()
method theAbstractContextAwareFilter
adds an extra argument to the function, the $context
argument, if your filter requires the $context to operate, then you will have to extend from that class, otherwise you can extend from the other one
I'm not sure if all of this applies to GraphQL but I'd assume it does
Hi Abdelkarim,
If you go to this URL: https://api-platform.com/docs/core/filters/#doctrine-orm-and-mongodb-odm-filters and click on the YAML tab for its examples, you will find ways to do this using yaml. Hope it helps!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.5.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.1.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.4.5", // 2.8.2
"nelmio/cors-bundle": "^2.1", // 2.1.0
"nesbot/carbon": "^2.17", // 2.39.1
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
"ramsey/uuid-doctrine": "^1.6", // 1.6.0
"symfony/asset": "5.1.*", // v5.1.5
"symfony/console": "5.1.*", // v5.1.5
"symfony/debug-bundle": "5.1.*", // v5.1.5
"symfony/dotenv": "5.1.*", // v5.1.5
"symfony/expression-language": "5.1.*", // v5.1.5
"symfony/flex": "^1.1", // v1.18.7
"symfony/framework-bundle": "5.1.*", // v5.1.5
"symfony/http-client": "5.1.*", // v5.1.5
"symfony/monolog-bundle": "^3.4", // v3.5.0
"symfony/security-bundle": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/validator": "5.1.*", // v5.1.5
"symfony/webpack-encore-bundle": "^1.6", // v1.8.0
"symfony/yaml": "5.1.*" // v5.1.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
"symfony/browser-kit": "5.1.*", // v5.1.5
"symfony/css-selector": "5.1.*", // v5.1.5
"symfony/maker-bundle": "^1.11", // v1.23.0
"symfony/phpunit-bridge": "5.1.*", // v5.1.5
"symfony/stopwatch": "5.1.*", // v5.1.5
"symfony/twig-bundle": "5.1.*", // v5.1.5
"symfony/web-profiler-bundle": "5.1.*", // v5.1.5
"zenstruck/foundry": "^1.1" // v1.8.0
}
}
Hi,
if I need to decorate the name of the parameter for a nested resource do I need to create a Custom filter or there is a simple way to do so?
I would like to have this parameter as
api/partnerRestriction?restriction=test
and not
api/partnerRestriction?partnerRestrictionTag.value=test