Skip to content

Commit e533d67

Browse files
committed
Merge branch '2.3'
2 parents cc4f3c9 + 5dcfe2e commit e533d67

File tree

14 files changed

+589
-88
lines changed

14 files changed

+589
-88
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
* Add a `show_webby` configuration option to hide the spider in API docs
3333
* Add an easter egg (find it!)
3434

35+
## 2.3.6
36+
37+
* Fix normalization of raw collections (not API resources)
38+
* Fix content negotiation format matching
39+
3540
## 2.3.5
3641

3742
* GraphQL: compatibility with `webonyx/graphql-php` 0.13

features/graphql/mutation.feature

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,21 @@ Feature: GraphQL mutation support
122122
And the JSON node "data.deleteFoo.id" should be equal to "/foos/1"
123123
And the JSON node "data.deleteFoo.clientMutationId" should be equal to "anotherId"
124124

125+
Scenario: Trigger an error trying to delete item of different resource
126+
When I send the following GraphQL request:
127+
"""
128+
mutation {
129+
deleteFoo(input: {id: "/dummies/1", clientMutationId: "myId"}) {
130+
id
131+
clientMutationId
132+
}
133+
}
134+
"""
135+
Then the response status code should be 200
136+
And the response should be in JSON
137+
And the header "Content-Type" should be equal to "application/json"
138+
And the JSON node "errors[0].message" should be equal to 'Item "/dummies/1" did not match expected type "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo".'
139+
125140
Scenario: Delete an item with composite identifiers through a mutation
126141
Given there are Composite identifier objects
127142
When I send the following GraphQL request:

src/Api/FormatMatcher.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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\Core\Api;
15+
16+
/**
17+
* Matches a mime type to a format.
18+
*
19+
* @internal
20+
*/
21+
final class FormatMatcher
22+
{
23+
private $formats;
24+
25+
public function __construct(array $formats)
26+
{
27+
$normalizedFormats = [];
28+
foreach ($formats as $format => $mimeTypes) {
29+
$normalizedFormats[$format] = (array) $mimeTypes;
30+
}
31+
$this->formats = $normalizedFormats;
32+
}
33+
34+
/**
35+
* Gets the format associated with the mime type.
36+
*
37+
* Adapted from {@see \Symfony\Component\HttpFoundation\Request::getFormat}.
38+
*/
39+
public function getFormat(string $mimeType): ?string
40+
{
41+
$canonicalMimeType = null;
42+
$pos = strpos($mimeType, ';');
43+
if (false !== $pos) {
44+
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
45+
}
46+
47+
foreach ($this->formats as $format => $mimeTypes) {
48+
if (\in_array($mimeType, $mimeTypes, true)) {
49+
return $format;
50+
}
51+
if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) {
52+
return $format;
53+
}
54+
}
55+
56+
return null;
57+
}
58+
}

src/EventListener/AddFormatListener.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\EventListener;
1515

16+
use ApiPlatform\Core\Api\FormatMatcher;
1617
use ApiPlatform\Core\Api\FormatsProviderInterface;
1718
use ApiPlatform\Core\Exception\InvalidArgumentException;
1819
use ApiPlatform\Core\Util\RequestAttributesExtractor;
@@ -23,7 +24,7 @@
2324
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2425

2526
/**
26-
* Chooses the format to user according to the Accept header and supported formats.
27+
* Chooses the format to use according to the Accept header and supported formats.
2728
*
2829
* @author Kévin Dunglas <[email protected]>
2930
*/
@@ -33,6 +34,7 @@ final class AddFormatListener
3334
private $formats = [];
3435
private $mimeTypes;
3536
private $formatsProvider;
37+
private $formatMatcher;
3638

3739
/**
3840
* @throws InvalidArgumentException
@@ -43,14 +45,13 @@ public function __construct(Negotiator $negotiator, /* FormatsProviderInterface
4345
if (\is_array($formatsProvider)) {
4446
@trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED);
4547
$this->formats = $formatsProvider;
48+
} else {
49+
if (!$formatsProvider instanceof FormatsProviderInterface) {
50+
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
51+
}
4652

47-
return;
53+
$this->formatsProvider = $formatsProvider;
4854
}
49-
if (!$formatsProvider instanceof FormatsProviderInterface) {
50-
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
51-
}
52-
53-
$this->formatsProvider = $formatsProvider;
5455
}
5556

5657
/**
@@ -69,6 +70,7 @@ public function onKernelRequest(GetResponseEvent $event)
6970
if (null !== $this->formatsProvider) {
7071
$this->formats = $this->formatsProvider->getFormatsFromAttributes(RequestAttributesExtractor::extractAttributes($request));
7172
}
73+
$this->formatMatcher = new FormatMatcher($this->formats);
7274

7375
$this->populateMimeTypes();
7476
$this->addRequestFormats($request, $this->formats);
@@ -86,11 +88,11 @@ public function onKernelRequest(GetResponseEvent $event)
8688
/** @var string|null $accept */
8789
$accept = $request->headers->get('Accept');
8890
if (null !== $accept) {
89-
if (null === $acceptHeader = $this->negotiator->getBest($accept, $mimeTypes)) {
91+
if (null === $mediaType = $this->negotiator->getBest($accept, $mimeTypes)) {
9092
throw $this->getNotAcceptableHttpException($accept, $mimeTypes);
9193
}
9294

93-
$request->setRequestFormat($request->getFormat($acceptHeader->getType()));
95+
$request->setRequestFormat($this->formatMatcher->getFormat($mediaType->getType()));
9496

9597
return;
9698
}
@@ -116,12 +118,14 @@ public function onKernelRequest(GetResponseEvent $event)
116118
}
117119

