Skip to content

Commit

Permalink
Merge branch '2.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas committed Jan 14, 2019
2 parents cc4f3c9 + 5dcfe2e commit e533d67
Show file tree
Hide file tree
Showing 14 changed files with 589 additions and 88 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -32,6 +32,11 @@
* Add a `show_webby` configuration option to hide the spider in API docs
* Add an easter egg (find it!)

## 2.3.6

* Fix normalization of raw collections (not API resources)
* Fix content negotiation format matching

## 2.3.5

* GraphQL: compatibility with `webonyx/graphql-php` 0.13
Expand Down
15 changes: 15 additions & 0 deletions features/graphql/mutation.feature
Expand Up @@ -122,6 +122,21 @@ Feature: GraphQL mutation support
And the JSON node "data.deleteFoo.id" should be equal to "/foos/1"
And the JSON node "data.deleteFoo.clientMutationId" should be equal to "anotherId"

Scenario: Trigger an error trying to delete item of different resource
When I send the following GraphQL request:
"""
mutation {
deleteFoo(input: {id: "/dummies/1", clientMutationId: "myId"}) {
id
clientMutationId
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
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".'

Scenario: Delete an item with composite identifiers through a mutation
Given there are Composite identifier objects
When I send the following GraphQL request:
Expand Down
58 changes: 58 additions & 0 deletions src/Api/FormatMatcher.php
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Api;

/**
* Matches a mime type to a format.
*
* @internal
*/
final class FormatMatcher
{
private $formats;

public function __construct(array $formats)
{
$normalizedFormats = [];
foreach ($formats as $format => $mimeTypes) {
$normalizedFormats[$format] = (array) $mimeTypes;
}
$this->formats = $normalizedFormats;
}

/**
* Gets the format associated with the mime type.
*
* Adapted from {@see \Symfony\Component\HttpFoundation\Request::getFormat}.
*/
public function getFormat(string $mimeType): ?string
{
$canonicalMimeType = null;
$pos = strpos($mimeType, ';');
if (false !== $pos) {
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
}

foreach ($this->formats as $format => $mimeTypes) {
if (\in_array($mimeType, $mimeTypes, true)) {
return $format;
}
if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) {
return $format;
}
}

return null;
}
}
26 changes: 15 additions & 11 deletions src/EventListener/AddFormatListener.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\EventListener;

use ApiPlatform\Core\Api\FormatMatcher;
use ApiPlatform\Core\Api\FormatsProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
Expand All @@ -23,7 +24,7 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
* Chooses the format to user according to the Accept header and supported formats.
* Chooses the format to use according to the Accept header and supported formats.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
Expand All @@ -33,6 +34,7 @@ final class AddFormatListener
private $formats = [];
private $mimeTypes;
private $formatsProvider;
private $formatMatcher;

/**
* @throws InvalidArgumentException
Expand All @@ -43,14 +45,13 @@ public function __construct(Negotiator $negotiator, /* FormatsProviderInterface
if (\is_array($formatsProvider)) {
@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);
$this->formats = $formatsProvider;
} else {
if (!$formatsProvider instanceof FormatsProviderInterface) {
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
}

return;
$this->formatsProvider = $formatsProvider;
}
if (!$formatsProvider instanceof FormatsProviderInterface) {
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
}

$this->formatsProvider = $formatsProvider;
}

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

$this->populateMimeTypes();
$this->addRequestFormats($request, $this->formats);
Expand All @@ -86,11 +88,11 @@ public function onKernelRequest(GetResponseEvent $event)
/** @var string|null $accept */
$accept = $request->headers->get('Accept');
if (null !== $accept) {
if (null === $acceptHeader = $this->negotiator->getBest($accept, $mimeTypes)) {
if (null === $mediaType = $this->negotiator->getBest($accept, $mimeTypes)) {
throw $this->getNotAcceptableHttpException($accept, $mimeTypes);
}

$request->setRequestFormat($request->getFormat($acceptHeader->getType()));
$request->setRequestFormat($this->formatMatcher->getFormat($mediaType->getType()));

return;
}
Expand All @@ -116,12 +118,14 @@ public function onKernelRequest(GetResponseEvent $event)
}

/**
* Adds API formats to the HttpFoundation Request.
* Adds the supported formats to the request.
*
* This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work.
*/
private function addRequestFormats(Request $request, array $formats)
{
foreach ($formats as $format => $mimeTypes) {
$request->setFormat($format, $mimeTypes);
$request->setFormat($format, (array) $mimeTypes);
}
}

Expand Down
16 changes: 9 additions & 7 deletions src/EventListener/DeserializeListener.php
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\EventListener;

use ApiPlatform\Core\Api\FormatMatcher;
use ApiPlatform\Core\Api\FormatsProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
Expand All @@ -34,6 +35,7 @@ final class DeserializeListener
private $serializerContextBuilder;
private $formats = [];
private $formatsProvider;
private $formatMatcher;

/**
* @throws InvalidArgumentException
Expand All @@ -45,14 +47,13 @@ public function __construct(SerializerInterface $serializer, SerializerContextBu
if (\is_array($formatsProvider)) {
@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);
$this->formats = $formatsProvider;
} else {
if (!$formatsProvider instanceof FormatsProviderInterface) {
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
}

return;
$this->formatsProvider = $formatsProvider;
}
if (!$formatsProvider instanceof FormatsProviderInterface) {
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
}

$this->formatsProvider = $formatsProvider;
}

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

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

$format = $request->getFormat($contentType);
$format = $this->formatMatcher->getFormat($contentType);
if (null === $format || !isset($this->formats[$format])) {
$supportedMimeTypes = [];
foreach ($this->formats as $mimeTypes) {
Expand Down
6 changes: 6 additions & 0 deletions src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
Expand Up @@ -23,6 +23,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
use ApiPlatform\Core\Validator\Exception\ValidationException;
use ApiPlatform\Core\Validator\ValidatorInterface;
use GraphQL\Error\Error;
Expand All @@ -39,6 +40,7 @@
*/
final class ItemMutationResolverFactory implements ResolverFactoryInterface
{
use ClassInfoTrait;
use FieldsToAttributesTrait;
use ResourceAccessCheckerTrait;

Expand Down Expand Up @@ -83,6 +85,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
} catch (ItemNotFoundException $e) {
throw Error::createLocatedError(sprintf('Item "%s" not found.', $args['input']['id']), $info->fieldNodes, $info->path);
}

if ($resourceClass !== $this->getObjectClass($item)) {
throw Error::createLocatedError(sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $resourceClass), $info->fieldNodes, $info->path);
}
}

$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
Expand Down
11 changes: 10 additions & 1 deletion src/Hydra/Serializer/CollectionFiltersNormalizer.php
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Core\Api\FilterInterface;
use ApiPlatform\Core\Api\FilterLocatorTrait;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
Expand Down Expand Up @@ -74,7 +75,15 @@ public function normalize($object, $format = null, array $context = [])
return $data;
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
try {
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
} catch (InvalidArgumentException $e) {
if (!isset($context['resource_class'])) {
return $data;
}

throw $e;
}
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

$operationName = $context['collection_operation_name'] ?? null;
Expand Down
29 changes: 23 additions & 6 deletions src/Hydra/Serializer/CollectionNormalizer.php
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\PaginatorInterface;
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
use ApiPlatform\Core\JsonLd\Serializer\JsonLdContextTrait;
use ApiPlatform\Core\Serializer\ContextTrait;
Expand Down Expand Up @@ -65,15 +66,18 @@ public function supportsNormalization($data, $format = null)
public function normalize($object, $format = null, array $context = [])
{
if (isset($context['api_sub_level'])) {
$data = [];
foreach ($object as $index => $obj) {
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
return $this->normalizeRawCollection($object, $format, $context);
}

try {
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
} catch (InvalidArgumentException $e) {
if (!isset($context['resource_class'])) {
return $this->normalizeRawCollection($object, $format, $context);
}

return $data;
throw $e;
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
$context = $this->initContext($resourceClass, $context);

Expand Down Expand Up @@ -109,4 +113,17 @@ public function hasCacheableSupportsMethod(): bool
{
return true;
}

/**
* Normalizes a raw collection (not API resources).
*/
private function normalizeRawCollection($object, $format = null, array $context = []): array
{
$data = [];
foreach ($object as $index => $obj) {
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
}

return $data;
}
}

0 comments on commit e533d67

Please sign in to comment.