Skip to content

Commit 6da5467

Browse files
authored
feat: add state providers for Doctrine ORM (#4462)
1 parent 98e4439 commit 6da5467

File tree

19 files changed

+1618
-9
lines changed

19 files changed

+1618
-9
lines changed

.github/workflows/ci.yml

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,10 @@ jobs:
530530
php-version: ${{ matrix.php }}
531531
tools: pecl, composer
532532
extensions: intl, bcmath, curl, openssl, mbstring, mongodb
533-
coverage: none
533+
coverage: pcov
534534
ini-values: memory_limit=-1
535+
- name: Enable code coverage
536+
run: echo "COVERAGE=1" >> $GITHUB_ENV
535537
- name: Get composer cache directory
536538
id: composercache
537539
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
@@ -551,9 +553,43 @@ jobs:
551553
- name: Clear test app cache
552554
run: tests/Fixtures/app/console cache:clear --ansi
553555
- name: Run PHPUnit tests
554-
run: vendor/bin/simple-phpunit --group mongodb
556+
run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mongodb
555557
- name: Run Behat tests
556-
run: vendor/bin/behat -vv --out=std --format=progress --profile=mongodb --no-interaction
558+
run: |
559+
mkdir -p build/logs/behat
560+
if [ "$COVERAGE" = '1' ]; then
561+
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb-coverage --no-interaction --tags='~@php8'
562+
else
563+
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction --tags='~@php8'
564+
fi
565+
- name: Merge code coverage reports
566+
run: |
567+
wget -qO /usr/local/bin/phpcov https://phar.phpunit.de/phpcov.phar
568+
chmod +x /usr/local/bin/phpcov
569+
phpcov merge --clover build/logs/behat/clover.xml build/coverage
570+
continue-on-error: true
571+
- name: Upload test artifacts
572+
if: always()
573+
uses: actions/upload-artifact@v1
574+
with:
575+
name: behat-logs-php${{ matrix.php }}
576+
path: build/logs/behat
577+
continue-on-error: true
578+
- name: Upload coverage results to Codecov
579+
uses: codecov/codecov-action@v1
580+
with:
581+
name: behat-php${{ matrix.php }}
582+
flags: behat
583+
fail_ci_if_error: true
584+
continue-on-error: true
585+
- name: Upload coverage results to Coveralls
586+
env:
587+
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
588+
run: |
589+
composer global require --prefer-dist --no-interaction --no-progress --ansi cedx/coveralls
590+
export PATH="$PATH:$HOME/.composer/vendor/bin"
591+
coveralls build/logs/behat/clover.xml
592+
continue-on-error: true
557593

558594
elasticsearch:
559595
name: Behat (PHP ${{ matrix.php }}) (Elasticsearch)

features/main/crud.feature

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,3 +583,69 @@ Feature: Create-Retrieve-Update-Delete
583583
"foo": "bar"
584584
}
585585
"""
586+
587+
@php8
588+
Scenario: Create a resource ProviderEntity
589+
When I add "Content-Type" header equal to "application/ld+json"
590+
And I send a "POST" request to "/provider_entities" with body:
591+
"""
592+
{
593+
"foo": "bar"
594+
}
595+
"""
596+
Then the response status code should be 201
597+
And the response should be in JSON
598+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
599+
And the header "Content-Location" should be equal to "/provider_entities/1"
600+
And the header "Location" should be equal to "/provider_entities/1"
601+
And the JSON should be equal to:
602+
"""
603+
{
604+
"@context": "/contexts/ProviderEntity",
605+
"@id": "/provider_entities/1",
606+
"@type": "ProviderEntity",
607+
"id": 1,
608+
"foo": "bar"
609+
}
610+
"""
611+
612+
@php8
613+
Scenario: Get a collection of Provider Entities
614+
When I send a "GET" request to "/provider_entities"
615+
Then the response status code should be 200
616+
And the response should be in JSON
617+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
618+
And the JSON should be equal to:
619+
"""
620+
{
621+
"@context": "/contexts/ProviderEntity",
622+
"@id": "/provider_entities",
623+
"@type": "hydra:Collection",
624+
"hydra:member": [
625+
{
626+
"@id": "/provider_entities/1",
627+
"@type": "ProviderEntity",
628+
"id": 1,
629+
"foo": "bar"
630+
}
631+
],
632+
"hydra:totalItems": 1
633+
}
634+
"""
635+
636+
@php8
637+
Scenario: Get a resource ProviderEntity
638+
When I send a "GET" request to "/provider_entities/1"
639+
Then the response status code should be 200
640+
And the response should be in JSON
641+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
642+
And the JSON should be equal to:
643+
"""
644+
{
645+
"@context": "/contexts/ProviderEntity",
646+
"@id": "/provider_entities/1",
647+
"@type": "ProviderEntity",
648+
"id": 1,
649+
"foo": "bar"
650+
}
651+
"""
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Bridge\Doctrine\Odm\State;
15+
16+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationCollectionExtensionInterface;
17+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultCollectionExtensionInterface;
18+
use ApiPlatform\Exception\OperationNotFoundException;
19+
use ApiPlatform\Exception\RuntimeException;
20+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
21+
use ApiPlatform\State\ProviderInterface;
22+
use Doctrine\ODM\MongoDB\DocumentManager;
23+
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
24+
use Doctrine\Persistence\ManagerRegistry;
25+
26+
/**
27+
* Collection state provider using the Doctrine ODM.
28+
*/
29+
final class CollectionProvider implements ProviderInterface
30+
{
31+
private $resourceMetadataCollectionFactory;
32+
private $managerRegistry;
33+
private $collectionExtensions;
34+
35+
/**
36+
* @param AggregationCollectionExtensionInterface[] $collectionExtensions
37+
*/
38+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ManagerRegistry $managerRegistry, iterable $collectionExtensions = [])
39+
{
40+
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
41+
$this->managerRegistry = $managerRegistry;
42+
$this->collectionExtensions = $collectionExtensions;
43+
}
44+
45+
public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = [])
46+
{
47+
/** @var DocumentManager $manager */
48+
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
49+
50+
$repository = $manager->getRepository($resourceClass);
51+
if (!$repository instanceof DocumentRepository) {
52+
throw new RuntimeException(sprintf('The repository for "%s" must be an instance of "%s".', $resourceClass, DocumentRepository::class));
53+
}
54+
55+
$aggregationBuilder = $repository->createAggregationBuilder();
56+
foreach ($this->collectionExtensions as $extension) {
57+
$extension->applyToCollection($aggregationBuilder, $resourceClass, $operationName, $context);
58+
59+
if ($extension instanceof AggregationResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
60+
return $extension->getResult($aggregationBuilder, $resourceClass, $operationName, $context);
61+
}
62+
}
63+
64+
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
65+
try {
66+
$operation = $context['operation'] ?? (isset($context['graphql_operation_name']) ? $resourceMetadata->getGraphQlOperation($operationName) : $resourceMetadata->getOperation($operationName));
67+
$attribute = $operation->getExtraProperties()['doctrine_mongodb'] ?? [];
68+
} catch (OperationNotFoundException $e) {
69+
$attribute = $resourceMetadata->getOperation(null, true)->getExtraProperties()['doctrine_mongodb'] ?? [];
70+
}
71+
$executeOptions = $attribute['execute_options'] ?? [];
72+
73+
return $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions);
74+
}
75+
76+
public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool
77+
{
78+
if (!$this->managerRegistry->getManagerForClass($resourceClass) instanceof DocumentManager) {
79+
return false;
80+
}
81+
82+
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
83+
84+
return $operation->isCollection() ?? false;
85+
}
86+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Bridge\Doctrine\Odm\State;
15+
16+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationItemExtensionInterface;
17+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultItemExtensionInterface;
18+
use ApiPlatform\Exception\RuntimeException;
19+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
21+
use ApiPlatform\State\ProviderInterface;
22+
use Doctrine\ODM\MongoDB\DocumentManager;
23+
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
24+
use Doctrine\Persistence\ManagerRegistry;
25+
26+
/**
27+
* Item state provider using the Doctrine ODM.
28+
*
29+
* @author Kévin Dunglas <[email protected]>
30+
* @author Samuel ROZE <[email protected]>
31+
*/
32+
final class ItemProvider implements ProviderInterface
33+
{
34+
private $resourceMetadataCollectionFactory;
35+
private $managerRegistry;
36+
private $itemExtensions;
37+
38+
/**
39+
* @param AggregationItemExtensionInterface[] $itemExtensions
40+
*/
41+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ManagerRegistry $managerRegistry, iterable $itemExtensions = [])
42+
{
43+
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
44+
$this->managerRegistry = $managerRegistry;
45+
$this->itemExtensions = $itemExtensions;
46+
}
47+
48+
public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = [])
49+
{
50+
/** @var DocumentManager $manager */
51+
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
52+
53+
$fetchData = $context['fetch_data'] ?? true;
54+
if (!$fetchData) {
55+
return $manager->getReference($resourceClass, $identifiers);
56+
}
57+
58+
$repository = $manager->getRepository($resourceClass);
59+
if (!$repository instanceof DocumentRepository) {
60+
throw new RuntimeException(sprintf('The repository for "%s" must be an instance of "%s".', $resourceClass, DocumentRepository::class));
61+
}
62+
63+
$aggregationBuilder = $repository->createAggregationBuilder();
64+
65+
foreach ($identifiers as $propertyName => $value) {
66+
$aggregationBuilder->match()->field($propertyName)->equals($value);
67+
}
68+
69+
foreach ($this->itemExtensions as $extension) {
70+
$extension->applyToItem($aggregationBuilder, $resourceClass, $identifiers, $operationName, $context);
71+
72+
if ($extension instanceof AggregationResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
73+
return $extension->getResult($aggregationBuilder, $resourceClass, $operationName, $context);
74+
}
75+
}
76+
77+
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
78+
79+
if ($resourceMetadata instanceof ResourceMetadataCollection) {
80+
$attribute = $resourceMetadata->getOperation()->getExtraProperties()['doctrine_mongodb'] ?? [];
81+
}
82+
83+
$executeOptions = $attribute['execute_options'] ?? [];
84+
85+
return $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions)->current() ?: null;
86+
}
87+
88+
public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool
89+
{
90+
if (!$this->managerRegistry->getManagerForClass($resourceClass) instanceof DocumentManager) {
91+
return false;
92+
}
93+
94+
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
95+
96+
return !($operation->isCollection() ?? false);
97+
}
98+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Bridge\Doctrine\Orm\State;
15+
16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
17+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
18+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
19+
use ApiPlatform\Exception\RuntimeException;
20+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
21+
use ApiPlatform\State\ProviderInterface;
22+
use Doctrine\ORM\EntityManagerInterface;
23+
use Doctrine\Persistence\ManagerRegistry;
24+
25+
/**
26+
* Collection state provider using the Doctrine ORM.
27+
*
28+
* @author Kévin Dunglas <[email protected]>
29+
* @author Samuel ROZE <[email protected]>
30+
*/
31+
final class CollectionProvider implements ProviderInterface
32+
{
33+
private $resourceMetadataCollectionFactory;
34+
private $managerRegistry;
35+
private $collectionExtensions;
36+
37+
/**
38+
* @param QueryCollectionExtensionInterface[] $collectionExtensions
39+
*/
40+
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ManagerRegistry $managerRegistry, iterable $collectionExtensions = [])
41+
{
42+
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
43+
$this->managerRegistry = $managerRegistry;
44+
$this->collectionExtensions = $collectionExtensions;
45+
}
46+
47+
public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = [])
48+
{
49+
/** @var EntityManagerInterface $manager */
50+
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
51+
52+
$repository = $manager->getRepository($resourceClass);
53+
if (!method_exists($repository, 'createQueryBuilder')) {
54+
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
55+
}
56+
57+
$queryBuilder = $repository->createQueryBuilder('o');
58+
$queryNameGenerator = new QueryNameGenerator();
59+
foreach ($this->collectionExtensions as $extension) {
60+
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
61+
62+
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
63+
return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
64+
}
65+
}
66+
67+
return $queryBuilder->getQuery()->getResult();
68+
}
69+
70+
public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool
71+
{
72+
if (!$this->managerRegistry->getManagerForClass($resourceClass) instanceof EntityManagerInterface) {
73+
return false;
74+
}
75+
76+
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
77+
78+
return $operation->isCollection() ?? false;
79+
}
80+
}

0 commit comments

Comments
 (0)