Skip to content

Commit d9d83cf

Browse files
committed
Merge branch '2.3'
2 parents 797cea6 + d38fbfc commit d9d83cf

25 files changed

+312
-48
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
- *update-project-dependencies
111111
- run:
112112
name: Install PHPStan
113-
command: composer global require phpstan/phpstan:^0.10
113+
command: composer global require phpstan/phpstan:0.10.1
114114
- *save-composer-cache-by-revision
115115
- *save-composer-cache-by-branch
116116
- run:

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"psr/log": "^1.0",
4949
"ramsey/uuid": "^3.7",
5050
"ramsey/uuid-doctrine": "^1.4",
51-
"sensio/framework-extra-bundle": "^3.0.11 || ^4.0 || ^5.0",
5251
"symfony/asset": "^3.3 || ^4.0",
5352
"symfony/cache": "^3.3 || ^4.0",
5453
"symfony/config": "^3.4 || ^4.0",

features/bootstrap/FeatureContext.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,32 @@ public function thereAreDummyPropertyObjectsWithASharedGroup(int $nb)
244244
$this->manager->flush();
245245
}
246246

247+
/**
248+
* @Given there are :nb dummy property objects with different number of related groups
249+
*/
250+
public function thereAreDummyPropertyObjectsWithADifferentNumberRelatedGroups(int $nb)
251+
{
252+
for ($i = 1; $i <= $nb; ++$i) {
253+
$dummyGroup = new DummyGroup();
254+
$dummyProperty = new DummyProperty();
255+
256+
foreach (['foo', 'bar', 'baz'] as $property) {
257+
$dummyProperty->$property = $dummyGroup->$property = ucfirst($property).' #'.$i;
258+
}
259+
260+
$this->manager->persist($dummyGroup);
261+
$dummyGroups[$i] = $dummyGroup;
262+
263+
for ($j = 1; $j <= $i; ++$j) {
264+
$dummyProperty->groups[] = $dummyGroups[$j];
265+
}
266+
267+
$this->manager->persist($dummyProperty);
268+
}
269+
270+
$this->manager->flush();
271+
}
272+
247273
/**
248274
* @Given there are :nb dummy property objects with :nb2 groups
249275
*/

features/graphql/authorization.feature

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Feature: Authorization checking
1515
}
1616
}
1717
"""
18-
Then the response status code should be 400
18+
Then the response status code should be 200
1919
And the response should be in JSON
2020
And the header "Content-Type" should be equal to "application/json"
2121
And the JSON node "errors[0].message" should be equal to "Access Denied."
@@ -35,7 +35,7 @@ Feature: Authorization checking
3535
}
3636
}
3737
"""
38-
Then the response status code should be 400
38+
Then the response status code should be 200
3939
And the response should be in JSON
4040
And the header "Content-Type" should be equal to "application/json"
4141
And the JSON node "errors[0].message" should be equal to "Access Denied."
@@ -50,7 +50,7 @@ Feature: Authorization checking
5050
}
5151
}
5252
"""
53-
Then the response status code should be 400
53+
Then the response status code should be 200
5454
And the response should be in JSON
5555
And the header "Content-Type" should be equal to "application/json"
5656
And the JSON node "errors[0].message" should be equal to "Only admins can create a secured dummy."

features/graphql/mutation.feature

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Feature: GraphQL mutation support
142142

143143
@createSchema
144144
Scenario: Modify an item through a mutation
145-
Given there are 1 dummy objects
145+
Given there are 1 dummy objects having each 2 relatedDummies
146146
When I send the following GraphQL request:
147147
"""
148148
mutation {
@@ -151,6 +151,13 @@ Feature: GraphQL mutation support
151151
name
152152
description
153153
dummyDate
154+
relatedDummies {
155+
edges {
156+
node {
157+
name
158+
}
159+
}
160+
}
154161
clientMutationId
155162
}
156163
}
@@ -162,6 +169,7 @@ Feature: GraphQL mutation support
162169
And the JSON node "data.updateDummy.name" should be equal to "Dummy #1"
163170
And the JSON node "data.updateDummy.description" should be equal to "Modified description."
164171
And the JSON node "data.updateDummy.dummyDate" should be equal to "2018-06-05T00:00:00+00:00"
172+
And the JSON node "data.updateDummy.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy11"
165173
And the JSON node "data.updateDummy.clientMutationId" should be equal to "myId"
166174

