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 SubscribeOur UserDataProvider
is now responsible for loading the collection of users. But we lost pagination and filtering because the normal data provider from Doctrine usually handles that!
Let's actually find that class. I'll hit Shift
+Shift
and look for CollectionDataProvider
- I'm kind of guessing that name. Oh, and make sure to include "Non-Project" items. Here it is: a CollectionDataProvider
in the Doctrine ORM Bridge.
Look down at getCollection()
: it creates a query builder and then loops over something called the "collection extensions". This is the Doctrine extension system in API Platform, and these extensions are actually what is responsible for changing the query to add things like pagination and filtering. Finally, at the bottom, we execute the query and get the result.
Oh, and notice: this class uses the ContextAwareCollectionDataProviderInterface
. That is why - in our UserDataProvider
- I chose to implement that instead of just CollectionDataProviderInterface
. I knew that we were going to eventually want to call the core Doctrine provider. And when we do, we would want to pass it the $context
argument.
So instead of doing the query ourselves, let's call the core Doctrine service! Our first job is to... find out what its service ID is! At your terminal, run bin/console debug:container
and let's search for data_provider
?
php bin/console debug:container data_provider
Let's see: ah! api_platform.doctrine.orm.collection_data_provider
! Oh, but there is another one with almost the same name below it. Huh. And at the bottom, there's another one called api_platform.collection_data_provider
.
This last one is almost definitely the entire data provider system: it's the service that's responsible for calling the supports()
method on all the true data providers. So we don't want to inject and call this because we would just end up calling ourselves again. We saw this when we decorated the data persisters.
But what about these 2 orm.collection_data_provider
services up here? Which one should we use? I'll hit 0 to get more details about the first.
Ah. It's subtle: "abstract: Yes". An abstract service is not a real service. It's more of a template that you can use to build real services. If we tried to inject this, Symfony would give us an error to tell us.
Let's run the command again:
php bin/console debug:container data_provider
This time, check out the other one. Yes! "Abstract no". This is the real service. The "default" is referring to our "default" ORM connection.
Ok! Let's go use this! Over in UserDataProvider
, remove the UserRepository
argument. Instead, inject a collection data provider: CollectionDataProviderInterface
and I'll call it $collectionDataProvider
:
... lines 1 - 4 | |
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; | |
... lines 6 - 9 | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
private $collectionDataProvider; | |
public function __construct(CollectionDataProviderInterface $collectionDataProvider) | |
{ | |
$this->collectionDataProvider = $collectionDataProvider; | |
} | |
... lines 18 - 27 | |
} |
You could also type-hint the specific Doctrine data provider class that we know we're going to inject - your call. Rename the argument and property... and then in getCollection()
, return $this->collectionDataProvider->getCollection()
and pass it $resourceClass
, $operationName
and - it looks a bit silly, but also pass $context
:
... lines 1 - 9 | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... lines 12 - 18 | |
public function getCollection(string $resourceClass, string $operationName = null, array $context = []) | |
{ | |
return $this->collectionDataProvider->getCollection($resourceClass, $operationName, $context); | |
} | |
... lines 23 - 27 | |
} |
That argument doesn't exist on the getCollection()
method of CollectionDataProviderInterface
, but we know that it will exist in the Doctrine class.
If we stopped now and tried things - um, don't try things unless you have XDebug installed - because... it's recursion! Every programmers favorite thing!
By default, Symfony will autowire the main collection data provider. It calls us, we call it, it calls us, chaos ensues. We had the same problem with our data persister.
To fix this, open config/services.yaml
and, at the bottom, override the user data provider service: App\DataProvider\UserDataProvider
. Add bind
and, in this case, we're going to bind the $collectionDataProvider
argument. Set this to @
, then go copy the service id that we know we need, and paste:
... lines 1 - 47 | |
App\DataProvider\UserDataProvider: | |
bind: | |
$collectionDataProvider: '@api_platform.doctrine.orm.default.collection_data_provider' |
Now it should work. Refresh the browser and... got it! Let's check the number of results and... yes! It stops at 30 users but says that there are 51 total. Pagination is back!
Now that we have a working UserDataProvider
, we are ready to add any custom fields we need. Let's do it next!
Got it working:
MongoDB example:
`
App\DataProvider\TestDataProvider:
arguments:
$collectionExtensions: !tagged api_platform.doctrine_mongodb.odm.aggregation_extension.collection
/**
* {@inheritdoc}
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): iterable
{
$queryBuilder = $this->testRepository->createAggregationBuilder();
foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $resourceClass, $operationName, $context);
if (($extension instanceof AggregationResultCollectionExtensionInterface) && $extension->supportsResult(
$resourceClass,
$operationName,
$context
)) {
return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
}
}
return $queryBuilder->getAggregation(['allowDiskUse' => true]);
}
`
Hey Rudi T.
I'm afraid MongoDB integration goes beyond the scope of this tutorial, but I'm glad you got it working and share your solution to others
Cheers!
// 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
}
}
Could you provide a MongoDB example as well?
Trying so far with:
/**