Skip to content

Commit cc8ae0b

Browse files
authored
feat(mongodb): add link implementation (#4599)
* feat(mongodb): add link implementation * fix: identifiers extractor should not always return strings
1 parent 821a077 commit cc8ae0b

File tree

21 files changed

+477
-78
lines changed

21 files changed

+477
-78
lines changed

.github/workflows/ci.yml

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,8 +856,6 @@ jobs:
856856
run: rm -Rf tests/Fixtures/app/var/cache/*
857857
- name: Convert annotations to attributes
858858
run: |
859-
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --transform-apisubresource -s -n
860-
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --annotation-to-api-resource -s -n
861859
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Entity --transform-apisubresource -s -n
862860
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Entity --annotation-to-api-resource -s -n
863861
- name: Clear test app cache
@@ -897,7 +895,67 @@ jobs:
897895
name: openapi-docs-php${{ matrix.php }}
898896
path: build/out/openapi
899897
continue-on-error: true
900-
898+
899+
behat-rector-upgrade-mongodb:
900+
name: Behat (PHP ${{ matrix.php }}) (Rector / MongoDB)
901+
runs-on: ubuntu-latest
902+
env:
903+
APP_ENV: mongodb
904+
MONGODB_URL: mongodb://localhost:27017
905+
timeout-minutes: 20
906+
strategy:
907+
matrix:
908+
php:
909+
- '8.0'
910+
fail-fast: false
911+
steps:
912+
- name: Checkout
913+
uses: actions/checkout@v2
914+
- name: Check
915+
run: |
916+
sudo systemctl start mongod.service
917+
- name: Setup PHP
918+
uses: shivammathur/setup-php@v2
919+
with:
920+
php-version: ${{ matrix.php }}
921+
tools: pecl, composer
922+
extensions: intl, bcmath, curl, openssl, mbstring, mongodb
923+
ini-values: memory_limit=-1
924+
- name: Get composer cache directory
925+
id: composercache
926+
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
927+
- name: Cache dependencies
928+
uses: actions/cache@v2
929+
with:
930+
path: ${{ steps.composercache.outputs.dir }}
931+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
932+
restore-keys: ${{ runner.os }}-composer-
933+
- name: Update project dependencies
934+
run: composer update --no-interaction --no-progress --ansi
935+
- name: Require Symfony components and Rector dependencies
936+
run: composer require symfony/uid rector/rector:0.12.5 --dev --no-interaction --no-progress --ansi
937+
- name: Install PHPUnit
938+
run: vendor/bin/simple-phpunit --version
939+
- name: Clear test app cache
940+
run: rm -Rf tests/Fixtures/app/var/cache/*
941+
- name: Convert annotations to attributes
942+
run: |
943+
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --transform-apisubresource -s -n
944+
tests/Fixtures/app/console api:rector:upgrade tests/Fixtures/TestBundle/Document --annotation-to-api-resource -s -n
945+
- name: Clear test app cache
946+
run: rm -Rf tests/Fixtures/app/var/cache/*
947+
- name: Run Behat tests
948+
run: |
949+
mkdir -p build/logs/behat
950+
vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb --no-interaction
951+
- name: Upload test artifacts
952+
if: always()
953+
uses: actions/upload-artifact@v1
954+
with:
955+
name: behat-logs-php${{ matrix.php }}
956+
path: build/logs/behat
957+
continue-on-error: true
958+
901959
windows-phpunit:
902960
name: Windows PHPUnit (PHP ${{ matrix.php }}) (SQLite)
903961
runs-on: windows-latest

src/Api/IdentifiersExtractor.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ private function resolveIdentifierValue($identifierValue, string $parameterName)
117117
throw new RuntimeException('No identifier value found, did you forgot to persist the entity?');
118118
}
119119

120+
if (is_scalar($identifierValue)) {
121+
return $identifierValue;
122+
}
123+
120124
// TODO: php 8 remove method_exists
121-
if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
125+
if (method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
122126
return (string) $identifierValue;
123127
}
124128

@@ -132,7 +136,11 @@ private function resolveIdentifierValue($identifierValue, string $parameterName)
132136
if (1 === \count($relatedLinks)) {
133137
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedLinks)->getIdentifiers()[0], $parameterName);
134138

135-
if ($identifierValue instanceof \Stringable || is_scalar($identifierValue) || method_exists($identifierValue, '__toString')) {
139+
if (is_scalar($identifierValue)) {
140+
return $identifierValue;
141+
}
142+
143+
if ($identifierValue instanceof \Stringable || method_exists($identifierValue, '__toString')) {
136144
return (string) $identifierValue;
137145
}
138146
}

src/Core/Bridge/Rector/Rules/AbstractAnnotationToAttributeRector.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ protected function resolveAttributes($items): array
181181
foreach ($values as $attribute => $value) {
182182
[$updatedAttribute, $updatedValue] = $this->getKeyValue(str_replace('"', '', $camelCaseToSnakeCaseNameConverter->normalize($attribute)), $value);
183183
if ($attribute !== $updatedAttribute) {
184-
$values[$updatedAttribute] = $updatedValue;
184+
if (\array_key_exists($updatedAttribute, $values) && \is_array($values[$updatedAttribute])) {
185+
$values[$updatedAttribute] = array_merge($values[$updatedAttribute], $updatedValue);
186+
} else {
187+
$values[$updatedAttribute] = $updatedValue;
188+
}
185189
unset($values[$attribute]);
186190
}
187191
}

src/Core/Bridge/Rector/Service/SubresourceTransformer.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Util\Inflector;
1717
use Doctrine\Common\Annotations\AnnotationReader;
18+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
1819
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver as ODMAnnotationDriver;
1920
use Doctrine\ODM\MongoDB\Mapping\MappingException as ODMMappingException;
2021
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -43,7 +44,7 @@ public function toUriVariables(array $subresourceMetadata): array
4344
foreach (array_reverse($subresourceMetadata['identifiers']) as $identifier => $identifiedBy) {
4445
[$fromClass, $fromIdentifier, $fromPathVariable] = $identifiedBy;
4546
$fromClassMetadata = $this->getDoctrineMetadata($fromClass);
46-
$fromClassMetadataAssociationMappings = $fromClassMetadata->getAssociationMappings();
47+
$fromClassMetadataAssociationMappings = $fromClassMetadata->associationMappings;
4748

4849
$uriVariables[$identifier] = [
4950
'from_class' => $fromClass,
@@ -62,7 +63,8 @@ public function toUriVariables(array $subresourceMetadata): array
6263
$toClass = $fromClass;
6364

6465
if (isset($fromProperty, $fromClassMetadataAssociationMappings[$fromProperty])) {
65-
if ($fromClassMetadataAssociationMappings[$fromProperty]['type'] & ClassMetadataInfo::TO_MANY && isset($fromClassMetadataAssociationMappings[$fromProperty]['mappedBy'])) {
66+
$type = $fromClassMetadataAssociationMappings[$fromProperty]['type'];
67+
if (((class_exists(ODMClassMetadata::class) && ODMClassMetadata::MANY === $type) || (\is_int($type) && $type & ClassMetadataInfo::TO_MANY)) && isset($fromClassMetadataAssociationMappings[$fromProperty]['mappedBy'])) {
6668
$uriVariables[$identifier]['to_property'] = $fromClassMetadataAssociationMappings[$fromProperty]['mappedBy'];
6769
$fromProperty = $identifier;
6870
continue;
@@ -75,8 +77,26 @@ public function toUriVariables(array $subresourceMetadata): array
7577
return array_reverse($uriVariables);
7678
}
7779

78-
private function getDoctrineMetadata(string $class): ClassMetadata
80+
/**
81+
* @return ODMClassMetadata|ClassMetadata
82+
*/
83+
private function getDoctrineMetadata(string $class)
7984
{
85+
if ($this->odmMetadataFactory && class_exists(ODMClassMetadata::class)) {
86+
$isDocument = true;
87+
$metadata = new ODMClassMetadata($class);
88+
89+
try {
90+
$this->odmMetadataFactory->loadMetadataForClass($class, $metadata);
91+
} catch (ODMMappingException $e) {
92+
$isDocument = false;
93+
}
94+
95+
if ($isDocument) {
96+
return $metadata;
97+
}
98+
}
99+
80100
$metadata = new ClassMetadata($class);
81101
$metadata->initializeReflection(new RuntimeReflectionService());
82102

@@ -87,13 +107,6 @@ private function getDoctrineMetadata(string $class): ClassMetadata
87107
} catch (MappingException $e) {
88108
}
89109

90-
try {
91-
if ($this->odmMetadataFactory) {
92-
$this->odmMetadataFactory->loadMetadataForClass($class, $metadata);
93-
}
94-
} catch (ODMMappingException $e) {
95-
}
96-
97110
return $metadata;
98111
}
99112
}

