Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Lucky you! You found an early release chapter - it will be fully polished and published shortly!

Query Extension

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

Coming soon...

When we get a collection of treasures, we're currently returning all of the treasures, even if they're unpublished. So probably some of these are unpublished treasures. We did add a filter so that you could control this, but really, we need to not return the published treasures automatically. So if you look for the API Platform Upgrade Guide, we actually looked at this earlier, search for the word state. They have a really cool spot here where they talk about providers and processors. We've already talked about state processors, like the persist processor on the put and post endpoints, which is actually responsible for saving the item to the database. But there's also something called the provider, and this is what's responsible for loading that object. For example, when we make a get request for a single item, the item provider is what's responsible for taking the ID and loading that single item. So in this case, the item provider loads that from the database. There's also a collection provider to load a collection of items. So if we wanted to hide unpublished treasures, we could decorate this collection provider and make that change, just like how we've been decorating the persist processor. But one tricky problem with that is that if we decorated this collection provider, it would make the query for all of the treasures, and then we would have to filter them out. So it's not great for performance, because we might query for 100 treasures and then filter half of them out. So in that case, it's not the best extension point. Fortunately, this collection provider provides its own extension point that allows us to modify the query before it's executed. So let's first modify a test to show the behavior we want. So find testGetCollectionOfTreasures. And what I'm going to do here is I'm going to take control of these five treasures and say isPublished true, because right now in DragonTreasureFactory, the isPublished is set to just a random value. So it might be true or it might be false. So now we'll have five published Dragon Treasures. And let's create one more. So I'll say createOne. And this time, this time, let's say isPublished false. Awesome. So what we want is we want this still to just return five items. Let's make sure this fails. So symfony php bin/console phpunit --filter=DragonTreasureResourceTest, and awesome. So now we are currently returning all six items. All right. To modify the query for a collection endpoint, we're going to create something called a query extension. So anywhere in src, but I'll do it in the ApiPlatform directory. Create a new class called DragonTreasureIsPublishedExtension. We're gonna make this implement QueryCollectionExtensionInterface. I'll go to code generate or command and on the Mac and generate the one method we need, which is called applyToCollection(). So it's pretty cool. It passes us the queryBuilder and a couple of other pieces of information here. And we can modify that queryBuilder. So this queryBuilder is all we're going to take into account things like pagination and any filters that have been applied. So those will all be there and we just modify it to add our custom thing. Now thanks to Symfony's autoconfiguration system, just because we have this class and it implements this interface, this is automatically going to be called whenever a collection endpoint is being used. And it's going to be called for every single resource. The first thing we need to do is say if (DragonTreasure::class !== $resourceClass). So it passes us the class that it's currently loading right here. Then we're just going to return. So we don't want to modify, for example, the user endpoint. All right now one of the weird things inside the queryBuilder is every queryBuilder has an alias that refers to the root table that we're working on. So usually inside of a repository class when you're creating a custom query, you'll do things like $this->createQueryBuilder('d') and d becomes your root alias. And then you need to refer to that other parts in the query whenever you're doing stuff. In this case, we didn't create the queryBuilder, so we don't control that root alias, but we can read the root alias by saying $queryBuilder->getRootAliases()[0]. There is a single one, but we want the one that's plural. And there's almost always only one root alias, so we can just use the 0 key. Now it's just normal modification. So queryBuilder and where and then I use a sprintf here. This is going to be a little dynamic because now we need to say percent s.isPublished = :isPublished and then pass in the rootAlias. And down here we say setParameter('isPublished', true) to only return the published ones. And then one more thing before we add it. All right, let's try that. Spin over, try your test. It's just that easy. We are now modifying the collection query. By the way, would this also work for sub-resources? Like for example, over in our documentation, you can see that you can also get treasures by going to /API/users/userID/treasures. Will this also hide the unpublished treasures there? The answer is yes. So it's not something you need to worry about. I won't show it, but you are absolutely handled in that situation as well. By the way, if you wanted admin users to be able to see unpublished items, you could add a little logic here to only add this if this is not an admin. All right, next. This query extension fixed the collection endpoint, but someone could still fetch a single unpublished treasure by SID and that would work. Same time.

Leave a comment!

Login or Register to join the conversation
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.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0