From e10d658cb25c00b0f7172b5f9c74b47993ec181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 2 Feb 2019 08:57:42 +0100 Subject: [PATCH 01/13] input and output class refactoring (#2483) * input and output class refactoring * Fix WriteListener * Fix unit tests --- src/Api/IdentifiersExtractor.php | 5 +- .../Symfony/Bundle/Resources/config/api.xml | 1 + src/Bridge/Symfony/Routing/ApiLoader.php | 4 -- .../OperationDataProviderTrait.php | 8 +-- src/EventListener/DeserializeListener.php | 15 +++-- src/EventListener/WriteListener.php | 20 +++++- ...ntextAwareIdentifierConverterInterface.php | 35 ++++++++++ src/Identifier/IdentifierConverter.php | 14 +++- .../Factory/SubresourceOperationFactory.php | 2 - src/Serializer/SerializerContextBuilder.php | 26 +++----- src/Util/AttributesExtractor.php | 3 - .../RequestDataCollectorTest.php | 2 +- .../Bridge/Symfony/Routing/ApiLoaderTest.php | 4 -- tests/EventListener/AddFormatListenerTest.php | 2 +- .../EventListener/DeserializeListenerTest.php | 22 +++---- tests/EventListener/WriteListenerTest.php | 7 +- .../SubresourceOperationFactoryTest.php | 46 ------------- .../SerializerFilterContextBuilderTest.php | 8 --- tests/Util/RequestAttributesExtractorTest.php | 66 ------------------- 19 files changed, 108 insertions(+), 182 deletions(-) create mode 100644 src/Identifier/ContextAwareIdentifierConverterInterface.php diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index a36d3ac4154..d307b36c0a7 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\ClassInfoTrait; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -33,13 +34,15 @@ final class IdentifiersExtractor implements IdentifiersExtractorInterface private $propertyMetadataFactory; private $propertyAccessor; private $resourceClassResolver; + private $resourceMetadataFactory; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); $this->resourceClassResolver = $resourceClassResolver; + $this->resourceMetadataFactory = $resourceMetadataFactory; if (null === $this->resourceClassResolver) { @trigger_error(sprintf('Not injecting %s in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.', ResourceClassResolverInterface::class), E_USER_DEPRECATED); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 7a23930994f..dc15cb59e81 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -156,6 +156,7 @@ + diff --git a/src/Bridge/Symfony/Routing/ApiLoader.php b/src/Bridge/Symfony/Routing/ApiLoader.php index 991787712ec..6af14127068 100644 --- a/src/Bridge/Symfony/Routing/ApiLoader.php +++ b/src/Bridge/Symfony/Routing/ApiLoader.php @@ -123,8 +123,6 @@ public function load($data, $type = null): RouteCollection '_controller' => $controller, '_format' => null, '_api_resource_class' => $operation['resource_class'], - '_api_input_class' => $operation['input_class'], - '_api_output_class' => $operation['output_class'], '_api_subresource_operation_name' => $operation['route_name'], '_api_subresource_context' => [ 'property' => $operation['property'], @@ -212,8 +210,6 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, - '_api_input_class' => $resourceMetadata->getAttribute('input_class', $resourceClass), - '_api_output_class' => $resourceMetadata->getAttribute('output_class', $resourceClass), sprintf('_api_%s_operation_name', $operationType) => $operationName, ] + ($operation['defaults'] ?? []), $operation['requirements'] ?? [], diff --git a/src/DataProvider/OperationDataProviderTrait.php b/src/DataProvider/OperationDataProviderTrait.php index daddfc2bf9c..d5a656fbfaf 100644 --- a/src/DataProvider/OperationDataProviderTrait.php +++ b/src/DataProvider/OperationDataProviderTrait.php @@ -49,7 +49,7 @@ trait OperationDataProviderTrait */ private function getCollectionData(array $attributes, array $context) { - return $this->collectionDataProvider->getCollection($attributes['output_class'] ?: $attributes['resource_class'], $attributes['collection_operation_name'], $context); + return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context); } /** @@ -59,7 +59,7 @@ private function getCollectionData(array $attributes, array $context) */ private function getItemData($identifiers, array $attributes, array $context) { - return $this->itemDataProvider->getItem($attributes['output_class'] ?: $attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context); + return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context); } /** @@ -75,7 +75,7 @@ private function getSubresourceData($identifiers, array $attributes, array $cont throw new RuntimeException('Subresources not supported'); } - return $this->subresourceDataProvider->getSubresource($attributes['output_class'] ?: $attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); + return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); } /** @@ -93,7 +93,7 @@ private function extractIdentifiers(array $parameters, array $attributes) $id = $parameters['id']; if (null !== $this->identifierConverter) { - return $this->identifierConverter->convert((string) $id, $attributes['output_class'] ?: $attributes['resource_class']); + return $this->identifierConverter->convert((string) $id, $attributes['resource_class']); } return $id; diff --git a/src/EventListener/DeserializeListener.php b/src/EventListener/DeserializeListener.php index 1fff5848178..d25848beff1 100644 --- a/src/EventListener/DeserializeListener.php +++ b/src/EventListener/DeserializeListener.php @@ -63,11 +63,11 @@ public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $method = $request->getMethod(); + if ( 'DELETE' === $method || $request->isMethodSafe(false) || !($attributes = RequestAttributesExtractor::extractAttributes($request)) - || false === ($attributes['input_class'] ?? null) || !$attributes['receive'] || ( '' === ($requestContent = $request->getContent()) @@ -76,17 +76,18 @@ public function onKernelRequest(GetResponseEvent $event) ) { return; } + + $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); + if (false === $context['input_class']) { + return; + } + // BC check to be removed in 3.0 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); - if (isset($context['input_class'])) { - $context['resource_class'] = $context['input_class']; - } $data = $request->attributes->get('data'); if (null !== $data) { @@ -96,7 +97,7 @@ public function onKernelRequest(GetResponseEvent $event) $request->attributes->set( 'data', $this->serializer->deserialize( - $requestContent, $attributes['input_class'], $format, $context + $requestContent, $context['input_class'], $format, $context ) ); } diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index e4d0df994f6..7752f049ccf 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; @@ -28,11 +29,13 @@ final class WriteListener { private $dataPersister; private $iriConverter; + private $resourceMetadataFactory; - public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null) + public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->dataPersister = $dataPersister; $this->iriConverter = $iriConverter; + $this->resourceMetadataFactory = $resourceMetadataFactory; } /** @@ -65,10 +68,21 @@ public function onKernelView(GetResponseForControllerResultEvent $event) // Controller result must be immutable for _api_write_item_iri // if it's class changed compared to the base class let's avoid calling the IriConverter // especially that the Output class could be a DTO that's not referencing any route - if (null !== $this->iriConverter && (false !== $attributes['output_class'] ?? null) && $attributes['resource_class'] === ($class = \get_class($controllerResult)) && $class === \get_class($event->getControllerResult())) { + if (null === $this->iriConverter) { + return; + } + + $hasOutput = true; + if (null !== $this->resourceMetadataFactory) { + $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']); + $hasOutput = false !== $resourceMetadata->getOperationAttribute($attributes, 'output_class', null, true); + } + + $class = \get_class($controllerResult); + if ($hasOutput && $attributes['resource_class'] === $class && $class === \get_class($event->getControllerResult())) { $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult)); } - break; + break; case 'DELETE': $this->dataPersister->remove($controllerResult); $event->setControllerResult(null); diff --git a/src/Identifier/ContextAwareIdentifierConverterInterface.php b/src/Identifier/ContextAwareIdentifierConverterInterface.php new file mode 100644 index 00000000000..155f9cbfc74 --- /dev/null +++ b/src/Identifier/ContextAwareIdentifierConverterInterface.php @@ -0,0 +1,35 @@ + + * + * 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\Identifier; + +/** + * Identifier converter. + * + * @author Antoine Bluchet + */ + +namespace ApiPlatform\Core\Identifier; + +/** + * Gives access to the context. + * + * @author Kévin Dunglas + */ +interface ContextAwareIdentifierConverterInterface extends IdentifierConverterInterface +{ + /** + * {@inheritdoc} + */ + public function convert(string $data, string $class, array $context = []): array; +} diff --git a/src/Identifier/IdentifierConverter.php b/src/Identifier/IdentifierConverter.php index 2c84a1d2a2f..d392925afb3 100644 --- a/src/Identifier/IdentifierConverter.php +++ b/src/Identifier/IdentifierConverter.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Symfony\Component\PropertyInfo\Type; /** @@ -23,24 +24,31 @@ * * @author Antoine Bluchet */ -final class IdentifierConverter implements IdentifierConverterInterface +final class IdentifierConverter implements ContextAwareIdentifierConverterInterface { private $propertyMetadataFactory; private $identifiersExtractor; private $identifierDenormalizers; + private $resourceMetadataFactory; - public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, $identifierDenormalizers) + public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, $identifierDenormalizers, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->propertyMetadataFactory = $propertyMetadataFactory; $this->identifiersExtractor = $identifiersExtractor; $this->identifierDenormalizers = $identifierDenormalizers; + $this->resourceMetadataFactory = $resourceMetadataFactory; } /** * {@inheritdoc} */ - public function convert(string $data, string $class): array + public function convert(string $data, string $class, array $context = []): array { + if (null !== $this->resourceMetadataFactory) { + $resourceMetadata = $this->resourceMetadataFactory->create($class); + $class = $resourceMetadata->getOperationAttribute($context, 'output_class', $class, true); + } + $keys = $this->identifiersExtractor->getIdentifiersFromResourceClass($class); if (($numIdentifiers = \count($keys)) > 1) { diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php index cc4cb051673..49062af7d0b 100644 --- a/src/Operation/Factory/SubresourceOperationFactory.php +++ b/src/Operation/Factory/SubresourceOperationFactory.php @@ -106,8 +106,6 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre 'collection' => $subresource->isCollection(), 'resource_class' => $subresourceClass, 'shortNames' => [$subresourceMetadata->getShortName()], - 'input_class' => $subresourceMetadata->getAttribute('input_class', $subresourceClass), - 'output_class' => $subresourceMetadata->getAttribute('output_class', $subresourceClass), ]; if (null === $parentOperation) { diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index 28267939d78..6cb29ae638a 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -45,36 +45,28 @@ public function createFromRequest(Request $request, bool $normalization, array $ $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']); $key = $normalization ? 'normalization_context' : 'denormalization_context'; - - $operationKey = null; - $operationType = null; - if (isset($attributes['collection_operation_name'])) { $operationKey = 'collection_operation_name'; $operationType = OperationType::COLLECTION; - } elseif (isset($attributes['subresource_operation_name'])) { + } elseif (isset($attributes['item_operation_name'])) { + $operationKey = 'item_operation_name'; + $operationType = OperationType::ITEM; + } else { $operationKey = 'subresource_operation_name'; $operationType = OperationType::SUBRESOURCE; } - if (null !== $operationKey) { - $attribute = $attributes[$operationKey]; - $context = $resourceMetadata->getCollectionOperationAttribute($attribute, $key, [], true); - $context[$operationKey] = $attribute; - } else { - $context = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], $key, [], true); - $context['item_operation_name'] = $attributes['item_operation_name']; - } - - $context['operation_type'] = $operationType ?: OperationType::ITEM; + $context = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], $key, [], true); + $context['operation_type'] = $operationType; + $context[$operationKey] = $attributes[$operationKey]; if (!$normalization && !isset($context['api_allow_update'])) { $context['api_allow_update'] = \in_array($request->getMethod(), ['PUT', 'PATCH'], true); } $context['resource_class'] = $attributes['resource_class']; - $context['input_class'] = $attributes['input_class'] ?? $attributes['resource_class']; - $context['output_class'] = $attributes['output_class'] ?? $attributes['resource_class']; + $context['input_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'input_class', $attributes['resource_class'], true); + $context['output_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'output_class', $attributes['resource_class'], true); $context['request_uri'] = $request->getRequestUri(); $context['uri'] = $request->getUri(); diff --git a/src/Util/AttributesExtractor.php b/src/Util/AttributesExtractor.php index da8f6e2742e..3a12d427f9d 100644 --- a/src/Util/AttributesExtractor.php +++ b/src/Util/AttributesExtractor.php @@ -35,9 +35,6 @@ private function __construct() public static function extractAttributes(array $attributes): array { $result = ['resource_class' => $attributes['_api_resource_class'] ?? null]; - $result['input_class'] = $attributes['_api_input_class'] ?? $result['resource_class']; - $result['output_class'] = $attributes['_api_output_class'] ?? $result['resource_class']; - if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) { $result['subresource_context'] = $subresourceContext; } diff --git a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index 2a6a52e4778..52968a32f32 100644 --- a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -136,7 +136,7 @@ public function testWithResource() $this->response ); - $this->assertSame(['resource_class' => DummyEntity::class, 'input_class' => DummyEntity::class, 'output_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes()); + $this->assertSame(['resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes()); $this->assertSame(['foo', 'bar'], $dataCollector->getAcceptableContentTypes()); $this->assertSame(DummyEntity::class, $dataCollector->getResourceClass()); $this->assertSame(['foo' => null, 'a_filter' => \stdClass::class], $dataCollector->getFilters()); diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php index ec9ecf162c9..bd9bd2506b0 100644 --- a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php @@ -305,8 +305,6 @@ private function getRoute(string $path, string $controller, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, - '_api_input_class' => $resourceClass, - '_api_output_class' => $resourceClass, sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, ] + $extraDefaults, $requirements, @@ -326,8 +324,6 @@ private function getSubresourceRoute(string $path, string $controller, string $r '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, - '_api_input_class' => $resourceClass, - '_api_output_class' => $resourceClass, '_api_subresource_operation_name' => $operationName, '_api_subresource_context' => $context, ], diff --git a/tests/EventListener/AddFormatListenerTest.php b/tests/EventListener/AddFormatListenerTest.php index 61da5fa819d..3c60f0556cf 100644 --- a/tests/EventListener/AddFormatListenerTest.php +++ b/tests/EventListener/AddFormatListenerTest.php @@ -262,7 +262,7 @@ public function testResourceClassSupportedRequestFormat() $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_class' => 'Foo', 'output_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled(); + $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php index de32a3a6991..dec97154af0 100644 --- a/tests/EventListener/DeserializeListenerTest.php +++ b/tests/EventListener/DeserializeListenerTest.php @@ -128,7 +128,7 @@ public function testDoNotCallWhenInputClassDisabled() { $eventProphecy = $this->prophesize(GetResponseEvent::class); - $request = new Request([], [], ['data' => new \stdClass(), '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_input_class' => false]); + $request = new Request([], [], ['data' => new \stdClass(), '_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post'], [], [], [], 'content'); $request->setMethod('POST'); $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); @@ -136,7 +136,7 @@ public function testDoNotCallWhenInputClassDisabled() $serializerProphecy->deserialize()->shouldNotBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->shouldNotBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => false, 'output_class' => false]); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->shouldNotBeCalled(); @@ -159,14 +159,14 @@ public function testDeserialize(string $method, bool $populateObject) $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : []; + $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject, 'input_class' => 'Foo', 'output_class' => 'Foo'] : ['input_class' => 'Foo', 'output_class' => 'Foo']; $serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(self::FORMATS)->shouldBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled(); $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($eventProphecy->reveal()); @@ -186,14 +186,14 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : []; + $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject, 'input_class' => 'Foo', 'output_class' => 'Foo'] : ['input_class' => 'Foo', 'output_class' => 'Foo']; $serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_class' => 'Foo', 'output_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled(); + $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled(); $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal()); @@ -216,10 +216,10 @@ public function testContentNegotiation() $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize('{}', 'Foo', 'xml', [])->willReturn(new \stdClass())->shouldBeCalled(); + $serializerProphecy->deserialize('{}', 'Foo', 'xml', ['input_class' => 'Foo', 'output_class' => 'Foo'])->willReturn(new \stdClass())->shouldBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo'])->shouldBeCalled(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled(); @@ -249,7 +249,7 @@ public function testNotSupportedContentType() $serializerProphecy->deserialize()->shouldNotBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest()->shouldNotBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo']); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled(); @@ -278,7 +278,7 @@ public function testNoContentType() $serializerProphecy->deserialize()->shouldNotBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest()->shouldNotBeCalled(); + $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input_class' => 'Foo', 'output_class' => 'Foo']); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); $formatsProviderProphecy->getFormatsFromAttributes(Argument::type('array'))->willReturn(['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']])->shouldBeCalled(); diff --git a/tests/EventListener/WriteListenerTest.php b/tests/EventListener/WriteListenerTest.php index 49fde689b04..0ed15abd39e 100644 --- a/tests/EventListener/WriteListenerTest.php +++ b/tests/EventListener/WriteListenerTest.php @@ -16,6 +16,8 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; use ApiPlatform\Core\EventListener\WriteListener; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; @@ -145,6 +147,9 @@ public function testOnKernelViewDoNotCallIriConverterWhenOutputClassDisabled() $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled(); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['output_class' => false])); + $dataPersisterProphecy->persist($dummy)->willReturn($dummy)->shouldBeCalled(); $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_collection_operation_name' => 'post', '_api_output_class' => false]); @@ -157,7 +162,7 @@ public function testOnKernelViewDoNotCallIriConverterWhenOutputClassDisabled() $dummy ); - (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event); + (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()))->onKernelView($event); } public function testOnKernelViewWithControllerResultAndRemove() diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php index 7cc87b44c5d..0f4521b0399 100644 --- a/tests/Operation/Factory/SubresourceOperationFactoryTest.php +++ b/tests/Operation/Factory/SubresourceOperationFactoryTest.php @@ -66,8 +66,6 @@ public function testCreate() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -80,8 +78,6 @@ public function testCreate() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -95,8 +91,6 @@ public function testCreate() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -111,8 +105,6 @@ public function testCreate() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -125,8 +117,6 @@ public function testCreate() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -140,8 +130,6 @@ public function testCreate() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -196,8 +184,6 @@ public function testCreateByOverriding() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -210,8 +196,6 @@ public function testCreateByOverriding() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -225,8 +209,6 @@ public function testCreateByOverriding() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -241,8 +223,6 @@ public function testCreateByOverriding() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -255,8 +235,6 @@ public function testCreateByOverriding() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -270,8 +248,6 @@ public function testCreateByOverriding() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -319,8 +295,6 @@ public function testCreateWithMaxDepth() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -381,8 +355,6 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -395,8 +367,6 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'secondSubresource', 'collection' => false, 'resource_class' => DummyValidatedEntity::class, - 'input_class' => DummyValidatedEntity::class, - 'output_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -409,8 +379,6 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'moreSubresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyValidatedEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -471,8 +439,6 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -485,8 +451,6 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'property' => 'secondSubresource', 'collection' => false, 'resource_class' => DummyValidatedEntity::class, - 'input_class' => DummyValidatedEntity::class, - 'output_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -530,8 +494,6 @@ public function testCreateSelfReferencingSubresources() 'property' => 'subresource', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -577,8 +539,6 @@ public function testCreateWithEnd() 'property' => 'subresource', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -591,8 +551,6 @@ public function testCreateWithEnd() 'property' => 'id', 'collection' => false, 'resource_class' => DummyEntity::class, - 'input_class' => DummyEntity::class, - 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -639,8 +597,6 @@ public function testCreateWithEndButNoCollection() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -690,8 +646,6 @@ public function testCreateWithRootResourcePrefix() 'identifiers' => [ ['id', DummyEntity::class, true], ], - 'input_class' => RelatedDummyEntity::class, - 'output_class' => RelatedDummyEntity::class, 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/root_resource_prefix/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php index 6ff7d027f9d..2ebd1708c7f 100644 --- a/tests/Serializer/SerializerFilterContextBuilderTest.php +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -36,8 +36,6 @@ public function testCreateFromRequestWithCollectionOperation() $attributes = [ 'resource_class' => DummyGroup::class, - 'input_class' => DummyGroup::class, - 'output_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -79,8 +77,6 @@ public function testCreateFromRequestWithItemOperation() $attributes = [ 'resource_class' => DummyGroup::class, - 'input_class' => DummyGroup::class, - 'output_class' => DummyGroup::class, 'item_operation_name' => 'put', ]; @@ -122,8 +118,6 @@ public function testCreateFromRequestWithoutFilters() $attributes = [ 'resource_class' => DummyGroup::class, - 'input_class' => DummyGroup::class, - 'output_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -158,8 +152,6 @@ public function testCreateFromRequestWithoutAttributes() $attributes = [ 'resource_class' => DummyGroup::class, - 'input_class' => DummyGroup::class, - 'output_class' => DummyGroup::class, 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true, diff --git a/tests/Util/RequestAttributesExtractorTest.php b/tests/Util/RequestAttributesExtractorTest.php index f383585ed74..464a7857378 100644 --- a/tests/Util/RequestAttributesExtractorTest.php +++ b/tests/Util/RequestAttributesExtractorTest.php @@ -30,8 +30,6 @@ public function testExtractCollectionAttributes() [ 'resource_class' => 'Foo', 'collection_operation_name' => 'post', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'receive' => true, 'persist' => true, ], @@ -47,8 +45,6 @@ public function testExtractItemAttributes() [ 'resource_class' => 'Foo', 'item_operation_name' => 'get', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'receive' => true, 'persist' => true, ], @@ -64,8 +60,6 @@ public function testExtractReceive() [ 'resource_class' => 'Foo', 'item_operation_name' => 'get', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'receive' => false, 'persist' => true, ], @@ -78,8 +72,6 @@ public function testExtractReceive() [ 'resource_class' => 'Foo', 'item_operation_name' => 'get', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'receive' => true, 'persist' => true, ], @@ -91,8 +83,6 @@ public function testExtractReceive() $this->assertEquals( [ 'resource_class' => 'Foo', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true, @@ -108,8 +98,6 @@ public function testExtractPersist() $this->assertEquals( [ 'resource_class' => 'Foo', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => false, @@ -122,8 +110,6 @@ public function testExtractPersist() $this->assertEquals( [ 'resource_class' => 'Foo', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true, @@ -136,8 +122,6 @@ public function testExtractPersist() $this->assertEquals( [ 'resource_class' => 'Foo', - 'input_class' => 'Foo', - 'output_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true, @@ -146,56 +130,6 @@ public function testExtractPersist() ); } - public function testExtractInputOutputResourceClass() - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_input_class' => 'Bar']); - - $this->assertEquals( - [ - 'resource_class' => 'Foo', - 'item_operation_name' => 'get', - 'input_class' => 'Bar', - 'output_class' => 'Foo', - 'receive' => true, - 'persist' => true, - ], - RequestAttributesExtractor::extractAttributes($request) - ); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_output_class' => 'Bar']); - - $this->assertEquals( - [ - 'resource_class' => 'Foo', - 'item_operation_name' => 'get', - 'input_class' => 'Foo', - 'output_class' => 'Bar', - 'receive' => true, - 'persist' => true, - ], - RequestAttributesExtractor::extractAttributes($request) - ); - - $request = new Request([], [], [ - '_api_resource_class' => 'Foo', - '_api_item_operation_name' => 'get', - '_api_input_class' => 'FooBar', - '_api_output_class' => 'Bar', - ]); - - $this->assertEquals( - [ - 'resource_class' => 'Foo', - 'item_operation_name' => 'get', - 'input_class' => 'FooBar', - 'output_class' => 'Bar', - 'receive' => true, - 'persist' => true, - ], - RequestAttributesExtractor::extractAttributes($request) - ); - } - public function testResourceClassNotSet() { $this->assertEmpty(RequestAttributesExtractor::extractAttributes(new Request([], [], ['_api_item_operation_name' => 'get']))); From bcfc1c4422832681df53ba577b2d91da12e5cb7f Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Sat, 2 Feb 2019 18:37:31 +0100 Subject: [PATCH 02/13] Error if references are not stored correctly for lookups (#2465) * Error if references are not stored correctly for lookups * Fix tests * Throw repositoryMethodLookupNotAllowed exception and only allow id ref --- .circleci/config.yml | 2 +- behat.yml.dist | 6 ++-- features/doctrine/date_filter.feature | 26 ++++++++++++++- features/main/crud.feature | 32 +++++++++++++++---- features/main/relation.feature | 1 + features/main/subresource.feature | 2 ++ features/mongodb/filters.feature | 29 +++++++++++++++++ .../MongoDbOdm/PropertyHelperTrait.php | 20 ++++++++++-- .../Extension/OrderExtensionTest.php | 2 +- .../TestBundle/Document/FourthLevel.php | 5 +++ .../TestBundle/Document/ThirdLevel.php | 5 +++ .../TestBundle/Entity/FourthLevel.php | 5 +++ .../Fixtures/TestBundle/Entity/ThirdLevel.php | 5 +++ tests/Fixtures/app/config/config_mongodb.yml | 2 +- tests/Fixtures/app/config/config_orm.yml | 2 +- 15 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 features/mongodb/filters.feature diff --git a/.circleci/config.yml b/.circleci/config.yml index 5fd4f0c9829..c501ac06b62 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -195,7 +195,7 @@ jobs: name: Run Behat tests command: |- mkdir -p build/logs/tmp build/cov - for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' | circleci tests split --split-by=timings); do + for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' -not -path 'features/mongodb/*' | circleci tests split --split-by=timings); do _f=${f//\//_} FEATURE="${_f}" phpdbg -qrr vendor/bin/behat --profile=coverage --suite=default --format=progress --out=std --format=junit --out=build/logs/tmp/"${_f}" "$f" done diff --git a/behat.yml.dist b/behat.yml.dist index 730c4f1bd59..e199ad83ea1 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -18,7 +18,7 @@ default: - 'Behat\MinkExtension\Context\MinkContext' - 'Behatch\Context\RestContext' filters: - tags: '~@postgres&&~@elasticsearch' + tags: '~@postgres&&~@mongodb&&~@elasticsearch' postgres: contexts: - 'DoctrineContext': @@ -37,7 +37,7 @@ default: - 'Behat\MinkExtension\Context\MinkContext' - 'Behatch\Context\RestContext' filters: - tags: '~@sqlite&&~@elasticsearch' + tags: '~@sqlite&&~@mongodb&&~@elasticsearch' mongodb: contexts: - 'DoctrineContext': @@ -106,4 +106,4 @@ coverage: - 'Behat\MinkExtension\Context\MinkContext' - 'Behatch\Context\RestContext' filters: - tags: '~@postgres&&~@elasticsearch' + tags: '~@postgres&&~@mongodb&&~@elasticsearch' diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature index f58c231d546..c4e2597fc70 100644 --- a/features/doctrine/date_filter.feature +++ b/features/doctrine/date_filter.feature @@ -404,7 +404,7 @@ Feature: Date filter on collections }, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],properties[]}", + "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -701,6 +701,30 @@ Feature: Date filter on collections "property": "relatedDummy.thirdLevel.fourthLevel.level", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.badFourthLevel.level", + "property": "relatedDummy.thirdLevel.badFourthLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.badFourthLevel.level[]", + "property": "relatedDummy.thirdLevel.badFourthLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[]", + "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "properties[]", diff --git a/features/main/crud.feature b/features/main/crud.feature index faa57966bf5..c24e480e1da 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -4,7 +4,6 @@ Feature: Create-Retrieve-Update-Delete I need to be able to retrieve, create, update and delete JSON-LD encoded resources. @createSchema - @mongodb Scenario: Create a resource When I add "Content-Type" header equal to "application/ld+json" And I send a "POST" request to "/dummies" with body: @@ -56,7 +55,6 @@ Feature: Create-Retrieve-Update-Delete } """ - @mongodb Scenario: Get a resource When I send a "GET" request to "/dummies/1" Then the response status code should be 200 @@ -93,7 +91,6 @@ Feature: Create-Retrieve-Update-Delete } """ - @mongodb Scenario: Get a not found exception When I send a "GET" request to "/dummies/42" Then the response status code should be 404 @@ -140,7 +137,7 @@ Feature: Create-Retrieve-Update-Delete "hydra:totalItems": 1, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],properties[]}", + "hydra:template": "/dummies{?dummyBoolean,relatedDummy.embeddedDummy.dummyBoolean,dummyDate[before],dummyDate[strictly_before],dummyDate[after],dummyDate[strictly_after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[strictly_before],relatedDummy.dummyDate[after],relatedDummy.dummyDate[strictly_after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],relatedDummy[exists],dummyFloat,dummyFloat[],dummyPrice,dummyPrice[],order[id],order[name],order[description],order[relatedDummy.name],order[relatedDummy.symfony],order[dummyDate],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,relatedDummy.thirdLevel.level,relatedDummy.thirdLevel.level[],relatedDummy.thirdLevel.fourthLevel.level,relatedDummy.thirdLevel.fourthLevel.level[],relatedDummy.thirdLevel.badFourthLevel.level,relatedDummy.thirdLevel.badFourthLevel.level[],relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level,relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[],properties[]}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { @@ -437,6 +434,30 @@ Feature: Create-Retrieve-Update-Delete "property": "relatedDummy.thirdLevel.fourthLevel.level", "required": false }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.badFourthLevel.level", + "property": "relatedDummy.thirdLevel.badFourthLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.badFourthLevel.level[]", + "property": "relatedDummy.thirdLevel.badFourthLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level[]", + "property": "relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "properties[]", @@ -448,7 +469,6 @@ Feature: Create-Retrieve-Update-Delete } """ - @mongodb Scenario: Update a resource When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/dummies/1" with body: @@ -502,7 +522,6 @@ Feature: Create-Retrieve-Update-Delete } """ - @mongodb Scenario: Update a resource with empty body When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/dummies/1" @@ -543,7 +562,6 @@ Feature: Create-Retrieve-Update-Delete } """ - @mongodb Scenario: Delete a resource When I send a "DELETE" request to "/dummies/1" Then the response status code should be 204 diff --git a/features/main/relation.feature b/features/main/relation.feature index af87a58c845..4491674aae0 100644 --- a/features/main/relation.feature +++ b/features/main/relation.feature @@ -20,6 +20,7 @@ Feature: Relations support "@id": "/third_levels/1", "@type": "ThirdLevel", "fourthLevel": null, + "badFourthLevel": null, "id": 1, "level": 3, "test": true diff --git a/features/main/subresource.feature b/features/main/subresource.feature index 60ed4e1bb53..16fa448cb03 100644 --- a/features/main/subresource.feature +++ b/features/main/subresource.feature @@ -245,6 +245,7 @@ Feature: Subresource support "@id": "/third_levels/1", "@type": "ThirdLevel", "fourthLevel": "/fourth_levels/1", + "badFourthLevel": null, "id": 1, "level": 3, "test": true @@ -262,6 +263,7 @@ Feature: Subresource support "@context": "/contexts/FourthLevel", "@id": "/fourth_levels/1", "@type": "FourthLevel", + "badThirdLevel": [], "id": 1, "level": 4 } diff --git a/features/mongodb/filters.feature b/features/mongodb/filters.feature new file mode 100644 index 00000000000..9e89239215d --- /dev/null +++ b/features/mongodb/filters.feature @@ -0,0 +1,29 @@ +@mongodb +Feature: Filters on collections + In order to retrieve large collections of resources + As a client software developer + I need to retrieve collections with filters + + @createSchema + Scenario: Error when getting collection with nested properties if references are not correctly stored (owning side) + Given there is a dummy object with a fourth level relation + When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4" + Then the response status code should be 500 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "@context" should be equal to "/contexts/Error" + And the JSON node "@type" should be equal to "hydra:Error" + And the JSON node "hydra:title" should be equal to "An error occurred" + And the JSON node "hydra:description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "trace" should exist + + Scenario: Error when getting collection with nested properties if references are not correctly stored (not owning side) + When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3" + Then the response status code should be 500 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "@context" should be equal to "/contexts/Error" + And the JSON node "@type" should be equal to "hydra:Error" + And the JSON node "hydra:title" should be equal to "An error occurred" + And the JSON node "hydra:description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "trace" should exist diff --git a/src/Bridge/Doctrine/MongoDbOdm/PropertyHelperTrait.php b/src/Bridge/Doctrine/MongoDbOdm/PropertyHelperTrait.php index aad64b3edcb..a27a381f23d 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/PropertyHelperTrait.php +++ b/src/Bridge/Doctrine/MongoDbOdm/PropertyHelperTrait.php @@ -17,6 +17,7 @@ use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDbOdmClassMetadata; +use Doctrine\ODM\MongoDB\Mapping\MappingException; /** * Helper trait regarding a property in a MongoDB document using the resource metadata. @@ -41,6 +42,7 @@ abstract protected function getClassMetadata(string $resourceClass): ClassMetada * Adds the necessary lookups for a nested property. * * @throws InvalidArgumentException If property is not nested + * @throws MappingException * * @return array An array where the first element is the $alias of the lookup, * the second element is the $field name @@ -66,9 +68,23 @@ protected function addLookupsForNestedProperty(string $property, Builder $aggreg $alias .= $propertyAlias; $referenceMapping = $classMetadata->getFieldMapping($association); + if (($isOwningSide = $referenceMapping['isOwningSide']) && MongoDbOdmClassMetadata::REFERENCE_STORE_AS_ID !== $referenceMapping['storeAs']) { + throw MappingException::cannotLookupDbRefReference($classMetadata->getReflectionClass()->getShortName(), $association); + } + if (!$isOwningSide) { + if (isset($referenceMapping['repositoryMethod']) || !isset($referenceMapping['mappedBy'])) { + throw MappingException::repositoryMethodLookupNotAllowed($classMetadata->getReflectionClass()->getShortName(), $association); + } + + $targetClassMetadata = $this->getClassMetadata($referenceMapping['targetDocument']); + if ($targetClassMetadata instanceof MongoDbOdmClassMetadata && MongoDbOdmClassMetadata::REFERENCE_STORE_AS_ID !== $targetClassMetadata->getFieldMapping($referenceMapping['mappedBy'])['storeAs']) { + throw MappingException::cannotLookupDbRefReference($classMetadata->getReflectionClass()->getShortName(), $association); + } + } + $aggregationBuilder->lookup($classMetadata->getAssociationTargetClass($association)) - ->localField($referenceMapping['isOwningSide'] ? $localField : '_id') - ->foreignField($referenceMapping['isOwningSide'] ? '_id' : $referenceMapping['mappedBy']) + ->localField($isOwningSide ? $localField : '_id') + ->foreignField($isOwningSide ? '_id' : $referenceMapping['mappedBy']) ->alias($alias); $aggregationBuilder->unwind("\$$alias"); diff --git a/tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php b/tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php index 80c9621cfc1..83eb238f046 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtensionTest.php @@ -135,7 +135,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation() $classMetadataProphecy->hasAssociation('name')->shouldBeCalled()->willReturn(false); $classMetadataProphecy->getAssociationTargetClass('author')->shouldBeCalled()->willReturn(Dummy::class); $classMetadataProphecy->hasReference('author')->shouldBeCalled()->willReturn(true); - $classMetadataProphecy->getFieldMapping('author')->shouldBeCalled()->willReturn(['isOwningSide' => true]); + $classMetadataProphecy->getFieldMapping('author')->shouldBeCalled()->willReturn(['isOwningSide' => true, 'storeAs' => ClassMetadata::REFERENCE_STORE_AS_ID]); $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadata(null, null, null, null, null, ['order' => ['author.name']])); diff --git a/tests/Fixtures/TestBundle/Document/FourthLevel.php b/tests/Fixtures/TestBundle/Document/FourthLevel.php index 35c6618a73b..089cc3194b2 100644 --- a/tests/Fixtures/TestBundle/Document/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Document/FourthLevel.php @@ -42,6 +42,11 @@ class FourthLevel */ private $level = 4; + /** + * @ODM\ReferenceMany(targetDocument=ThirdLevel::class, cascade={"persist"}, mappedBy="badFourthLevel", storeAs="id") + */ + public $badThirdLevel; + /** * @return int */ diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index c62a29f8c0e..6674be09efe 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -58,6 +58,11 @@ class ThirdLevel */ public $fourthLevel; + /** + * @ODM\ReferenceOne(targetDocument=FourthLevel::class, cascade={"persist"}) + */ + public $badFourthLevel; + /** * @return int */ diff --git a/tests/Fixtures/TestBundle/Entity/FourthLevel.php b/tests/Fixtures/TestBundle/Entity/FourthLevel.php index a1fe58b543e..cf15505f07a 100644 --- a/tests/Fixtures/TestBundle/Entity/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Entity/FourthLevel.php @@ -44,6 +44,11 @@ class FourthLevel */ private $level = 4; + /** + * @ORM\OneToMany(targetEntity=ThirdLevel::class, cascade={"persist"}, mappedBy="badFourthLevel") + */ + public $badThirdLevel; + /** * @return int */ diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 0a6b22146b2..54996296200 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -59,6 +59,11 @@ class ThirdLevel */ public $fourthLevel; + /** + * @ORM\ManyToOne(targetEntity=FourthLevel::class, cascade={"persist"}) + */ + public $badFourthLevel; + /** * @return int */ diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index e89f97ce586..19b68d05c7c 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -36,7 +36,7 @@ services: tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.range' } ] app.my_dummy_resource.mongodb.search_filter: parent: 'api_platform.doctrine_mongodb.odm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact' } ] + arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.search' } ] app.related_dummy_resource.mongodb.search_filter: parent: 'api_platform.doctrine_mongodb.odm.search_filter' diff --git a/tests/Fixtures/app/config/config_orm.yml b/tests/Fixtures/app/config/config_orm.yml index 257ccda7185..c268e70adb2 100644 --- a/tests/Fixtures/app/config/config_orm.yml +++ b/tests/Fixtures/app/config/config_orm.yml @@ -4,7 +4,7 @@ imports: services: app.my_dummy_resource.search_filter: parent: 'api_platform.doctrine.orm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact' } ] + arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] tags: [ { name: 'api_platform.filter', id: 'my_dummy.search' } ] # Tests if the id default to the service name, do not add id attributes here From 244d53d7fe9b510cc034b43b202481cbfba9740c Mon Sep 17 00:00:00 2001 From: RJ Garcia Date: Sat, 2 Feb 2019 14:21:59 -0800 Subject: [PATCH 03/13] Input Output Class Operation Fix - getTypedOperationAttribute expects the operation type constant Signed-off-by: RJ Garcia --- src/Serializer/SerializerContextBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index 6cb29ae638a..b14d8a7ca2d 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -65,8 +65,8 @@ public function createFromRequest(Request $request, bool $normalization, array $ } $context['resource_class'] = $attributes['resource_class']; - $context['input_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'input_class', $attributes['resource_class'], true); - $context['output_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'output_class', $attributes['resource_class'], true); + $context['input_class'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'input_class', $attributes['resource_class'], true); + $context['output_class'] = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], 'output_class', $attributes['resource_class'], true); $context['request_uri'] = $request->getRequestUri(); $context['uri'] = $request->getUri(); From 864fa7451bf6934d29fe7ce023be91a963b8ed1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 3 Feb 2019 12:12:37 +0100 Subject: [PATCH 04/13] Fix deps=lowest --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index b8cc1465a56..0fd47435faa 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,7 @@ "psr/log": "^1.0", "ramsey/uuid": "^3.7", "ramsey/uuid-doctrine": "^1.4", + "sebastian/object-enumerator": "^3.0.3", "symfony/asset": "^3.4 || ^4.0", "symfony/cache": "^3.4 || ^4.0", "symfony/config": "^3.4 || ^4.0", From 391e9df25698663b964e1adc36c8f3bbc2e1cf61 Mon Sep 17 00:00:00 2001 From: RJ Garcia Date: Sun, 3 Feb 2019 01:13:50 -0800 Subject: [PATCH 05/13] Denormalize Input Class with Different Fields Fixes the bug where AbstractItemSerializer would use the resource class instead of the input class to determine allowed properties. Enforced `$context['resource_class']` to be set to current denormalized class to resolve issue. Figured this should be the case since AbstractItemNormalizer only works on resources. Signed-off-by: RJ Garcia --- src/Serializer/AbstractItemNormalizer.php | 4 +- .../Entity/DummyForAdditionalFields.php | 56 +++++++++++++++++++ .../Entity/DummyForAdditionalFieldsInput.php | 34 +++++++++++ .../Serializer/AbstractItemNormalizerTest.php | 44 +++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/DummyForAdditionalFields.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyForAdditionalFieldsInput.php diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 4e5c2ca79b1..950fe22866a 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -128,9 +128,7 @@ public function supportsDenormalization($data, $type, $format = null) public function denormalize($data, $class, $format = null, array $context = []) { $context['api_denormalize'] = true; - if (!isset($context['resource_class'])) { - $context['resource_class'] = $class; - } + $context['resource_class'] = $class; return parent::denormalize($data, $class, $format, $context); } diff --git a/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFields.php b/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFields.php new file mode 100644 index 00000000000..4031ac8ff0c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFields.php @@ -0,0 +1,56 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + */ +class DummyForAdditionalFields +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + /** @ORM\Column */ + private $name; + /** @ORM\Column */ + private $slug; + + public function __construct(string $name, string $slug) + { + $this->name = $name; + $this->slug = $slug; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getSlug(): string + { + return $this->slug; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFieldsInput.php b/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFieldsInput.php new file mode 100644 index 00000000000..58a061645ef --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyForAdditionalFieldsInput.php @@ -0,0 +1,34 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; + +/** + * @ApiResource + */ +final class DummyForAdditionalFieldsInput +{ + private $dummyName; + + public function __construct(string $dummyName) + { + $this->dummyName = $dummyName; + } + + public function getDummyName(): string + { + return $this->dummyName; + } +} diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index 0d5fd6fb1f9..611087bc127 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -23,6 +23,8 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFields; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyForAdditionalFieldsInput; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; @@ -290,6 +292,48 @@ public function testDenormalize() ], Dummy::class); } + public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass() + { + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DummyForAdditionalFieldsInput::class, [])->willReturn( + new PropertyNameCollection(['dummyName']) + ); + $propertyNameCollectionFactoryProphecy->create(DummyForAdditionalFields::class, [])->willReturn( + new PropertyNameCollection(['id', 'name', 'slug']) + ); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + // Create DummyForAdditionalFieldsInput mocks + $propertyMetadataFactoryProphecy->create(DummyForAdditionalFieldsInput::class, 'dummyName', [])->willReturn( + (new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, false))->withInitializable(true) + ); + // Create DummyForAdditionalFields mocks + $propertyMetadataFactoryProphecy->create(DummyForAdditionalFields::class, 'id', [])->willReturn( + (new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), '', true, false))->withInitializable(false) + ); + $propertyMetadataFactoryProphecy->create(DummyForAdditionalFields::class, 'name', [])->willReturn( + (new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, false))->withInitializable(true) + ); + $propertyMetadataFactoryProphecy->create(DummyForAdditionalFields::class, 'slug', [])->willReturn( + (new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, false))->withInitializable(true) + ); + + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $this->prophesize(IriConverterInterface::class)->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal()) extends AbstractItemNormalizer { + }; + + /** @var DummyForAdditionalFieldsInput $res */ + $res = $normalizer->denormalize([ + 'dummyName' => 'Dummy Name', + ], DummyForAdditionalFieldsInput::class, 'json', [ + 'resource_class' => DummyForAdditionalFields::class, + 'input_class' => DummyForAdditionalFieldsInput::class, + 'output_class' => DummyForAdditionalFields::class, + ]); + + $this->assertInstanceOf(DummyForAdditionalFieldsInput::class, $res); + $this->assertEquals('Dummy Name', $res->getDummyName()); + } + public function testDenormalizeWritableLinks() { $relatedDummy1 = new RelatedDummy(); From aa7bb63ddfbf6a46121a820e7f6df0dfa6571fb5 Mon Sep 17 00:00:00 2001 From: RJ Garcia Date: Sat, 2 Feb 2019 18:50:28 -0800 Subject: [PATCH 06/13] Disable Messenger From Config - Added ability to disable messenger from config - Removed previous check if container has message_bus Signed-off-by: RJ Garcia --- .../DependencyInjection/ApiPlatformExtension.php | 15 ++++++++++----- .../Bundle/DependencyInjection/Configuration.php | 5 +++++ .../ApiPlatformExtensionTest.php | 14 +++++++++++++- .../DependencyInjection/ConfigurationTest.php | 3 +++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 0578fe3ee59..e19cf0e70f4 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -41,7 +41,6 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; @@ -149,11 +148,8 @@ public function load(array $configs, ContainerBuilder $container) $this->registerValidatorConfiguration($container, $config); $this->registerDataCollectorConfiguration($container, $config, $loader); $this->registerMercureConfiguration($container, $config, $loader, $useDoctrine); + $this->registerMessengerConfiguration($config, $loader); $this->registerElasticsearchConfiguration($container, $config, $loader); - - if (interface_exists(MessageBusInterface::class) && $container->has('message_bus')) { - $loader->load('messenger.xml'); - } } /** @@ -580,6 +576,15 @@ private function registerMercureConfiguration(ContainerBuilder $container, array } } + private function registerMessengerConfiguration(array $config, XmlFileLoader $loader) + { + if (!$config['messenger']['enabled']) { + return; + } + + $loader->load('messenger.xml'); + } + private function registerElasticsearchConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader) { $enabled = $config['elasticsearch']['enabled'] && class_exists(Client::class); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index 05f799652a4..6186cad80d0 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -28,6 +28,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Serializer\Exception\ExceptionInterface; /** @@ -245,6 +246,10 @@ public function getConfigTreeBuilder() ->end() ->end() + ->arrayNode('messenger') + ->{interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->end() + ->arrayNode('elasticsearch') ->canBeEnabled() ->addDefaultsIfNotSet() diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 9a7520084da..7d4999ef268 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -471,6 +471,19 @@ public function testDisabledSwaggerUIAndRedoc() $this->extension->load($config, $containerBuilder); } + public function testDisabledMessenger() + { + $containerBuilderProphecy = $this->getBaseContainerBuilderProphecy(); + $containerBuilderProphecy->setAlias('api_platform.message_bus', 'message_bus')->shouldNotBeCalled(); + $containerBuilderProphecy->setDefinition('api_platform.messenger.data_persister', Argument::type(Definition::class))->shouldNotBeCalled(); + $containerBuilder = $containerBuilderProphecy->reveal(); + + $config = self::DEFAULT_CONFIG; + $config['api_platform']['messenger']['enabled'] = false; + + $this->extension->load($config, $containerBuilder); + } + public function testDisableDoctrine() { $containerBuilderProphecy = $this->getBaseContainerBuilderProphecy(); @@ -837,7 +850,6 @@ private function getPartialContainerBuilderProphecy() $containerBuilderProphecy->getParameter('kernel.debug')->willReturn(false); $containerBuilderProphecy->getDefinition('api_platform.http_cache.purger.varnish')->willReturn(new Definition()); - $containerBuilderProphecy->has('message_bus')->willReturn(true); return $containerBuilderProphecy; } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index cbe9afe8750..6c8c8d0d2ff 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -157,6 +157,9 @@ public function testDefaultConfig() 'doctrine_mongodb_odm' => [ 'enabled' => true, ], + 'messenger' => [ + 'enabled' => true, + ], 'mercure' => [ 'enabled' => true, 'hub_url' => null, From 6f26004c8cd08834d3dd4304038fdc12dd90df37 Mon Sep 17 00:00:00 2001 From: meyerbaptiste Date: Mon, 11 Feb 2019 15:33:50 +0100 Subject: [PATCH 07/13] Fix CS --- src/Api/FormatsProvider.php | 2 +- src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php | 2 +- src/Bridge/Doctrine/MongoDbOdm/Paginator.php | 2 +- src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php | 2 +- src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php | 4 ++-- src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php | 2 +- src/Bridge/Elasticsearch/DataProvider/Paginator.php | 2 +- src/Cache/CachedTrait.php | 2 +- src/DataProvider/Pagination.php | 2 +- src/GraphQl/Type/SchemaBuilder.php | 2 +- src/Hal/Serializer/ItemNormalizer.php | 2 +- src/JsonApi/Serializer/ItemNormalizer.php | 2 +- src/Metadata/Extractor/AbstractExtractor.php | 2 +- src/Serializer/AbstractItemNormalizer.php | 4 ++-- src/Serializer/Filter/GroupFilter.php | 2 +- src/Serializer/Filter/PropertyFilter.php | 2 +- .../Bundle/DependencyInjection/ApiPlatformExtensionTest.php | 4 ++-- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Api/FormatsProvider.php b/src/Api/FormatsProvider.php index a5d0b77f634..9efdde324e3 100644 --- a/src/Api/FormatsProvider.php +++ b/src/Api/FormatsProvider.php @@ -92,7 +92,7 @@ private function getOperationFormats(array $annotationFormats): array if (!\is_string($value)) { throw new InvalidArgumentException(sprintf("The 'formats' attributes value must be a string when trying to include an already configured format, %s given.", \gettype($value))); } - if (array_key_exists($value, $this->configuredFormats)) { + if (\array_key_exists($value, $this->configuredFormats)) { $resourceFormats[$value] = $this->configuredFormats[$value]; continue; } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php index beb438c902c..a5fe637fdc7 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/AbstractFilter.php @@ -85,6 +85,6 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b return !$this->isPropertyNested($property, $resourceClass); } - return array_key_exists($property, $this->properties); + return \array_key_exists($property, $this->properties); } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Paginator.php b/src/Bridge/Doctrine/MongoDbOdm/Paginator.php index 27ec4727e75..850d82e34e5 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Paginator.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Paginator.php @@ -136,7 +136,7 @@ public function count(): int private function getFacetInfo(string $field): array { foreach ($this->pipeline as $indexStage => $infoStage) { - if (array_key_exists('$facet', $infoStage)) { + if (\array_key_exists('$facet', $infoStage)) { if (!isset($this->pipeline[$indexStage]['$facet'][$field])) { throw new InvalidArgumentException("\"$field\" facet was not applied to the aggregation pipeline."); } diff --git a/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php index 89524fca1f9..06300bc2566 100644 --- a/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -252,7 +252,7 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a } // If it's an embedded property see below - if (!array_key_exists($property, $targetClassMetadata->embeddedClasses)) { + if (!\array_key_exists($property, $targetClassMetadata->embeddedClasses)) { //the field test allows to add methods to a Resource which do not reflect real database fields if ($targetClassMetadata->hasField($property) && (true === $propertyMetadata->getAttribute('fetchable') || $propertyMetadata->isReadable())) { $select[] = $property; diff --git a/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php b/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php index 3b5dc06378f..2574c21d414 100644 --- a/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php @@ -81,7 +81,7 @@ public function __construct(ManagerRegistry $managerRegistry, /* ResourceMetadat ]; foreach ($legacyPaginationArgs as $pos => $arg) { - if (array_key_exists($pos, $args)) { + if (\array_key_exists($pos, $args)) { @trigger_error(sprintf('Passing "$%s" arguments is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "%s" as third argument instead.', implode('", "$', array_column($legacyPaginationArgs, 'arg_name')), Paginator::class), E_USER_DEPRECATED); if (!((null === $arg['default'] && null === $args[$pos]) || \call_user_func("is_{$arg['type']}", $args[$pos]))) { @@ -261,7 +261,7 @@ private function isPaginationEnabled(Request $request, ResourceMetadata $resourc private function getPaginationParameter(Request $request, string $parameterName, $default = null) { if (null !== $paginationAttribute = $request->attributes->get('_api_pagination')) { - return array_key_exists($parameterName, $paginationAttribute) ? $paginationAttribute[$parameterName] : $default; + return \array_key_exists($parameterName, $paginationAttribute) ? $paginationAttribute[$parameterName] : $default; } return $request->query->get($parameterName, $default); diff --git a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php index 60cdb23e169..33d3bda40ca 100644 --- a/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/AbstractFilter.php @@ -115,7 +115,7 @@ protected function isPropertyEnabled(string $property/*, string $resourceClass*/ return !$this->isPropertyNested($property, $resourceClass); } - return array_key_exists($property, $this->properties); + return \array_key_exists($property, $this->properties); } /** diff --git a/src/Bridge/Elasticsearch/DataProvider/Paginator.php b/src/Bridge/Elasticsearch/DataProvider/Paginator.php index a3e702ff019..84ba1de6793 100644 --- a/src/Bridge/Elasticsearch/DataProvider/Paginator.php +++ b/src/Bridge/Elasticsearch/DataProvider/Paginator.php @@ -99,7 +99,7 @@ public function getIterator(): \Traversable foreach ($this->documents['hits']['hits'] ?? [] as $document) { $cacheKey = isset($document['_index'], $document['_type'], $document['_id']) ? md5("${document['_index']}_${document['_type']}_${document['_id']}") : null; - if ($cacheKey && array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) { + if ($cacheKey && \array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) { $object = $this->cachedDenormalizedDocuments[$cacheKey]; } else { $object = $this->denormalizer->denormalize( diff --git a/src/Cache/CachedTrait.php b/src/Cache/CachedTrait.php index 41d9c0dc1f1..a5a5b3adf82 100644 --- a/src/Cache/CachedTrait.php +++ b/src/Cache/CachedTrait.php @@ -27,7 +27,7 @@ trait CachedTrait private function getCached(string $cacheKey, callable $getValue) { - if (array_key_exists($cacheKey, $this->localCache)) { + if (\array_key_exists($cacheKey, $this->localCache)) { return $this->localCache[$cacheKey]; } diff --git a/src/DataProvider/Pagination.php b/src/DataProvider/Pagination.php index 20b80a7a82e..8de3257c9f7 100644 --- a/src/DataProvider/Pagination.php +++ b/src/DataProvider/Pagination.php @@ -185,6 +185,6 @@ private function getParameterFromContext(array $context, string $parameterName, { $filters = $context['filters'] ?? []; - return array_key_exists($parameterName, $filters) ? $filters[$parameterName] : $default; + return \array_key_exists($parameterName, $filters) ? $filters[$parameterName] : $default; } } diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 27974bfb0b5..d92cbdeb41c 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -246,7 +246,7 @@ private function getResourceFieldConfiguration(string $resourceClass, ResourceMe } parse_str($key, $parsed); - if (array_key_exists($key, $parsed) && \is_array($parsed[$key])) { + if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) { $parsed = [$key => '']; } array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) { diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index fdb66dbf030..942d5f84d5c 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -164,7 +164,7 @@ private function populateRelation(array $data, $object, string $format = null, a { $class = $this->getObjectClass($object); - $attributesMetadata = array_key_exists($class, $this->attributesMetadataCache) ? + $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ? $this->attributesMetadataCache[$class] : $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null; diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 34235e912a2..8dbd25bcadb 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -157,7 +157,7 @@ protected function getAttributes($object, $format = null, array $context) */ protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) { - parent::setAttributeValue($object, $attribute, \is_array($value) && array_key_exists('data', $value) ? $value['data'] : $value, $format, $context); + parent::setAttributeValue($object, $attribute, \is_array($value) && \array_key_exists('data', $value) ? $value['data'] : $value, $format, $context); } /** diff --git a/src/Metadata/Extractor/AbstractExtractor.php b/src/Metadata/Extractor/AbstractExtractor.php index 335e1a6d298..8bc3bfd6df6 100644 --- a/src/Metadata/Extractor/AbstractExtractor.php +++ b/src/Metadata/Extractor/AbstractExtractor.php @@ -103,7 +103,7 @@ protected function resolve($value) throw new \RuntimeException(sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter)); } - if (array_key_exists($parameter, $this->collectedParameters)) { + if (\array_key_exists($parameter, $this->collectedParameters)) { return $this->collectedParameters[$parameter]; } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 950fe22866a..9f5c62b46ff 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -175,14 +175,14 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true)); $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); if ($constructorParameter->isVariadic()) { - if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { + if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { if (!\is_array($data[$paramName])) { throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); } $params = array_merge($params, $data[$paramName]); } - } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { + } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format); // Don't run set for a parameter passed to the constructor diff --git a/src/Serializer/Filter/GroupFilter.php b/src/Serializer/Filter/GroupFilter.php index 77b7c817543..5d504a6c17b 100644 --- a/src/Serializer/Filter/GroupFilter.php +++ b/src/Serializer/Filter/GroupFilter.php @@ -39,7 +39,7 @@ public function __construct(string $parameterName = 'groups', bool $overrideDefa */ public function apply(Request $request, bool $normalization, array $attributes, array &$context) { - if (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { + if (\array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { $groups = $commonAttribute[$this->parameterName]; } else { $groups = $request->query->get($this->parameterName); diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php index 68811bd9c84..c60464e1c62 100644 --- a/src/Serializer/Filter/PropertyFilter.php +++ b/src/Serializer/Filter/PropertyFilter.php @@ -41,7 +41,7 @@ public function apply(Request $request, bool $normalization, array $attributes, { if (null !== $propertyAttribute = $request->attributes->get('_api_filter_property')) { $properties = $propertyAttribute; - } elseif (array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { + } elseif (\array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { $properties = $commonAttribute[$this->parameterName]; } else { $properties = $request->query->get($this->parameterName); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 7d4999ef268..0dc3c51934f 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -163,7 +163,7 @@ public function testNotPrependSerializerWhenConfigExist() $containerBuilderProphecy->getExtensionConfig('framework')->willReturn([0 => ['serializer' => ['enabled' => false]]])->shouldBeCalled(); $containerBuilderProphecy->prependExtensionConfig('framework', Argument::any())->willReturn(null)->shouldBeCalled(); $containerBuilderProphecy->prependExtensionConfig('framework', Argument::that(function (array $config) { - return array_key_exists('serializer', $config); + return \array_key_exists('serializer', $config); }))->shouldNotBeCalled(); $containerBuilder = $containerBuilderProphecy->reveal(); @@ -176,7 +176,7 @@ public function testNotPrependPropertyInfoWhenConfigExist() $containerBuilderProphecy->getExtensionConfig('framework')->willReturn([0 => ['property_info' => ['enabled' => false]]])->shouldBeCalled(); $containerBuilderProphecy->prependExtensionConfig('framework', Argument::any())->willReturn(null)->shouldBeCalled(); $containerBuilderProphecy->prependExtensionConfig('framework', Argument::that(function (array $config) { - return array_key_exists('property_info', $config); + return \array_key_exists('property_info', $config); }))->shouldNotBeCalled(); $containerBuilder = $containerBuilderProphecy->reveal(); From ae7aa9741b0f9c255518ef7fdba63fa453191a6a Mon Sep 17 00:00:00 2001 From: Amrouche Hamza Date: Wed, 31 Oct 2018 15:52:34 +0100 Subject: [PATCH 08/13] bugfix: throw InvalidArgument on matching something that looks like an url --- features/main/crud.feature | 5 +++-- src/Bridge/Symfony/Routing/IriConverter.php | 3 +++ tests/Bridge/Symfony/Routing/IriConverterTest.php | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/features/main/crud.feature b/features/main/crud.feature index c24e480e1da..61b6a78a1b7 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -476,6 +476,7 @@ Feature: Create-Retrieve-Update-Delete { "@id": "/dummies/1", "name": "A nice dummy", + "dummyDate": "2018-12-01 13:12", "jsonData": [{ "key": "value1" }, @@ -498,7 +499,7 @@ Feature: Create-Retrieve-Update-Delete "description": null, "dummy": null, "dummyBoolean": null, - "dummyDate": "2015-03-01T10:00:00+00:00", + "dummyDate": "2018-12-01T13:12:00+00:00", "dummyFloat": null, "dummyPrice": null, "relatedDummy": null, @@ -538,7 +539,7 @@ Feature: Create-Retrieve-Update-Delete "description": null, "dummy": null, "dummyBoolean": null, - "dummyDate": "2015-03-01T10:00:00+00:00", + "dummyDate": "2018-12-01T13:12:00+00:00", "dummyFloat": null, "dummyPrice": null, "relatedDummy": null, diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index 7c590b754d7..dc5281caee6 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -30,6 +30,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Util\AttributesExtractor; use ApiPlatform\Core\Util\ClassInfoTrait; +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; @@ -73,6 +74,8 @@ public function getItemFromIri(string $iri, array $context = []) $parameters = $this->router->match($iri); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e); + } catch (RequestExceptionInterface $e) { + throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e); } if (!isset($parameters['_api_resource_class'])) { diff --git a/tests/Bridge/Symfony/Routing/IriConverterTest.php b/tests/Bridge/Symfony/Routing/IriConverterTest.php index f428e266fd0..742f4e55492 100644 --- a/tests/Bridge/Symfony/Routing/IriConverterTest.php +++ b/tests/Bridge/Symfony/Routing/IriConverterTest.php @@ -31,6 +31,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\RouterInterface; @@ -98,6 +99,20 @@ public function testGetItemFromIriItemNotFoundException() $converter->getItemFromIri('/users/3'); } + public function testGetItemFromIriWithDateLooksLikeUrl() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No route matches "28-01-2018 10:10".'); + + $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); + + $routerProphecy = $this->prophesize(RouterInterface::class); + $routerProphecy->match('28-01-2018 10:10')->willThrow(new SuspiciousOperationException())->shouldBeCalledTimes(1); + + $converter = $this->getIriConverter($routerProphecy, null, $itemDataProviderProphecy); + $converter->getItemFromIri('28-01-2018 10:10'); + } + public function testGetItemFromIri() { $item = new \stdClass(); From 359c329f51d6e301fd3c3d468724e71753f7c682 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 12 Feb 2019 17:21:26 +0100 Subject: [PATCH 09/13] Alternative fix to #2298 --- src/Bridge/Symfony/Routing/IriConverter.php | 3 --- src/Bridge/Symfony/Routing/Router.php | 9 ++++++++- tests/Bridge/Symfony/Routing/IriConverterTest.php | 15 --------------- tests/Bridge/Symfony/Routing/RouterTest.php | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index dc5281caee6..7c590b754d7 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -30,7 +30,6 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Util\AttributesExtractor; use ApiPlatform\Core\Util\ClassInfoTrait; -use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; @@ -74,8 +73,6 @@ public function getItemFromIri(string $iri, array $context = []) $parameters = $this->router->match($iri); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e); - } catch (RequestExceptionInterface $e) { - throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e); } if (!isset($parameters['_api_resource_class'])) { diff --git a/src/Bridge/Symfony/Routing/Router.php b/src/Bridge/Symfony/Routing/Router.php index 4ebdb5ca190..5524cd90d4b 100644 --- a/src/Bridge/Symfony/Routing/Router.php +++ b/src/Bridge/Symfony/Routing/Router.php @@ -14,7 +14,9 @@ namespace ApiPlatform\Core\Bridge\Symfony\Routing; use ApiPlatform\Core\Api\UrlGeneratorInterface; +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouterInterface; @@ -72,7 +74,12 @@ public function match($pathInfo) $pathInfo = str_replace($baseContext->getBaseUrl(), '', $pathInfo); $request = Request::create($pathInfo, 'GET', [], [], [], ['HTTP_HOST' => $baseContext->getHost()]); - $context = (new RequestContext())->fromRequest($request); + try { + $context = (new RequestContext())->fromRequest($request); + } catch (RequestExceptionInterface $e) { + throw new RouteNotFoundException('Invalid request context.'); + } + $context->setPathInfo($pathInfo); $context->setScheme($baseContext->getScheme()); $context->setHost($baseContext->getHost()); diff --git a/tests/Bridge/Symfony/Routing/IriConverterTest.php b/tests/Bridge/Symfony/Routing/IriConverterTest.php index 742f4e55492..f428e266fd0 100644 --- a/tests/Bridge/Symfony/Routing/IriConverterTest.php +++ b/tests/Bridge/Symfony/Routing/IriConverterTest.php @@ -31,7 +31,6 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; -use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\RouterInterface; @@ -99,20 +98,6 @@ public function testGetItemFromIriItemNotFoundException() $converter->getItemFromIri('/users/3'); } - public function testGetItemFromIriWithDateLooksLikeUrl() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No route matches "28-01-2018 10:10".'); - - $itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class); - - $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->match('28-01-2018 10:10')->willThrow(new SuspiciousOperationException())->shouldBeCalledTimes(1); - - $converter = $this->getIriConverter($routerProphecy, null, $itemDataProviderProphecy); - $converter->getItemFromIri('28-01-2018 10:10'); - } - public function testGetItemFromIri() { $item = new \stdClass(); diff --git a/tests/Bridge/Symfony/Routing/RouterTest.php b/tests/Bridge/Symfony/Routing/RouterTest.php index 58eb06695e7..50373145985 100644 --- a/tests/Bridge/Symfony/Routing/RouterTest.php +++ b/tests/Bridge/Symfony/Routing/RouterTest.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Bridge\Symfony\Routing\Router; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouterInterface; @@ -72,4 +73,17 @@ public function testMatch() $this->assertEquals(['bar'], $router->match('/app_dev.php/foo')); } + + public function testWithinvalidContext() + { + $this->expectException(RoutingExceptionInterface::class); + $this->expectExceptionMessage('Invalid request context.'); + $context = new RequestContext('/app_dev.php', 'GET', 'localhost', 'https'); + + $mockedRouter = $this->prophesize('Symfony\Component\Routing\RouterInterface'); + $mockedRouter->getContext()->willReturn($context)->shouldBeCalled(); + + $router = new Router($mockedRouter->reveal()); + $router->match('28-01-2018 10:10'); + } } From 9d546879bbd8d161600cf2195a37292ab0183fa1 Mon Sep 17 00:00:00 2001 From: Anto Date: Tue, 12 Feb 2019 18:14:20 +0100 Subject: [PATCH 10/13] Add tests on the debug bar/profiler view (#2314) * Add tests on the debug bar/profiler view update tests to support lowest dependencies Avoid deprecation errors when booting kernel with NelmioApiDocBundle (try to fix appveyor) * Adapt tests for MongoDB --- .travis.yml | 2 +- appveyor.yml | 1 + composer.json | 4 +- .../views/DataCollector/request.html.twig | 87 +++--- .../Twig/ApiPlatformProfilerPanelTest.php | 252 ++++++++++++++++++ tests/Fixtures/app/AppKernel.php | 2 + tests/Fixtures/app/config/config_common.yml | 7 + tests/Fixtures/app/config/config_mongodb.yml | 56 ---- .../app/config/config_services_mongodb.yml | 58 ++++ .../app/config/config_test_mongodb.yml | 2 +- tests/Fixtures/app/config/routing_test.yml | 8 + .../app/config/routing_test_mongodb.yml | 8 + 12 files changed, 372 insertions(+), 115 deletions(-) create mode 100644 tests/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php create mode 100644 tests/Fixtures/app/config/config_services_mongodb.yml diff --git a/.travis.yml b/.travis.yml index 7e9e1810528..33df8456376 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ jobs: env: NO_UNIT_TESTS=true before_install: - composer remove --dev ext-mongodb doctrine/mongodb-odm doctrine/mongodb-odm-bundle - - sed -i '26,32d' tests/Fixtures/app/config/config_common.yml + - sed -i '33,39d' tests/Fixtures/app/config/config_common.yml - php: '7.2' - php: '7.3' - php: '7.3' diff --git a/appveyor.yml b/appveyor.yml index 75e54e48e83..8b3a9cdbde2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,4 +34,5 @@ services: test_script: - cd %APPVEYOR_BUILD_FOLDER% - php vendor\behat\behat\bin\behat --format=progress --suite=default + - rmdir tests\Fixtures\app\var\cache /s /q - php vendor\phpunit\phpunit\phpunit diff --git a/composer.json b/composer.json index 0fd47435faa..011e73f0c74 100644 --- a/composer.json +++ b/composer.json @@ -56,9 +56,11 @@ "symfony/cache": "^3.4 || ^4.0", "symfony/config": "^3.4 || ^4.0", "symfony/console": "^3.4 || ^4.0", + "symfony/css-selector": "^3.4 || ^4.0", "symfony/debug": "^3.4 || ^4.0", "symfony/dependency-injection": "^3.4 || ^4.0", "symfony/doctrine-bridge": "^3.4 || ^4.0", + "symfony/dom-crawler": "^3.4 || ^4.0", "symfony/event-dispatcher": "^3.4 || ^4.0", "symfony/expression-language": "^3.4 || ^4.0", "symfony/finder": "^3.4 || ^4.0", @@ -72,7 +74,7 @@ "symfony/security-bundle": "^3.4 || ^4.0", "symfony/twig-bundle": "^3.4 || ^4.0", "symfony/validator": "^3.4 || ^4.0", - "symfony/web-profiler-bundle": "^3.4 || ^4.0", + "symfony/web-profiler-bundle": "^4.2", "symfony/yaml": "^3.4 || ^4.0", "webonyx/graphql-php": ">=0.13 <1.0" }, diff --git a/src/Bridge/Symfony/Bundle/Resources/views/DataCollector/request.html.twig b/src/Bridge/Symfony/Bundle/Resources/views/DataCollector/request.html.twig index d26493a0c11..e5983280b4e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/views/DataCollector/request.html.twig +++ b/src/Bridge/Symfony/Bundle/Resources/views/DataCollector/request.html.twig @@ -7,6 +7,30 @@ {% endmacro %} +{% macro operationTable(object, name, actualOperationName) %} + {% import _self as apiPlatform %} + + + + + + + + + + {% for key, itemOperation in object %} + {{ apiPlatform.operationLine(key, itemOperation, actualOperationName) }} + {% else %} + + + + {% endfor %} + +
{{- name|capitalize }} operationsAttributes
+ No available {{ name|lower }} operation for this resource. +
+{% endmacro %} + {% macro providerTable(object, name) %} {% if object.responses is empty %}
@@ -99,56 +123,13 @@

Resource Metadata

Short name: "{{ collector.resourceMetadata.shortName }}"

+ {{ apiPlatform.operationTable(collector.resourceMetadata.itemOperations, 'item', collector.requestAttributes.item_operation_name|default('')) }} + {{ apiPlatform.operationTable(collector.resourceMetadata.collectionOperations, 'collection', collector.requestAttributes.collection_operation_name|default('')) }} - - - - - - - {% for key, itemOperation in collector.resourceMetadata.itemOperations %} - {{ apiPlatform.operationLine(key, itemOperation, collector.requestAttributes.item_operation_name|default('')) }} - {% endfor %} - -
- Item operations - - Attributes -
- - - - - - - - - - - {% for key, collectionOperation in collector.resourceMetadata.collectionOperations %} - {{ apiPlatform.operationLine(key, collectionOperation, collector.requestAttributes.collection_operation_name|default('')) }} - {% else %} - - - - {% endfor %} - -
- Collection operations - - Attributes -
- No available collection operation for this resource. -
- - - - - + + @@ -177,9 +158,7 @@
- Filters - Filters
- + @@ -187,12 +166,8 @@ {% for key, value in collector.resourceMetadata.attributes if key != 'filters' %} - - + + {% endfor %} diff --git a/tests/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php new file mode 100644 index 00000000000..a4fdf572844 --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php @@ -0,0 +1,252 @@ + + * + * 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\Tests\Bridge\Symfony\Bundle\Twig; + +use ApiPlatform\Core\Bridge\Doctrine\Common\DataPersister; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\CollectionDataProvider as OdmCollectionDataProvider; +use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\ItemDataProvider as OdmItemDataProvider; +use ApiPlatform\Core\Bridge\Doctrine\Orm\CollectionDataProvider; +use ApiPlatform\Core\Bridge\Doctrine\Orm\ItemDataProvider; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\ContainNonResourceItemDataProvider; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * @author Anthony GRASSIOT + */ +class ApiPlatformProfilerPanelTest extends WebTestCase +{ + /** + * @var EntityManagerInterface + */ + private $manager; + private $schemaTool; + private $env; + + public function setUp() + { + parent::setUp(); + $kernel = self::bootKernel(); + $this->env = $kernel->getEnvironment(); + + /** @var ManagerRegistry $doctrine */ + $doctrine = $kernel->getContainer()->get('doctrine'); + /** @var EntityManagerInterface $manager */ + $manager = $doctrine->getManager(); + $this->manager = $manager; + $this->schemaTool = new SchemaTool($this->manager); + $classes = $this->manager->getMetadataFactory()->getAllMetadata(); + $this->schemaTool->dropSchema($classes); + $this->manager->clear(); + $this->schemaTool->createSchema($classes); + } + + public function tearDown() + { + $this->schemaTool->dropSchema($this->manager->getMetadataFactory()->getAllMetadata()); + $this->manager->clear(); + parent::tearDown(); + } + + public function testDebugBarContentNotResourceClass() + { + $client = static::createClient(); + $client->enableProfiler(); + // Using html to get default Swagger UI + $client->request('GET', '/'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + /** @var string $token */ + $token = $client->getResponse()->headers->get('X-Debug-Token'); + $crawler = $client->request('GET', "/_wdt/$token"); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $block = $crawler->filter('div[class*=sf-toolbar-block-api_platform]'); + + // Check extra info content + $this->assertContains('sf-toolbar-status-default', $block->attr('class'), 'The toolbar block should have the default color.'); + $this->assertSame('Not an API Platform resource', $block->filter('.sf-toolbar-info-piece span')->html()); + } + + public function testDebugBarContent() + { + $client = static::createClient(); + $client->enableProfiler(); + $client->request('GET', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + /** @var string $token */ + $token = $client->getResponse()->headers->get('X-Debug-Token'); + + $crawler = $client->request('GET', "/_wdt/$token"); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $block = $crawler->filter('div[class*=sf-toolbar-block-api_platform]'); + + // Check extra info content + $this->assertContains('sf-toolbar-status-default', $block->attr('class'), 'The toolbar block should have the default color.'); + $this->assertSame('test_mongodb' === $this->env ? DocumentDummy::class : Dummy::class, $block->filter('.sf-toolbar-info-piece span')->html()); + } + + public function testProfilerGeneralLayoutNotResourceClass() + { + $client = static::createClient(); + $client->enableProfiler(); + // Using html to get default Swagger UI + $client->request('GET', '/'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $crawler = $client->request('GET', '/_profiler/latest?panel=api_platform.data_collector.request', [], [], []); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // Check that the Api-Platform sidebar link is active + $this->assertNotEmpty($menuLink = $crawler->filter('a[href$="panel=api_platform.data_collector.request"]')); + $this->assertNotEmpty($menuLink->filter('.disabled'), 'The sidebar menu should be disabled.'); + + $metrics = $crawler->filter('.metrics'); + $this->assertCount(1, $metrics->filter('.metric'), 'The should be one metric displayed (resource class).'); + $this->assertSame('Not an API Platform resource', $metrics->filter('span.value')->html()); + + $this->assertEmpty($crawler->filter('.sf-tabs .tab'), 'Tabs must not be presents on the panel.'); + } + + public function testProfilerGeneralLayout() + { + $client = static::createClient(); + $client->enableProfiler(); + $client->request('GET', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $crawler = $client->request('GET', '/_profiler/latest?panel=api_platform.data_collector.request', [], [], []); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // Check that the Api-Platform sidebar link is active + $this->assertNotEmpty($menuLink = $crawler->filter('a[href$="panel=api_platform.data_collector.request"]')); + $this->assertEmpty($menuLink->filter('.disabled'), 'The sidebar menu should not be disabled.'); + + $metrics = $crawler->filter('.metrics'); + $this->assertCount(1, $metrics->filter('.metric'), 'The should be one metric displayed (resource class).'); + $this->assertSame('test_mongodb' === $this->env ? DocumentDummy::class : Dummy::class, $metrics->filter('span.value')->html()); + + $this->assertCount(3, $crawler->filter('.sf-tabs .tab'), 'Tabs must be presents on the panel.'); + + // Metadata tab + $this->assertSame('Resource Metadata', $crawler->filter('.tab:nth-of-type(1) .tab-title')->html()); + $tabContent = $crawler->filter('.tab:nth-of-type(1) .tab-content'); + $this->assertStringEndsWith('"Dummy"', $tabContent->filter('h3')->html(), 'the resource shortname should be displayed.'); + + $this->assertCount(4, $tabContent->filter('table')); + $this->assertSame('Item operations', $tabContent->filter('table:first-of-type thead th:first-of-type')->html()); + $this->assertSame('Collection operations', $tabContent->filter('table:nth-of-type(2) thead th:first-of-type')->html()); + $this->assertSame('Filters', $tabContent->filter('table:nth-of-type(3) thead th:first-of-type')->html()); + $this->assertSame('Attributes', $tabContent->filter('table:last-of-type thead th:first-of-type')->html()); + + // Data providers tab + $this->assertSame('Data Providers', $crawler->filter('.tab:nth-of-type(2) .tab-title')->html()); + $this->assertNotEmpty($crawler->filter('.tab:nth-of-type(2) .tab-content')); + + // Data persisters tab + $this->assertSame('Data Persisters', $crawler->filter('.tab:last-child .tab-title')->html()); + $this->assertNotEmpty($crawler->filter('.tab:nth-of-type(3) .tab-content')); + } + + public function testGetCollectionProfiler() + { + $client = static::createClient(); + $client->enableProfiler(); + $client->request('GET', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $crawler = $client->request('GET', '/_profiler/latest?panel=api_platform.data_collector.request'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // Metadata tab + $tabContent = $crawler->filter('.tab:nth-of-type(1) .tab-content'); + $this->assertSame('get', $tabContent->filter('table:nth-of-type(2) th.status-success')->html(), 'The actual operation should be highlighted.'); + $this->assertEmpty($tabContent->filter('table:not(:nth-of-type(2)) .status-success'), 'Only the actual operation should be highlighted.'); + + // Data provider tab + $tabContent = $crawler->filter('.tab:nth-of-type(2) .tab-content'); + $this->assertSame('TRUE', $tabContent->filter('table tbody .status-success')->html()); + $this->assertContains('test_mongodb' === $this->env ? OdmCollectionDataProvider::class : CollectionDataProvider::class, $tabContent->filter('table tbody')->html()); + + $this->assertContains('No calls to item data provider have been recorded.', $tabContent->html()); + $this->assertContains('No calls to subresource data provider have been recorded.', $tabContent->html()); + + // Data persiters tab + $this->assertContains('No calls to data persister have been recorded.', $crawler->filter('.tab:nth-of-type(3) .tab-content .empty')->html()); + } + + public function testPostCollectionProfiler() + { + $client = static::createClient(); + $client->enableProfiler(); + $client->request('POST', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json', 'CONTENT_TYPE' => 'application/ld+json'], '{"name": "foo"}'); + $this->assertEquals(201, $client->getResponse()->getStatusCode()); + $crawler = $client->request('get', '/_profiler/latest?panel=api_platform.data_collector.request'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // Metadata tab + $tabContent = $crawler->filter('.tab:nth-of-type(1) .tab-content'); + $this->assertSame('post', $tabContent->filter('table:nth-of-type(2) th.status-success')->html(), 'The actual operation should be highlighted.'); + $this->assertEmpty($tabContent->filter('table:not(:nth-of-type(2)) .status-success'), 'Only the actual operation should be highlighted.'); + + // Data provider tab + $tabContent = $crawler->filter('.tab:nth-of-type(2) .tab-content'); + $this->assertContains('No calls to collection data provider have been recorded.', $tabContent->html()); + $this->assertContains('No calls to item data provider have been recorded.', $tabContent->html()); + $this->assertContains('No calls to subresource data provider have been recorded.', $tabContent->html()); + + // Data persiters tab + $tabContent = $crawler->filter('.tab:nth-of-type(3) .tab-content'); + $this->assertSame('TRUE', $tabContent->filter('table tbody .status-success')->html()); + $this->assertContains(DataPersister::class, $tabContent->filter('table tbody')->html()); + } + + /** + * @group legacy + * Group legacy is due ApiPlatform\Core\Exception\ResourceClassNotSupportedException, the annotation could be removed in 3.0 but the test should stay + */ + public function testGetItemProfiler() + { + $dummy = new Dummy(); + $dummy->setName('bar'); + $this->manager->persist($dummy); + $this->manager->flush(); + + $client = static::createClient(); + $client->enableProfiler(); + $client->request('GET', '/dummies/1', [], [], ['HTTP_ACCEPT' => 'application/ld+json', 'CONTENT_TYPE' => 'application/ld+json'], '{"name": "foo"}'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $crawler = $client->request('get', '/_profiler/latest?panel=api_platform.data_collector.request'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // Metadata tab + $tabContent = $crawler->filter('.tab:nth-of-type(1) .tab-content'); + $this->assertSame('get', $tabContent->filter('table:nth-of-type(1) th.status-success')->html(), 'The actual operation should be highlighted.'); + $this->assertEmpty($tabContent->filter('table:not(:nth-of-type(1)) .status-success'), 'Only the actual operation should be highlighted.'); + + // Data provider tab + $tabContent = $crawler->filter('.tab:nth-of-type(2) .tab-content'); + $this->assertSame('FALSE', $tabContent->filter('table tbody tr:first-of-type .status-error')->html()); + $this->assertSame(ContainNonResourceItemDataProvider::class, $tabContent->filter('table tbody tr:first-of-type td:nth-of-type(3)')->html()); + + $this->assertSame('TRUE', $tabContent->filter('table tbody .status-success')->html()); + $this->assertContains('test_mongodb' === $this->env ? OdmItemDataProvider::class : ItemDataProvider::class, $tabContent->filter('table tbody')->html()); + + $this->assertContains('No calls to collection data provider have been recorded.', $tabContent->html()); + $this->assertContains('No calls to subresource data provider have been recorded.', $tabContent->html()); + + // Data persiters tab + $this->assertContains('No calls to data persister have been recorded.', $crawler->filter('.tab:nth-of-type(3) .tab-content .empty')->html()); + } +} diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 9ba7d1c4667..b4fbe7cd095 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -24,6 +24,7 @@ use Symfony\Bundle\MercureBundle\MercureBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; @@ -57,6 +58,7 @@ public function registerBundles(): array new ApiPlatformBundle(), new SecurityBundle(), new FOSUserBundle(), + new WebProfilerBundle(), ]; if (class_exists(DoctrineMongoDBBundle::class)) { diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 456baa29f73..836d8cbb48d 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -10,6 +10,13 @@ framework: form: ~ # For FOSUser templating: engines: ['twig'] # For Swagger UI + profiler: + enabled: true + collect: false + +web_profiler: + toolbar: true + intercept_redirects: false doctrine: dbal: diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index 19b68d05c7c..b98821a1d5c 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -8,59 +8,3 @@ doctrine_mongodb: server: '%server%' options: {} default_database: '%dbname%' - -services: - app.my_dummy_resource.mongodb.boolean_filter: - parent: 'api_platform.doctrine_mongodb.odm.boolean_filter' - arguments: [ { 'dummyBoolean': ~, 'embeddedDummy.dummyBoolean': ~, 'relatedDummy.embeddedDummy.dummyBoolean': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.boolean' } ] - app.my_dummy_resource.mongodb.date_filter: - parent: 'api_platform.doctrine_mongodb.odm.date_filter' - arguments: [ { 'dummyDate': ~, 'relatedDummy.dummyDate': ~, 'embeddedDummy.dummyDate': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.date' } ] - app.my_dummy_resource.mongodb.exists_filter: - parent: 'api_platform.doctrine_mongodb.odm.exists_filter' - arguments: [ { 'description': ~, 'relatedDummy.name': ~, 'dummyBoolean': ~, 'relatedDummy': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.exists' } ] - app.my_dummy_resource.mongodb.numeric_filter: - parent: 'api_platform.doctrine_mongodb.odm.numeric_filter' - arguments: [ { 'dummyFloat': ~, 'dummyPrice': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.numeric' } ] - app.my_dummy_resource.mongodb.order_filter: - parent: 'api_platform.doctrine_mongodb.odm.order_filter' - arguments: [ { 'id': ~, 'name': 'desc', 'description': ~, 'relatedDummy.name': ~, 'embeddedDummy.dummyName': 'desc', 'relatedDummy.symfony': ~, 'dummyDate': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.order' } ] - app.my_dummy_resource.mongodb.range_filter: - parent: 'api_platform.doctrine_mongodb.odm.range_filter' - arguments: [ { 'dummyFloat': ~, 'dummyPrice': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.range' } ] - app.my_dummy_resource.mongodb.search_filter: - parent: 'api_platform.doctrine_mongodb.odm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.search' } ] - app.related_dummy_resource.mongodb.search_filter: - parent: 'api_platform.doctrine_mongodb.odm.search_filter' - arguments: [ { 'relatedToDummyFriend.dummyFriend': 'exact', 'name': 'partial' } ] - tags: [ { name: 'api_platform.filter', id: 'related_dummy.mongodb.friends' } ] - app.my_dummy_date_resource.mongodb.date_filter: - parent: 'api_platform.doctrine_mongodb.odm.date_filter' - arguments: [ { 'dummyDate': ~ } ] - tags: [ { name: 'api_platform.filter', id: 'my_dummy_date.mongodb.date' } ] - app.related_dummy_to_friend_resource.mongodb.search_filter: - parent: 'api_platform.doctrine_mongodb.odm.search_filter' - arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ] - tags: [ { name: 'api_platform.filter', id: 'related_to_dummy_friend.mongodb.name' } ] - - dummy_dto_no_input.data_provider: - class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\DummyDtoNoInputCollectionDataProvider' - public: false - arguments: ['@doctrine_mongodb'] - tags: - - { name: 'api_platform.collection_data_provider' } - - app.dummy_dto_no_output_data_persister: - class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyDtoNoOutputDataPersister - arguments: ['@doctrine_mongodb'] - public: false - tags: - - { name: 'api_platform.data_persister' } diff --git a/tests/Fixtures/app/config/config_services_mongodb.yml b/tests/Fixtures/app/config/config_services_mongodb.yml new file mode 100644 index 00000000000..92da5389193 --- /dev/null +++ b/tests/Fixtures/app/config/config_services_mongodb.yml @@ -0,0 +1,58 @@ +imports: + - { resource: config_common.yml } + +services: + app.my_dummy_resource.mongodb.boolean_filter: + parent: 'api_platform.doctrine_mongodb.odm.boolean_filter' + arguments: [ { 'dummyBoolean': ~, 'embeddedDummy.dummyBoolean': ~, 'relatedDummy.embeddedDummy.dummyBoolean': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.boolean' } ] + app.my_dummy_resource.mongodb.date_filter: + parent: 'api_platform.doctrine_mongodb.odm.date_filter' + arguments: [ { 'dummyDate': ~, 'relatedDummy.dummyDate': ~, 'embeddedDummy.dummyDate': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.date' } ] + app.my_dummy_resource.mongodb.exists_filter: + parent: 'api_platform.doctrine_mongodb.odm.exists_filter' + arguments: [ { 'description': ~, 'relatedDummy.name': ~, 'dummyBoolean': ~, 'relatedDummy': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.exists' } ] + app.my_dummy_resource.mongodb.numeric_filter: + parent: 'api_platform.doctrine_mongodb.odm.numeric_filter' + arguments: [ { 'dummyFloat': ~, 'dummyPrice': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.numeric' } ] + app.my_dummy_resource.mongodb.order_filter: + parent: 'api_platform.doctrine_mongodb.odm.order_filter' + arguments: [ { 'id': ~, 'name': 'desc', 'description': ~, 'relatedDummy.name': ~, 'embeddedDummy.dummyName': 'desc', 'relatedDummy.symfony': ~, 'dummyDate': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.order' } ] + app.my_dummy_resource.mongodb.range_filter: + parent: 'api_platform.doctrine_mongodb.odm.range_filter' + arguments: [ { 'dummyFloat': ~, 'dummyPrice': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.range' } ] + app.my_dummy_resource.mongodb.search_filter: + parent: 'api_platform.doctrine_mongodb.odm.search_filter' + arguments: [ { 'id': 'exact', 'name': 'partial', 'alias': 'start', 'description': 'word_start', 'relatedDummy.name': 'exact', 'relatedDummies': 'exact', 'dummy': 'ipartial', 'relatedDummies.name': 'start', 'embeddedDummy.dummyName': 'partial', 'relatedDummy.thirdLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.level': 'exact', 'relatedDummy.thirdLevel.badFourthLevel.level': 'exact', 'relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level': 'exact' } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy.mongodb.search' } ] + app.related_dummy_resource.mongodb.search_filter: + parent: 'api_platform.doctrine_mongodb.odm.search_filter' + arguments: [ { 'relatedToDummyFriend.dummyFriend': 'exact', 'name': 'partial' } ] + tags: [ { name: 'api_platform.filter', id: 'related_dummy.mongodb.friends' } ] + app.my_dummy_date_resource.mongodb.date_filter: + parent: 'api_platform.doctrine_mongodb.odm.date_filter' + arguments: [ { 'dummyDate': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy_date.mongodb.date' } ] + app.related_dummy_to_friend_resource.mongodb.search_filter: + parent: 'api_platform.doctrine_mongodb.odm.search_filter' + arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ] + tags: [ { name: 'api_platform.filter', id: 'related_to_dummy_friend.mongodb.name' } ] + + dummy_dto_no_input.data_provider: + class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\DummyDtoNoInputCollectionDataProvider' + public: false + arguments: ['@doctrine_mongodb'] + tags: + - { name: 'api_platform.collection_data_provider' } + + app.dummy_dto_no_output_data_persister: + class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyDtoNoOutputDataPersister + arguments: ['@doctrine_mongodb'] + public: false + tags: + - { name: 'api_platform.data_persister' } diff --git a/tests/Fixtures/app/config/config_test_mongodb.yml b/tests/Fixtures/app/config/config_test_mongodb.yml index 092120be128..cb39ce23c31 100644 --- a/tests/Fixtures/app/config/config_test_mongodb.yml +++ b/tests/Fixtures/app/config/config_test_mongodb.yml @@ -1,5 +1,5 @@ imports: - - { resource: config_common.yml } + - { resource: config_services_mongodb.yml } api_platform: doctrine: false diff --git a/tests/Fixtures/app/config/routing_test.yml b/tests/Fixtures/app/config/routing_test.yml index 87afc0d4d25..8f1996d8a75 100644 --- a/tests/Fixtures/app/config/routing_test.yml +++ b/tests/Fixtures/app/config/routing_test.yml @@ -4,3 +4,11 @@ _main: controller: resource: '@TestBundle/Controller/Orm' type: annotation + +web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + +web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler diff --git a/tests/Fixtures/app/config/routing_test_mongodb.yml b/tests/Fixtures/app/config/routing_test_mongodb.yml index df901e6b3c4..85c7cccf021 100644 --- a/tests/Fixtures/app/config/routing_test_mongodb.yml +++ b/tests/Fixtures/app/config/routing_test_mongodb.yml @@ -4,3 +4,11 @@ _main: controller: resource: '@TestBundle/Controller/MongoDbOdm' type: annotation + +web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + +web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler From b0dfa2446563a6034b10f3fc337075c95e26da9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 12 Feb 2019 19:10:32 +0100 Subject: [PATCH 11/13] Fix Insight violations --- src/Api/IdentifiersExtractor.php | 4 +-- .../Common/Filter/RangeFilterTrait.php | 4 +-- .../MongoDbOdm/Filter/RangeFilter.php | 10 ++++---- .../Doctrine/MongoDbOdm/ItemDataProvider.php | 2 +- .../PropertyInfo/DoctrineExtractor.php | 25 ------------------- .../Doctrine/Orm/Filter/RangeFilter.php | 10 ++++---- 6 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index d307b36c0a7..cbb61f9e961 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -34,15 +34,13 @@ final class IdentifiersExtractor implements IdentifiersExtractorInterface private $propertyMetadataFactory; private $propertyAccessor; private $resourceClassResolver; - private $resourceMetadataFactory; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null) { $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); $this->resourceClassResolver = $resourceClassResolver; - $this->resourceMetadataFactory = $resourceMetadataFactory; if (null === $this->resourceClassResolver) { @trigger_error(sprintf('Not injecting %s in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.', ResourceClassResolverInterface::class), E_USER_DEPRECATED); diff --git a/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php index d66fd62a2f0..5ad0d5049c9 100644 --- a/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/RangeFilterTrait.php @@ -96,7 +96,7 @@ private function normalizeValues(array $values, string $property): ?array /** * Normalize the values array for between operator. */ - private function normalizeBetweenValues(array $values, string $property): ?array + private function normalizeBetweenValues(array $values): ?array { if (2 !== \count($values)) { $this->getLogger()->notice('Invalid filter ignored', [ @@ -120,7 +120,7 @@ private function normalizeBetweenValues(array $values, string $property): ?array /** * Normalize the value. */ - private function normalizeValue(string $value, string $property, string $operator): ?string + private function normalizeValue(string $value, string $operator): ?string { if (!is_numeric($value)) { $this->getLogger()->notice('Invalid filter ignored', [ diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/RangeFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/RangeFilter.php index 7b7048b89bb..99f423b6c9f 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/RangeFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/RangeFilter.php @@ -73,7 +73,7 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ case self::PARAMETER_BETWEEN: $rangeValue = explode('..', $value); - $rangeValue = $this->normalizeBetweenValues($rangeValue, $field); + $rangeValue = $this->normalizeBetweenValues($rangeValue); if (null === $rangeValue) { return; } @@ -82,7 +82,7 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ break; case self::PARAMETER_GREATER_THAN: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -91,7 +91,7 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ break; case self::PARAMETER_GREATER_THAN_OR_EQUAL: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -100,7 +100,7 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ break; case self::PARAMETER_LESS_THAN: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -109,7 +109,7 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ break; case self::PARAMETER_LESS_THAN_OR_EQUAL: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } diff --git a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php index e95ba18412b..9dcc53f35e1 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/MongoDbOdm/ItemDataProvider.php @@ -72,7 +72,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null $id = (array) $id; - if (!$fetchData = $context['fetch_data'] ?? true) { + if (!($context['fetch_data'] ?? true)) { return $manager->getReference($resourceClass, reset($id)); } diff --git a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php index 81a70263133..e9fdac022a3 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php +++ b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php @@ -111,31 +111,6 @@ public function getTypes($class, $property, array $context = []) } } - /** - * Determines whether an association is nullable. - * - * @see https://github.com/doctrine/doctrine2/blob/v2.5.4/lib/Doctrine/ORM/Tools/EntityGenerator.php#L1221-L1246 - */ - private function isAssociationNullable(array $associationMapping): bool - { - if (isset($associationMapping['id']) && $associationMapping['id']) { - return false; - } - - if (!isset($associationMapping['joinColumns'])) { - return true; - } - - $joinColumns = $associationMapping['joinColumns']; - foreach ($joinColumns as $joinColumn) { - if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) { - return false; - } - } - - return true; - } - /** * Gets the corresponding built-in PHP type. */ diff --git a/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php b/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php index b581b3c0ba8..58b3446a1c7 100644 --- a/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/RangeFilter.php @@ -80,7 +80,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf case self::PARAMETER_BETWEEN: $rangeValue = explode('..', $value); - $rangeValue = $this->normalizeBetweenValues($rangeValue, $field); + $rangeValue = $this->normalizeBetweenValues($rangeValue); if (null === $rangeValue) { return; } @@ -92,7 +92,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf break; case self::PARAMETER_GREATER_THAN: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -103,7 +103,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf break; case self::PARAMETER_GREATER_THAN_OR_EQUAL: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -114,7 +114,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf break; case self::PARAMETER_LESS_THAN: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } @@ -125,7 +125,7 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf break; case self::PARAMETER_LESS_THAN_OR_EQUAL: - $value = $this->normalizeValue($value, $field, $operator); + $value = $this->normalizeValue($value, $operator); if (null === $value) { return; } From 919a70523ed56fe946c630d58ab6e74992aa9886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 12 Feb 2019 19:14:24 +0100 Subject: [PATCH 12/13] Fix CS --- src/Api/IdentifiersExtractor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index cbb61f9e961..a36d3ac4154 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -16,7 +16,6 @@ use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\ClassInfoTrait; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; From 6c3fab2e180ef6f681d7ebb33b5bdd4d25840c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 14 Feb 2019 08:24:16 +0100 Subject: [PATCH 13/13] Add CVE-2019-1000011 to the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f92f68087d..e5470206c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2.10 -* /!\ Security: a vulnerability impacting the GraphQL subsystem was allowing users authorized to run mutations for a specific resource type, to execute it on any resource, of any type +* /!\ Security: a vulnerability impacting the GraphQL subsystem was allowing users authorized to run mutations for a specific resource type, to execute it on any resource, of any type (CVE-2019-1000011) ## 2.2.9
- Attributes - Attributes
- {{ key }} - - {{- profiler_dump(value, 2) -}} - {{ key }}{{- profiler_dump(value, 2) -}}