167175
Scenario: Modify an item with composite identifiers through a mutation
@@ -252,7 +260,7 @@ Feature: GraphQL mutation support
252260
}
253261
}
254262
"""
255-
Then the response status code should be 400
263+
Then the response status code should be 200
256264
And the response should be in JSON
257265
And the header "Content-Type" should be equal to "application/json"
258266
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."

features/jsonapi/related-resouces-inclusion.feature

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,3 +584,81 @@ Feature: JSON API Inclusion of Related Resources
584584
]
585585
}
586586
"""
587+
588+
@createSchema
589+
Scenario: Request inclusion of a related resources on collection should not duplicated included object
590+
Given there are 2 dummy property objects with different number of related groups
591+
When I send a "GET" request to "/dummy_properties?include=groups"
592+
Then the response status code should be 200
593+
And the response should be in JSON
594+
And the JSON should be valid according to the JSON API schema
595+
And the JSON should be deep equal to:
596+
"""
597+
{
598+
"links": {
599+
"self": "/dummy_properties?include=groups"
600+
},
601+
"meta": {
602+
"totalItems": 2,
603+
"itemsPerPage": 3,
604+
"currentPage": 1
605+
},
606+
"data": [{
607+
"id": "/dummy_properties/1",
608+
"type": "DummyProperty",
609+
"attributes": {
610+
"_id": 1,
611+
"foo": "Foo #1",
612+
"bar": "Bar #1",
613+
"baz": "Baz #1"
614+
},
615+
"relationships": {
616+
"groups": {
617+
"data": [{
618+
"type": "DummyGroup",
619+
"id": "/dummy_groups/1"
620+
}]
621+
}
622+
}
623+
}, {
624+
"id": "/dummy_properties/2",
625+
"type": "DummyProperty",
626+
"attributes": {
627+
"_id": 2,
628+
"foo": "Foo #2",
629+
"bar": "Bar #2",
630+
"baz": "Baz #2"
631+
},
632+
"relationships": {
633+
"groups": {
634+
"data": [{
635+
"type": "DummyGroup",
636+
"id": "/dummy_groups/1"
637+
}, {
638+
"type": "DummyGroup",
639+
"id": "/dummy_groups/2"
640+
}]
641+
}
642+
}
643+
}],
644+
"included": [{
645+
"id": "/dummy_groups/1",
646+
"type": "DummyGroup",
647+
"attributes": {
648+
"_id": 1,
649+
"foo": "Foo #1",
650+
"bar": "Bar #1",
651+
"baz": "Baz #1"
652+
}
653+
}, {
654+
"id": "/dummy_groups/2",
655+
"type": "DummyGroup",
656+
"attributes": {
657+
"_id": 2,
658+
"foo": "Foo #2",
659+
"bar": "Bar #2",
660+
"baz": "Baz #2"
661+
}
662+
}]
663+
}
664+
"""

src/Bridge/Doctrine/Orm/Extension/FilterExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Core\Api\FilterCollection;
1717
use ApiPlatform\Core\Api\FilterLocatorTrait;
1818
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\FilterInterface;
19+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
1920
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
2021
use ApiPlatform\Core\Exception\InvalidArgumentException;
2122
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
@@ -60,12 +61,25 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
6061
return;
6162
}
6263