src/Core/Bridge/Symfony/Bundle/Command/RectorCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
173173
passthru($command);
174174
}
175175

176-
$output->write('Migration successful.');
176+
$output->writeln('Migration successful.');
177177

178178
return Command::SUCCESS;
179179
}
@@ -269,7 +269,7 @@ private function transformApiSubresource(string $src, OutputInterface $output)
269269
private function isThereSubresources($io, $output): bool
270270
{
271271
if ($io->confirm('Do you have any @ApiSubresource or #[ApiSubresource] left in your code ?')) {
272-
$output->write('You will not be able to convert them afterwards. Please run the command "Transform @ApiSubresource to alternate resources" first.');
272+
$output->writeln('You will not be able to convert them afterwards. Please run the command "Transform @ApiSubresource to alternate resources" first.');
273273

274274
return true;
275275
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Doctrine\Common\State;
15+
16+
use ApiPlatform\Exception\RuntimeException;
17+
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
18+
use ApiPlatform\Metadata\Link;
19+
use ApiPlatform\Metadata\Operation;
20+
21+
trait LinksHandlerTrait
22+
{
23+
/**
24+
* @param Operation|GraphQlOperation $operation
25+
*
26+
* @return Link[]
27+
*/
28+
private function getLinks(string $resourceClass, $operation, array $context): array
29+
{
30+
$links = ($operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables()) ?? [];
31+
32+
if (!($linkClass = $context['linkClass'] ?? false)) {
33+
return $links;
34+
}
35+
36+
$newLinks = [];
37+
38+
foreach ($links as $link) {
39+
if ($linkClass === $link->getFromClass()) {
40+
$newLinks[] = $link;
41+
}
42+
}
43+
44+
$operation = $this->resourceMetadataCollectionFactory->create($linkClass)->getOperation($operation->getName());
45+
foreach ($operation instanceof GraphQlOperation ? $operation->getLinks() : $operation->getUriVariables() as $link) {
46+
if ($resourceClass === $link->getToClass()) {
47+
$newLinks[] = $link;
48+
}
49+
}
50+
51+
if (!$newLinks) {
52+
throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
53+
}
54+
55+
return $newLinks;
56+
}
57+
58+
private function getIdentifierValue(array &$identifiers, string $name = null)
59+
{
60+
if (isset($identifiers[$name])) {
61+
$value = $identifiers[$name];
62+
unset($identifiers[$name]);
63+
64+
return $value;
65+
}
66+
67+
return array_shift($identifiers);
68+
}
69+
}

src/Doctrine/Orm/State/Processor.php renamed to src/Doctrine/Common/State/Processor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Doctrine\Orm\State;
14+
namespace ApiPlatform\Doctrine\Common\State;
1515

1616
use ApiPlatform\State\ProcessorInterface;
1717
use ApiPlatform\Util\ClassInfoTrait;

src/Doctrine/Odm/State/CollectionProvider.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Doctrine\Odm\Extension\AggregationResultCollectionExtensionInterface;
1818
use ApiPlatform\Exception\OperationNotFoundException;
1919
use ApiPlatform\Exception\RuntimeException;
20+
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
2021
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2122
use ApiPlatform\State\ProviderInterface;
2223
use Doctrine\ODM\MongoDB\DocumentManager;
@@ -29,6 +30,8 @@
2930
*/
3031
final class CollectionProvider implements ProviderInterface
3132
{
33+
use LinksHandlerTrait;
34+
3235
private $resourceMetadataCollectionFactory;
3336
private $managerRegistry;
3437
private $collectionExtensions;
@@ -55,6 +58,9 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
5558
}
5659

5760
$aggregationBuilder = $repository->createAggregationBuilder();
61+
62+
$this->handleLinks($aggregationBuilder, $identifiers, $context, $resourceClass, $operationName);
63+
5864
foreach ($this->collectionExtensions as $extension) {
5965
$extension->applyToCollection($aggregationBuilder, $resourceClass, $operationName, $context);
6066

@@ -83,6 +89,10 @@ public function supports(string $resourceClass, array $identifiers = [], ?string
8389

8490
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
8591

92+
if ($operation instanceof GraphQlOperation) {
93+
return true;
94+
}
95+
8696
return $operation->isCollection() ?? false;
8797
}
8898
}

src/Doctrine/Odm/State/ItemProvider.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
*/
3333
final class ItemProvider implements ProviderInterface
3434
{
35+
use LinksHandlerTrait;
36+
3537
private $resourceMetadataCollectionFactory;
3638
private $managerRegistry;
3739
private $itemExtensions;
@@ -53,7 +55,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
5355

5456
$fetchData = $context['fetch_data'] ?? true;
5557
if (!$fetchData) {
56-
return $manager->getReference($resourceClass, $identifiers);
58+
return $manager->getReference($resourceClass, reset($identifiers));
5759
}
5860

5961
/** @var ObjectRepository $repository */
@@ -64,9 +66,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string
6466

6567
$aggregationBuilder = $repository->createAggregationBuilder();
6668

67-
foreach ($identifiers as $propertyName => $value) {
68-
$aggregationBuilder->match()->field($propertyName)->equals($value);
69-
}
69+
$this->handleLinks($aggregationBuilder, $identifiers, $context, $resourceClass, $operationName);
7070

7171
foreach ($this->itemExtensions as $extension) {
7272
$extension->applyToItem($aggregationBuilder, $resourceClass, $identifiers, $operationName, $context);

0 commit comments

Comments
 (0)