118120
/**
119-
* Adds API formats to the HttpFoundation Request.
121+
* Adds the supported formats to the request.
122+
*
123+
* This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work.
120124
*/
121125
private function addRequestFormats(Request $request, array $formats)
122126
{
123127
foreach ($formats as $format => $mimeTypes) {
124-
$request->setFormat($format, $mimeTypes);
128+
$request->setFormat($format, (array) $mimeTypes);
125129
}
126130
}
127131

src/EventListener/DeserializeListener.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\EventListener;
1515

16+
use ApiPlatform\Core\Api\FormatMatcher;
1617
use ApiPlatform\Core\Api\FormatsProviderInterface;
1718
use ApiPlatform\Core\Exception\InvalidArgumentException;
1819
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
@@ -34,6 +35,7 @@ final class DeserializeListener
3435
private $serializerContextBuilder;
3536
private $formats = [];
3637
private $formatsProvider;
38+
private $formatMatcher;
3739

3840
/**
3941
* @throws InvalidArgumentException
@@ -45,14 +47,13 @@ public function __construct(SerializerInterface $serializer, SerializerContextBu
4547
if (\is_array($formatsProvider)) {
4648
@trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED);
4749
$this->formats = $formatsProvider;
50+
} else {
51+
if (!$formatsProvider instanceof FormatsProviderInterface) {
52+
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
53+
}
4854

49-
return;
55+
$this->formatsProvider = $formatsProvider;
5056
}
51-
if (!$formatsProvider instanceof FormatsProviderInterface) {
52-
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
53-
}
54-
55-
$this->formatsProvider = $formatsProvider;
5657
}
5758

5859
/**
@@ -79,6 +80,7 @@ public function onKernelRequest(GetResponseEvent $event)
7980
if (null !== $this->formatsProvider) {
8081
$this->formats = $this->formatsProvider->getFormatsFromAttributes($attributes);
8182
}
83+
$this->formatMatcher = new FormatMatcher($this->formats);
8284

8385
$format = $this->getFormat($request);
8486
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
@@ -114,7 +116,7 @@ private function getFormat(Request $request): string
114116
throw new NotAcceptableHttpException('The "Content-Type" header must exist.');
115117
}
116118

117-
$format = $request->getFormat($contentType);
119+
$format = $this->formatMatcher->getFormat($contentType);
118120
if (null === $format || !isset($this->formats[$format])) {
119121
$supportedMimeTypes = [];
120122
foreach ($this->formats as $mimeTypes) {

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2424
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2525
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
26+
use ApiPlatform\Core\Util\ClassInfoTrait;
2627
use ApiPlatform\Core\Validator\Exception\ValidationException;
2728
use ApiPlatform\Core\Validator\ValidatorInterface;
2829
use GraphQL\Error\Error;
@@ -39,6 +40,7 @@
3940
*/
4041
final class ItemMutationResolverFactory implements ResolverFactoryInterface
4142
{
43+
use ClassInfoTrait;
4244
use FieldsToAttributesTrait;
4345
use ResourceAccessCheckerTrait;
4446

@@ -83,6 +85,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
8385
} catch (ItemNotFoundException $e) {
8486
throw Error::createLocatedError(sprintf('Item "%s" not found.', $args['input']['id']), $info->fieldNodes, $info->path);
8587
}
88+
89+
if ($resourceClass !== $this->getObjectClass($item)) {
90+
throw Error::createLocatedError(sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $resourceClass), $info->fieldNodes, $info->path);
91+
}
8692
}
8793

8894
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Core\Api\FilterInterface;
1818
use ApiPlatform\Core\Api\FilterLocatorTrait;
1919
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
20+
use ApiPlatform\Core\Exception\InvalidArgumentException;
2021
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2122
use Psr\Container\ContainerInterface;
2223
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
@@ -74,7 +75,15 @@ public function normalize($object, $format = null, array $context = [])
7475
return $data;
7576
}
7677

77-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
78+
try {
79+
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
80+
} catch (InvalidArgumentException $e) {
81+
if (!isset($context['resource_class'])) {
82+
return $data;
83+
}
84+
85+
throw $e;
86+
}
7887
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
7988

8089
$operationName = $context['collection_operation_name'] ?? null;

src/Hydra/Serializer/CollectionNormalizer.php

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1919
use ApiPlatform\Core\DataProvider\PaginatorInterface;
2020
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
21+
use ApiPlatform\Core\Exception\InvalidArgumentException;
2122
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
2223
use ApiPlatform\Core\JsonLd\Serializer\JsonLdContextTrait;
2324
use ApiPlatform\Core\Serializer\ContextTrait;
@@ -65,15 +66,18 @@ public function supportsNormalization($data, $format = null)
6566
public function normalize($object, $format = null, array $context = [])
6667
{
6768
if (isset($context['api_sub_level'])) {
68-
$data = [];
69-
foreach ($object as $index => $obj) {
70-
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
69+
return $this->normalizeRawCollection($object, $format, $context);
70+
}
71+
72+
try {
73+
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
74+
} catch (InvalidArgumentException $e) {
75+
if (!isset($context['resource_class'])) {
76+
return $this->normalizeRawCollection($object, $format, $context);
7177
}
7278

73-
return $data;
79+
throw $e;
7480
}
75-
76-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
7781
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
7882
$context = $this->initContext($resourceClass, $context);
7983

@@ -109,4 +113,17 @@ public function hasCacheableSupportsMethod(): bool
109113
{
110114
return true;
111115
}
116+
117+
/**
118+
* Normalizes a raw collection (not API resources).
119+
*/
120+
private function normalizeRawCollection($object, $format = null, array $context = []): array
121+
{
122+
$data = [];
123+
foreach ($object as $index => $obj) {
124+
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
125+
}
126+
127+
return $data;
128+
}
112129
}

0 commit comments

Comments
 (0)