64+
$orderFilter = null;
65+
6366
foreach ($resourceFilters as $filterId) {
6467
$filter = $this->getFilter($filterId);
6568
if ($filter instanceof FilterInterface) {
69+
// Apply the OrderFilter after every other filter to avoid an edge case where OrderFilter would do a LEFT JOIN instead of an INNER JOIN
70+
if ($filter instanceof OrderFilter) {
71+
$orderFilter = $filter;
72+
continue;
73+
}
74+
6675
$context['filters'] = $context['filters'] ?? [];
6776
$filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
6877
}
6978
}
79+
80+
if (null !== $orderFilter) {
81+
$context['filters'] = $context['filters'] ?? [];
82+
$orderFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
83+
}
7084
}
7185
}

src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ protected function extractProperties(Request $request/*, string $resourceClass*/
269269
* the second element is the $field name
270270
* the third element is the $associations array
271271
*/
272-
protected function addJoinsForNestedProperty(string $property, string $rootAlias, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator/*, string $resourceClass*/): array
272+
protected function addJoinsForNestedProperty(string $property, string $rootAlias, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator/*, string $resourceClass, string $joinType*/): array
273273
{
274274
if (\func_num_args() > 4) {
275275
$resourceClass = func_get_arg(4);
@@ -283,12 +283,18 @@ protected function addJoinsForNestedProperty(string $property, string $rootAlias
283283
$resourceClass = null;
284284
}
285285

286+
if (\func_num_args() > 5) {
287+
$joinType = func_get_arg(5);
288+
} else {
289+
$joinType = null;
290+
}
291+
286292
$propertyParts = $this->splitPropertyParts($property, $resourceClass);
287293
$parentAlias = $rootAlias;
288294
$alias = null;
289295

290296
foreach ($propertyParts['associations'] as $association) {
291-
$alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association);
297+
$alias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $parentAlias, $association, $joinType);
292298
$parentAlias = $alias;
293299
}
294300

src/Bridge/Doctrine/Orm/Filter/OrderFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
1717
use Doctrine\Common\Persistence\ManagerRegistry;
18+
use Doctrine\ORM\Query\Expr\Join;
1819
use Doctrine\ORM\QueryBuilder;
1920
use Psr\Log\LoggerInterface;
2021
use Symfony\Component\HttpFoundation\Request;
@@ -146,7 +147,7 @@ protected function filterProperty(string $property, $direction, QueryBuilder $qu
146147
$field = $property;
147148

148149
if ($this->isPropertyNested($property, $resourceClass)) {
149-
list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
150+
list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::LEFT_JOIN);
150151
}
151152

152153
if (null !== $nullsComparison = $this->properties[$property]['nulls_comparison'] ?? null) {

src/Bridge/Doctrine/Orm/SubresourceDataProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private function buildQuery(array $identifiers, array $context, QueryNameGenerat
191191
foreach ($normalizedIdentifiers as $key => $value) {
192192
$placeholder = $queryNameGenerator->generateParameterName($key);
193193
$qb->andWhere("$alias.$key = :$placeholder");
194-
$topQueryBuilder->setParameter($placeholder, $value);
194+
$topQueryBuilder->setParameter($placeholder, $value, $classMetadata->getTypeOfField($key));
195195
}
196196

197197
// Recurse queries

src/Bridge/Doctrine/Orm/Util/QueryChecker.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static function hasRootEntityWithIdentifier(QueryBuilder $queryBuilder,
6464
->getManagerForClass($rootEntity)
6565
->getClassMetadata($rootEntity);
6666

67-
if ($rootMetadata instanceof ClassMetadata && ($isForeign ? $rootMetadata->isIdentifierComposite : $rootMetadata->containsForeignIdentifier)) {
67+
if ($rootMetadata instanceof ClassMetadata && ($isForeign ? $rootMetadata->containsForeignIdentifier : $rootMetadata->isIdentifierComposite)) {
6868
return true;
6969
}
7070
}

src/DataProvider/OperationDataProviderTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ private function extractIdentifiers(array $parameters, array $attributes)
9999
return $id;
100100
}
101101

102+
if (!isset($attributes['subresource_context'])) {
103+
throw new RuntimeException('Either "item_operation_name" or "collection_operation_name" must be defined, unless the "_api_receive" request attribute is set to false.');
104+
}
105+
102106
$identifiers = [];
103107

104108
foreach ($attributes['subresource_context']['identifiers'] as $key => list($id, $resourceClass, $hasIdentifier)) {

src/GraphQl/Action/EntrypointAction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function __invoke(Request $request): Response
6767
$executionResult = new ExecutionResult(null, [$e]);
6868
}
6969

70-
return new JsonResponse($executionResult->toArray($this->debug), $executionResult->errors ? Response::HTTP_BAD_REQUEST : Response::HTTP_OK);
70+
return new JsonResponse($executionResult->toArray($this->debug));
7171
}
7272

7373
private function parseRequest(Request $request): array

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2929

3030
/**
31-
* Creates a function retrieving a collection to resolve a GraphQL query.
31+
* Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation.
3232
*
3333
* @experimental
3434
*
@@ -62,7 +62,7 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
6262

6363
public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
6464
{
65-
return function ($source, $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass) {
65+
return function ($source, $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operationName) {
6666
if (null === $resourceClass) {
6767
return null;
6868
}
@@ -75,7 +75,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
7575
}
7676

7777
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
78-
$dataProviderContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true);
78+
$dataProviderContext = $resourceMetadata->getGraphqlAttribute($operationName ?? 'query', 'normalization_context', [], true);
7979
$dataProviderContext['attributes'] = $this->fieldsToAttributes($info);
8080
$dataProviderContext['filters'] = $this->getNormalizedFilters($args);
8181

@@ -87,7 +87,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
8787
$collection = $this->collectionDataProvider->getCollection($resourceClass, null, $dataProviderContext);
8888
}
8989

90-
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, 'query');
90+
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, $operationName ?? 'query');
9191

9292
if (!$this->paginationEnabled) {
9393
$data = [];

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,11 @@ private function getResourceFieldConfiguration(string $resourceClass, ResourceMe
249249
$args = $this->convertFilterArgsToTypes($args);
250250
}
251251

252-
if ($isInternalGraphqlType || $input || null !== $mutationName) {
252+
if ($isInternalGraphqlType || $input) {
253253
$resolve = null;
254254
} elseif ($this->isCollection($type)) {
255255
$resolverFactory = $this->collectionResolverFactory;
256-
$resolve = $resolverFactory($className, $rootResource);
256+
$resolve = $resolverFactory($className, $rootResource, $mutationName);
257257
} else {
258258
$resolve = $this->itemResolver;
259259
}

src/JsonApi/Serializer/CollectionNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ protected function getItemsData($object, string $format = null, array $context =
9090
$data['data'][] = $item['data'];
9191

9292
if (isset($item['included'])) {
93-
$data['included'] = array_unique(array_merge($data['included'] ?? [], $item['included']), SORT_REGULAR);
93+
$data['included'] = array_values(array_unique(array_merge($data['included'] ?? [], $item['included']), SORT_REGULAR));
9494
}
9595
}
9696

src/Operation/Factory/SubresourceOperationFactory.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
8888
// Handle maxDepth
8989
if ($rootResourceClass === $resourceClass) {
9090
$maxDepth = $subresource->getMaxDepth();
91+
// reset depth when we return to rootResourceClass
92+
$depth = 0;
9193
}
9294

9395
if (null !== $maxDepth && $depth >= $maxDepth) {
@@ -97,11 +99,6 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
9799
continue;
98100
}
99101

100-
if ($rootResourceClass === $resourceClass) {
101-
// reset depth when we return to rootResourceClass
102-
$depth = 0;
103-
}
104-
105102
$rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
106103
$operationName = 'get';
107104
$operation = [

0 commit comments

Comments
 (0)