PropertyFilter: Sparse Fieldsets
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSince dragons love expensive treasure, let's add a way for them to filter based on the value, like within a range. There's a built-in filter for that called RangeFilter. Find the $value property and, like we did before, use #[ApiFilter()] and inside RangeFilter (the one from ORM) ::class:
| // ... lines 1 - 6 | |
| use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; | |
| // ... lines 8 - 40 | |
| class DragonTreasure | |
| { | |
| // ... lines 43 - 62 | |
| (RangeFilter::class) | |
| private ?int $value = null; | |
| // ... lines 65 - 169 | |
| } |
This one doesn't need any other options, so... we're done! Dang... that was easy. When we refresh... open it up, and hit "Try it out".... look at that! We have a ton of new filters - value[between], value[gt] (or "greater than"), value[gte], etc. Let's try value[gt]... with a random number... maybe 500000. When we click "Execute"... yup! It updated the URL here. It's... not the prettiest URL ever - due to the encoding - but it works like a charm. And down in the results... apparently there are 18 treasures worth more than that!
PropertyFilter
The last filter I want to show you... isn't really a filter at all. It's a way our API clients to choose which fields they want returned... instead of which results.
To show this off, find the getDescription() method. Pretend that we want to return a shorter, truncated version of the description. To do this, copy the getDescription() method, paste it below, and rename it to getShortDescription():
| // ... lines 1 - 41 | |
| class DragonTreasure | |
| { | |
| // ... lines 44 - 99 | |
| public function getShortDescription(): string | |
| { | |
| // ... line 102 | |
| } | |
| // ... lines 104 - 176 | |
| } |
To truncate this, we can use the u() function from Symfony. Type u and make sure to hit "tab" to autocomplete that. This is a rare function that Symfony gives us... and hitting "tab" did add a use statement for it:
| // ... lines 1 - 20 | |
| use function Symfony\Component\String\u; | |
| // ... lines 22 - 41 | |
| class DragonTreasure | |
| { | |
| // ... lines 44 - 99 | |
| public function getShortDescription(): string | |
| { | |
| return u($this->getDescription())->truncate(40, '...'); | |
| } | |
| // ... lines 104 - 176 | |
| } |
This creates an object with all sorts of string-related goodies on it, including truncate(). Pass 40 to truncate at 40 characters followed by ....
Method done! To expose this to our API, above, add the Groups attribute with treasure:read:
| // ... lines 1 - 20 | |
| use function Symfony\Component\String\u; | |
| // ... lines 22 - 41 | |
| class DragonTreasure | |
| { | |
| // ... lines 44 - 98 | |
| (['treasure:read']) | |
| public function getShortDescription(): string | |
| { | |
| return u($this->getDescription())->truncate(40, '...'); | |
| } | |
| // ... lines 104 - 176 | |
| } |
Beautiful! Okay, head back to the documentation and refresh. Open the GET endpoint, hit "Try it out", "Execute" and... nice. Here's our truncated description!
Though... it is weird that we now return two descriptions: a short one and the regular one. If our API client wants the short description, it may not want us to also return the full-length description... for the sake of bandwidth.
What can we do? Introducing: the PropertyFilter! Head back to DragonTreasure. Unlike the others, this filter must go above the class. So right here, say ApiFilter, and then PropertyFilter (in this case, there's only one of them) ::class. There are some options you can pass to this - which you can find in the docs - but we don't need any of them:
| // ... lines 1 - 14 | |
| use ApiPlatform\Serializer\Filter\PropertyFilter; | |
| // ... lines 16 - 42 | |
| (PropertyFilter::class) | |
| class DragonTreasure | |
| { | |
| // ... lines 46 - 178 | |
| } |
So... what does that do? Head back, refresh the documentation, open up the GET collection endpoint, and hit "Try it out". Woh! We now see a properties[] box and we can add items to it. Let's try it! Add a new string called name and another called description.
Moment of truth. Hit "Execute", and... there it is! It popped these onto the URL like normal. But look at the response: it only contains the name and description fields. Well... it also contains the JSON-LD fields, but the real data is just those two fields.
If we removed the properties strings, we would get the normal, full response. So, by default, you get all fields. But users can now choose fewer fields if they want to.
What about Vulcain?
This all works quite nicely. But if you look at the API Platform documentation for the PropertyFilter, they actually recommend a different solution: something called "Vulcain". Nope, not Spock's home planet. We're talking about a protocol that adds features to your web server. It was created by the API Platform team, and if we scroll down a bit, they have a really good example.
Pretend that we have the following API. If we make a request to /books, we get these two books back. Simple enough. Then maybe we want to get more info about the first book, so we make a request to that URL - /books/1. Great! But... now we want details about the author, so we make a request to /authors/1.
So, to get all the book information and all the author information, we ultimately needed to make four requests: the original, plus 3 more. That's not great for performance.
What Vulcain allows you to do is just make this first request... but tell the server that it should push the data from the other requests to you.
We can see this best in JavaScript, and there's a little example down here. In this case, imagine that we're making a request to /books/1 but we know that we also need the author information. So, when we make the request, we include a special Preload header. This tells the server:
Hey! After returning the book data, use a server push to send me the information found by following the
authorIRI.
The really cool thing is that your JavaScript doesn't really change. You still use fetch() to make a second request to the bookJSON.author URL... except that this will return instantly because the browser already has the data.
I'm not going to get into all the specifics, but the Preload on the first example is even more impressive: /member/*/author. That tells the server to push all the data as if we had also requested each of the member keys - so all the books - and their author URLs.
The point is: if you use Vulcain, your API users can make tiny changes to enjoy huge performance benefits... without us needing to add a lot of fanciness to our API.
Next: Let's talk about formats. We know that our API can return JSON-LD, JSON, and even HTML representations of our resources. Let's add two new formats, including a CSV format, which will be the fastest CSV export feature you've ever built.
4 Comments
Hello, I have two questions. 1) Is it possible to filter on enum property? 2) How can I filter on a non-persisted property in my entity? I have other filters such as Boolean added as services and then passed in as filters array on a one of my collections (because I have two collections and I want the filters on one but not the other). Thank you in advance.
Hiya @Brent-M!
I'm actually not sure! I'm sure you could create a custom entity filter for this - https://symfonycasts.com/screencast/api-platform2-extending/entity-filter-logic - but I also can't find any info one way or another on if enum property filter support is there. I would give it a try... though I'm guessing you already have :)
In general, when you create a custom filter - https://symfonycasts.com/screencast/api-platform2-extending/entity-filter-logic - you're free to create filters that use any name - e.g.
foobar=baz. So, I think you could create a custom filter for our non-persisted property - e.g.?nonPersistedProp=foo. The trick is that, in your custom filter, you then need to somehow take this value -foo- and modify the query to only include the items that match. For a non-persisted field, sometimes it still may be obvious how you could modify the query for that. But in other cases, that non-persisted field is so custom & "odd", that it's not really possible to modify the query to return only the correct records.If you have that case, I'm less sure. But the flow goes like this:
A) The collection provider is called to fetch all of the object for
/products, for example.B) Internally (assuming your
ApiResourceis an entity), this will trigger the "doctrine extensions" system in API Platform... which includes the filters. In short, your custom filter will be called. But, if you aren't able to modify the query at this point.... you're stuck.C) The collection provider executes the query and returns the result.
So, if you can't modify the query, your best situation might be to decorate the collection provider, allow it to execute and return the results, and then to loop over those results, remove the ones you don't want, and return the rest. This can get trickier with pagination, as the collection provider isn't returning just 25 Products, it's returning a Paginator with 25 objects in it. But we do something similar in one tutorial - https://symfonycasts.com/screencast/api-platform-extending/pagination#codeblock-da08790a13 - we loop over the results in the paginator, then put the final results into our own paginator.
Let me know if any of this help!
Cheers!
Hey,
It's is possible to make preload request in PHP-Symfony ? I mean in one request get all Collections inside Collection - like in your example From Book API request get author data, purchase data of this book and so on
Regards
Hey @Mepcuk!
Oh, that's an excellent question / point. Yes, check out https://api-platform.com/docs/core/push-relations/ - let me know if that's what you're looking for.
Cheers!
"Houston: no signs of life"
Start the conversation!