diff --git a/composer.json b/composer.json index c2cbb358973..8cf53aaf7f1 100644 --- a/composer.json +++ b/composer.json @@ -42,10 +42,12 @@ "phpdocumentor/reflection-docblock": "~3.0", "doctrine/orm": "~2.2,>=2.2.3", "doctrine/doctrine-bundle": "dev-property_info", - "php-mock/php-mock-phpunit": "~1.1" + "php-mock/php-mock-phpunit": "~1.1", + "nelmio/api-doc-bundle": "^2.11.2" }, "suggest": { - "friendsofsymfony/user-bundle": "To use the FOSUserBundle bridge." + "friendsofsymfony/user-bundle": "To use the FOSUserBundle bridge.", + "nelmio/api-doc-bundle": "To have the api sandbox & documentation." }, "autoload": { "psr-4": { "ApiPlatform\\Core\\": "src/" } diff --git a/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php b/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php new file mode 100644 index 00000000000..f8724f83697 --- /dev/null +++ b/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Bridge\NelmioApiDoc\Extractor\AnnotationsProvider; + +use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Bridge\NelmioApiDoc\Parser\ApiPlatformParser; +use ApiPlatform\Core\Bridge\Symfony\Routing\OperationMethodResolverInterface; +use ApiPlatform\Core\Hydra\ApiDocumentationBuilderInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Creates Nelmio ApiDoc annotations for the api platform. + * + * @author Kévin Dunglas + * @author Teoh Han Hui + */ +final class ApiPlatformProvider implements AnnotationsProviderInterface +{ + private $resourceNameCollectionFactory; + private $apiDocumentationBuilder; + private $resourceMetadataFactory; + private $filters; + private $operationMethodResolver; + + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ApiDocumentationBuilderInterface $apiDocumentationBuilder, ResourceMetadataFactoryInterface $resourceMetadataFactory, FilterCollection $filters, OperationMethodResolverInterface $operationMethodResolver) + { + $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; + $this->apiDocumentationBuilder = $apiDocumentationBuilder; + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->filters = $filters; + $this->operationMethodResolver = $operationMethodResolver; + } + + /** + * {@inheritdoc} + */ + public function getAnnotations() : array + { + $annotations = []; + $hydraDoc = $this->apiDocumentationBuilder->getApiDocumentation(); + $entrypointHydraDoc = $this->getResourceHydraDoc($hydraDoc, '#Entrypoint'); + + foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + + $prefixedShortName = ($iri = $resourceMetadata->getIri()) ? $iri : '#'.$resourceMetadata->getShortName(); + $resourceHydraDoc = $this->getResourceHydraDoc($hydraDoc, $prefixedShortName); + + if ($hydraDoc) { + foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) { + $annotations[] = $this->getApiDoc(true, $resourceClass, $resourceMetadata, $operationName, $resourceHydraDoc, $entrypointHydraDoc); + } + + foreach ($resourceMetadata->getItemOperations() as $operationName => $operation) { + $annotations[] = $this->getApiDoc(false, $resourceClass, $resourceMetadata, $operationName, $resourceHydraDoc); + } + } + } + + return $annotations; + } + + /** + * Builds ApiDoc annotation from ApiPlatform data. + * + * @param bool $collection + * @param string $resourceClass + * @param ResourceMetadata $resourceMetadata + * @param string $operationName + * @param array $resourceHydraDoc + * @param array $entrypointHydraDoc + * + * @return ApiDoc + */ + private function getApiDoc(bool $collection, string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $resourceHydraDoc, array $entrypointHydraDoc = []) : ApiDoc + { + if ($collection) { + $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); + $route = $this->operationMethodResolver->getCollectionOperationRoute($resourceClass, $operationName); + $operationHydraDoc = $this->getCollectionOperationHydraDoc($resourceMetadata->getShortName(), $method, $entrypointHydraDoc); + } else { + $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName); + $route = $this->operationMethodResolver->getItemOperationRoute($resourceClass, $operationName); + $operationHydraDoc = $this->getOperationHydraDoc($method, $resourceHydraDoc); + } + + $data = [ + 'resource' => $route->getPath(), + 'description' => $operationHydraDoc['hydra:title'], + 'resourceDescription' => $resourceHydraDoc['hydra:title'], + 'section' => $resourceHydraDoc['hydra:title'], + ]; + + if (isset($operationHydraDoc['expects']) && 'owl:Nothing' !== $operationHydraDoc['expects']) { + $data['input'] = sprintf('%s:%s', ApiPlatformParser::IN_PREFIX, $resourceClass); + } + + if (isset($operationHydraDoc['returns']) && 'owl:Nothing' !== $operationHydraDoc['returns']) { + $data['output'] = sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, $resourceClass); + } + + if ($collection && Request::METHOD_GET === $method) { + $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); + + $data['filters'] = []; + foreach ($this->filters as $filterName => $filter) { + if (in_array($filterName, $resourceFilters)) { + foreach ($filter->getDescription($resourceClass) as $name => $definition) { + $data['filters'][] = ['name' => $name] + $definition; + } + } + } + } + + $apiDoc = new ApiDoc($data); + $apiDoc->setRoute($route); + + return $apiDoc; + } + + /** + * Gets Hydra documentation for the given resource. + * + * @param array $hydraApiDoc + * @param string $prefixedShortName + * + * @return array|null + */ + private function getResourceHydraDoc(array $hydraApiDoc, string $prefixedShortName) + { + foreach ($hydraApiDoc['hydra:supportedClass'] as $supportedClass) { + if ($supportedClass['@id'] === $prefixedShortName) { + return $supportedClass; + } + } + } + + /** + * Gets the Hydra documentation of a given operation. + * + * @param string $method + * @param array $hydraDoc + * + * @return array|null + */ + private function getOperationHydraDoc(string $method, array $hydraDoc) + { + foreach ($hydraDoc['hydra:supportedOperation'] as $supportedOperation) { + if ($supportedOperation['hydra:method'] === $method) { + return $supportedOperation; + } + } + } + + /** + * Gets the Hydra documentation for the collection operation. + * + * @param string $shortName + * @param string $method + * @param array $hydraEntrypointDoc + * + * @return array|null + */ + private function getCollectionOperationHydraDoc(string $shortName, string $method, array $hydraEntrypointDoc) + { + $propertyName = '#Entrypoint/'.lcfirst($shortName); + + foreach ($hydraEntrypointDoc['hydra:supportedProperty'] as $supportedProperty) { + $hydraProperty = $supportedProperty['hydra:property']; + if ($hydraProperty['@id'] === $propertyName) { + return $this->getOperationHydraDoc($method, $hydraProperty); + } + } + } +} diff --git a/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php b/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php new file mode 100644 index 00000000000..b1f04ce4d2e --- /dev/null +++ b/src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php @@ -0,0 +1,210 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Bridge\NelmioApiDoc\Parser; + +use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use Nelmio\ApiDocBundle\DataTypes; +use Nelmio\ApiDocBundle\Parser\ParserInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Extract input and output information for the NelmioApiDocBundle. + * + * @author Kévin Dunglas + * @author Teoh Han Hui + */ +final class ApiPlatformParser implements ParserInterface +{ + const IN_PREFIX = 'api_platform_in'; + const OUT_PREFIX = 'api_platform_out'; + const TYPE_IRI = 'IRI'; + const TYPE_MAP = [ + Type::BUILTIN_TYPE_BOOL => DataTypes::BOOLEAN, + Type::BUILTIN_TYPE_FLOAT => DataTypes::FLOAT, + Type::BUILTIN_TYPE_INT => DataTypes::INTEGER, + Type::BUILTIN_TYPE_STRING => DataTypes::STRING, + ]; + + private $resourceMetadataFactory; + private $propertyNameCollectionFactory; + private $propertyMetadataFactory; + + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) + { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; + $this->propertyMetadataFactory = $propertyMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function supports(array $item) + { + $data = explode(':', $item['class'], 2); + if (!in_array($data[0], [self::IN_PREFIX, self::OUT_PREFIX])) { + return false; + } + if (!isset($data[1])) { + return false; + } + + try { + $this->resourceMetadataFactory->create($data[1]); + + return true; + } catch (ResourceClassNotFoundException $e) { + // return false + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function parse(array $item) : array + { + list($io, $resourceClass) = explode(':', $item['class'], 2); + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + + return $this->parseClass($resourceMetadata, $resourceClass, $io); + } + + /** + * Parses a class. + * + * @param ResourceMetadata $resourceMetadata + * @param string $resourceClass + * @param string $io + * @param string[] $visited + * + * @return array + */ + private function parseClass(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited = []) : array + { + $visited[] = $resourceClass; + + $attributes = $resourceMetadata->getAttributes(); + $options = []; + $data = []; + + if (isset($attributes['normalization_context']['groups'])) { + $options['serializer_groups'] = $attributes['normalization_context']['groups']; + } + + if (isset($attributes['denormalization_context']['groups'])) { + $options['serializer_groups'] = isset($options['serializer_groups']) ? array_merge($options['serializer_groups'], $attributes['denormalization_context']['groups']) : $options['serializer_groups']; + } + + foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); + + if ( + ($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) || + ($propertyMetadata->isWritable() && self::IN_PREFIX === $io) + ) { + $data[$propertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited); + } + } + + return $data; + } + + /** + * Parses a property. + * + * @param ResourceMetadata $resourceMetadata + * @param PropertyMetadata $propertyMetadata + * @param string $io + * @param Type|null $type + * @param string[] $visited + * + * @return array + */ + private function parseProperty(ResourceMetadata $resourceMetadata, PropertyMetadata $propertyMetadata, $io, Type $type = null, array $visited = []) + { + $data = [ + 'dataType' => null, + 'required' => $propertyMetadata->isRequired(), + 'description' => $propertyMetadata->getDescription(), + 'readonly' => !$propertyMetadata->isWritable(), + ]; + + if (null == $type) { + $type = $propertyMetadata->getType(); + + if (null === $type) { + // Default to string + $data['dataType'] = DataTypes::STRING; + + return $data; + } + } + + if ($type->isCollection()) { + $data['actualType'] = DataTypes::COLLECTION; + + if ($collectionType = $type->getCollectionValueType()) { + $subProperty = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, $collectionType, $visited); + if (self::TYPE_IRI === $subProperty['dataType']) { + $data['dataType'] = 'array of IRIs'; + $data['subType'] = DataTypes::STRING; + + return $data; + } + + $data['subType'] = $subProperty['subType']; + $data['children'] = $subProperty['children']; + } + + return $data; + } + + $builtinType = $type->getBuiltinType(); + if ('object' === $builtinType) { + $className = $type->getClassName(); + + if (is_subclass_of($className, \DateTimeInterface::class)) { + $data['dataType'] = DataTypes::DATETIME; + $data['format'] = sprintf('{DateTime %s}', \DateTime::RFC3339); + + return $data; + } + + if ( + (self::OUT_PREFIX === $io && $propertyMetadata->isReadableLink()) || + (self::IN_PREFIX === $io && $propertyMetadata->isWritableLink()) + ) { + $data['dataType'] = self::TYPE_IRI; + $data['actualType'] = DataTypes::STRING; + + return $data; + } + + $data['actualType'] = DataTypes::MODEL; + $data['subType'] = $className; + $data['children'] = in_array($className, $visited) ? [] : $this->parseClass($resourceMetadata, $className, $io, $visited); + + return $data; + } + + $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING; + + return $data; + } +} diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index e69cf50e47b..6fee0ccd85b 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -92,6 +92,11 @@ public function load(array $configs, ContainerBuilder $container) if ($config['enable_fos_user']) { $loader->load('fos_user.xml'); } + + // NelmioApiDoc support + if (isset($bundles['NelmioApiDocBundle']) && $config['enable_nelmio_api_doc']) { + $loader->load('nelmio_api_doc.xml'); + } } /** diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index 3c691fdf55d..16212aee746 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -60,6 +60,7 @@ public function getConfigTreeBuilder() ->end() ->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end() ->booleanNode('enable_fos_user')->defaultValue(false)->info('Enable the FOSUserBundle integration.')->end() + ->booleanNode('enable_nelmio_api_doc')->defaultTrue()->info('Enable the Nelmio Api doc integration.')->end() ->arrayNode('collection') ->addDefaultsIfNotSet() ->children() diff --git a/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml b/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml new file mode 100644 index 00000000000..000fef00a66 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bridge/Symfony/Routing/OperationMethodResolver.php b/src/Bridge/Symfony/Routing/OperationMethodResolver.php index 117abb90df0..7a7c6c46bc9 100644 --- a/src/Bridge/Symfony/Routing/OperationMethodResolver.php +++ b/src/Bridge/Symfony/Routing/OperationMethodResolver.php @@ -11,11 +11,17 @@ namespace ApiPlatform\Core\Bridge\Symfony\Routing; -use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouterInterface; +/** + * Resolves the HTTP method associated with an operation, extended for Symfony routing. + * + * @author Kévin Dunglas + * @author Teoh Han Hui + */ final class OperationMethodResolver implements OperationMethodResolverInterface { private $router; @@ -43,6 +49,22 @@ public function getItemOperationMethod(string $resourceClass, string $operationN return $this->getOperationMethod($resourceClass, $operationName, false); } + /** + * {@inheritdoc} + */ + public function getCollectionOperationRoute(string $resourceClass, string $operationName) : Route + { + return $this->getOperationRoute($resourceClass, $operationName, true); + } + + /** + * {@inheritdoc} + */ + public function getItemOperationRoute(string $resourceClass, string $operationName) : Route + { + return $this->getOperationRoute($resourceClass, $operationName, false); + } + /** * @param string $resourceClass * @param string $operationName @@ -52,7 +74,7 @@ public function getItemOperationMethod(string $resourceClass, string $operationN * * @return string */ - private function getOperationMethod(string $resourceClass, string $operationName, bool $collection = true) : string + private function getOperationMethod(string $resourceClass, string $operationName, bool $collection) : string { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); @@ -93,4 +115,29 @@ private function getOperationMethod(string $resourceClass, string $operationName throw new RuntimeException(sprintf('Route "%s" not found for the operation "%s" of the resource "%s".', $routeName, $operationName, $resourceClass)); } + + /** + * @param string $resourceClass + * @param string $operationName + * @param bool $collection + * + * @throws RuntimeException + * + * @return Route + */ + private function getOperationRoute(string $resourceClass, string $operationName, bool $collection) : Route + { + $operationNameKey = sprintf('_%s_operation_name', $collection ? 'collection' : 'item'); + + foreach ($this->router->getRouteCollection()->all() as $routeName => $route) { + $currentResourceClass = $route->getDefault('_resource_class'); + $currentOperationName = $route->getDefault($operationNameKey); + + if ($resourceClass === $currentResourceClass && $operationName === $currentOperationName) { + return $route; + } + } + + throw new RuntimeException(sprintf('No route found for operation "%s" for type "%s".', $operationName, $resourceClass)); + } } diff --git a/src/Bridge/Symfony/Routing/OperationMethodResolverInterface.php b/src/Bridge/Symfony/Routing/OperationMethodResolverInterface.php new file mode 100644 index 00000000000..ba0411dc43d --- /dev/null +++ b/src/Bridge/Symfony/Routing/OperationMethodResolverInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Bridge\Symfony\Routing; + +use ApiPlatform\Core\Api\OperationMethodResolverInterface as BaseOperationMethodResolverInterface; +use ApiPlatform\Core\Exception\RuntimeException; +use Symfony\Component\Routing\Route; + +/** + * Resolves the HTTP method associated with an operation, extended for Symfony routing. + * + * @author Teoh Han Hui + */ +interface OperationMethodResolverInterface extends BaseOperationMethodResolverInterface +{ + /** + * @param string $resourceClass + * @param string $operationName + * + * @throws RuntimeException + * + * @return Route + */ + public function getCollectionOperationRoute(string $resourceClass, string $operationName) : Route; + + /** + * @param string $resourceClass + * @param string $operationName + * + * @throws RuntimeException + * + * @return Route + */ + public function getItemOperationRoute(string $resourceClass, string $operationName) : Route; +} diff --git a/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php b/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php new file mode 100644 index 00000000000..a921c483670 --- /dev/null +++ b/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\NelmioApiDoc\Extractor\AnnotationsProvider; + +use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterInterface; +use ApiPlatform\Core\Bridge\NelmioApiDoc\Extractor\AnnotationsProvider\ApiPlatformProvider; +use ApiPlatform\Core\Bridge\NelmioApiDoc\Parser\ApiPlatformParser; +use ApiPlatform\Core\Bridge\Symfony\Routing\OperationMethodResolverInterface; +use ApiPlatform\Core\Hydra\ApiDocumentationBuilderInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; +use Symfony\Component\Routing\Route; + +/** + * @author Teoh Han Hui + */ +class ApiPlatformProviderTest extends \PHPUnit_Framework_TestCase +{ + public function testConstruct() + { + $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); + $resourceNameCollectionFactory = $resourceNameCollectionFactoryProphecy->reveal(); + + $apiDocumentationBuilderProphecy = $this->prophesize(ApiDocumentationBuilderInterface::class); + $apiDocumentationBuilder = $apiDocumentationBuilderProphecy->reveal(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $filters = new FilterCollection(); + + $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); + $operationMethodResolver = $operationMethodResolverProphecy->reveal(); + + $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $apiDocumentationBuilder, $resourceMetadataFactory, $filters, $operationMethodResolver); + + $this->assertInstanceOf(AnnotationsProviderInterface::class, $apiPlatformProvider); + } + + public function testGetAnnotations() + { + $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([ + Dummy::class, + ]))->shouldBeCalled(); + $resourceNameCollectionFactory = $resourceNameCollectionFactoryProphecy->reveal(); + + $apiDocumentationBuilderProphecy = $this->prophesize(ApiDocumentationBuilderInterface::class); + $hydraDoc = $this->getHydraDoc(); + $apiDocumentationBuilderProphecy->getApiDocumentation()->willReturn($hydraDoc)->shouldBeCalled(); + $apiDocumentationBuilder = $apiDocumentationBuilderProphecy->reveal(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $dummyResourceMetadata = (new ResourceMetadata()) + ->withShortName('Dummy') + ->withItemOperations([ + 'get' => [ + 'method' => 'GET', + ], + 'put' => [ + 'method' => 'PUT', + ], + 'delete' => [ + 'method' => 'DELETE', + ], + ]) + ->withCollectionOperations([ + 'get' => [ + 'filters' => [ + 'my_dummy.search', + ], + 'method' => 'GET', + ], + 'post' => [ + 'method' => 'POST', + ], + ]); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn($dummyResourceMetadata)->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $dummySearchFilterProphecy = $this->prophesize(FilterInterface::class); + $dummySearchFilterProphecy->getDescription(Dummy::class)->willReturn([ + 'name' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => 'false', + 'strategy' => 'partial', + ], + ])->shouldBeCalled(); + $dummySearchFilter = $dummySearchFilterProphecy->reveal(); + $filters = new FilterCollection([ + 'my_dummy.search' => $dummySearchFilter, + ]); + + $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); + $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'get')->willReturn('GET')->shouldBeCalled(); + $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'post')->willReturn('POST')->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationMethod(Dummy::class, 'get')->willReturn('GET')->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationMethod(Dummy::class, 'put')->willReturn('PUT')->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationMethod(Dummy::class, 'delete')->willReturn('DELETE')->shouldBeCalled(); + $operationMethodResolverProphecy->getCollectionOperationRoute(Dummy::class, 'get')->willReturn((new Route('/dummies'))->setMethods(['GET']))->shouldBeCalled(); + $operationMethodResolverProphecy->getCollectionOperationRoute(Dummy::class, 'post')->willReturn((new Route('/dummies'))->setMethods(['POST']))->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationRoute(Dummy::class, 'get')->willReturn((new Route('/dummies/{id}'))->setMethods(['GET']))->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationRoute(Dummy::class, 'put')->willReturn((new Route('/dummies/{id}'))->setMethods(['PUT']))->shouldBeCalled(); + $operationMethodResolverProphecy->getItemOperationRoute(Dummy::class, 'delete')->willReturn((new Route('/dummies/{id}'))->setMethods(['DELETE']))->shouldBeCalled(); + $operationMethodResolver = $operationMethodResolverProphecy->reveal(); + + $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $apiDocumentationBuilder, $resourceMetadataFactory, $filters, $operationMethodResolver); + + $actual = $apiPlatformProvider->getAnnotations(); + + $this->assertInternalType('array', $actual); + $this->assertCount(5, $actual); + + $this->assertInstanceOf(ApiDoc::class, $actual[0]); + $this->assertEquals('/dummies', $actual[0]->getResource()); + $this->assertEquals('Retrieves the collection of Dummy resources.', $actual[0]->getDescription()); + $this->assertEquals('Dummy', $actual[0]->getResourceDescription()); + $this->assertEquals('Dummy', $actual[0]->getSection()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), $actual[0]->getOutput()); + $this->assertEquals([ + 'name' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => 'false', + 'strategy' => 'partial', + ], + ], $actual[0]->getFilters()); + $this->assertInstanceOf(Route::class, $actual[0]->getRoute()); + $this->assertEquals('/dummies', $actual[0]->getRoute()->getPath()); + $this->assertEquals(['GET'], $actual[0]->getRoute()->getMethods()); + + $this->assertInstanceOf(ApiDoc::class, $actual[1]); + $this->assertEquals('/dummies', $actual[1]->getResource()); + $this->assertEquals('Creates a Dummy resource.', $actual[1]->getDescription()); + $this->assertEquals('Dummy', $actual[1]->getResourceDescription()); + $this->assertEquals('Dummy', $actual[1]->getSection()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::IN_PREFIX, Dummy::class), $actual[1]->getInput()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), $actual[1]->getOutput()); + $this->assertInstanceOf(Route::class, $actual[1]->getRoute()); + $this->assertEquals('/dummies', $actual[1]->getRoute()->getPath()); + $this->assertEquals(['POST'], $actual[1]->getRoute()->getMethods()); + + $this->assertInstanceOf(ApiDoc::class, $actual[2]); + $this->assertEquals('/dummies/{id}', $actual[2]->getResource()); + $this->assertEquals('Retrieves Dummy resource.', $actual[2]->getDescription()); + $this->assertEquals('Dummy', $actual[2]->getResourceDescription()); + $this->assertEquals('Dummy', $actual[2]->getSection()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), $actual[2]->getOutput()); + $this->assertInstanceOf(Route::class, $actual[2]->getRoute()); + $this->assertEquals('/dummies/{id}', $actual[2]->getRoute()->getPath()); + $this->assertEquals(['GET'], $actual[2]->getRoute()->getMethods()); + + $this->assertInstanceOf(ApiDoc::class, $actual[3]); + $this->assertEquals('/dummies/{id}', $actual[3]->getResource()); + $this->assertEquals('Replaces the Dummy resource.', $actual[3]->getDescription()); + $this->assertEquals('Dummy', $actual[3]->getResourceDescription()); + $this->assertEquals('Dummy', $actual[3]->getSection()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::IN_PREFIX, Dummy::class), $actual[3]->getInput()); + $this->assertEquals(sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), $actual[3]->getOutput()); + $this->assertInstanceOf(Route::class, $actual[3]->getRoute()); + $this->assertEquals('/dummies/{id}', $actual[3]->getRoute()->getPath()); + $this->assertEquals(['PUT'], $actual[3]->getRoute()->getMethods()); + + $this->assertInstanceOf(ApiDoc::class, $actual[4]); + $this->assertEquals('/dummies/{id}', $actual[4]->getResource()); + $this->assertEquals('Deletes the Dummy resource.', $actual[4]->getDescription()); + $this->assertEquals('Dummy', $actual[4]->getResourceDescription()); + $this->assertEquals('Dummy', $actual[4]->getSection()); + $this->assertInstanceOf(Route::class, $actual[4]->getRoute()); + $this->assertEquals('/dummies/{id}', $actual[4]->getRoute()->getPath()); + $this->assertEquals(['DELETE'], $actual[4]->getRoute()->getMethods()); + } + + private function getHydraDoc() + { + $hydraDocJson = << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\NelmioApiDoc\Parser; + +use ApiPlatform\Core\Bridge\NelmioApiDoc\Parser\ApiPlatformParser; +use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +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\RelatedDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UnknownDummy; +use Nelmio\ApiDocBundle\DataTypes; +use Nelmio\ApiDocBundle\Parser\ParserInterface; +use Prophecy\Argument; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Teoh Han Hui + */ +class ApiPlatformParserTest extends \PHPUnit_Framework_TestCase +{ + public function testConstruct() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertInstanceOf(ParserInterface::class, $apiPlatformParser); + } + + public function testSupports() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata())->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertTrue($apiPlatformParser->supports([ + 'class' => sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), + ])); + } + + public function testSupportsUnknownResource() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(UnknownDummy::class)->willThrow(ResourceClassNotFoundException::class)->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertFalse($apiPlatformParser->supports([ + 'class' => sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, UnknownDummy::class), + ])); + } + + public function testSupportsUnsupportedClassFormat() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Argument::any())->shouldNotBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $this->assertFalse($apiPlatformParser->supports([ + 'class' => Dummy::class, + ])); + } + + public function testParse() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata())->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn(new PropertyNameCollection([ + 'id', + 'name', + 'dummyPrice', + ]))->shouldBeCalled(); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $idPropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_INT, false)) + ->withDescription('The id.') + ->withReadable(true) + ->withWritable(false) + ->withRequired(true); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->willReturn($idPropertyMetadata)->shouldBeCalled(); + $namePropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_STRING, false)) + ->withDescription('The dummy name.') + ->withReadable(true) + ->withWritable(true) + ->withRequired(true); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->willReturn($namePropertyMetadata)->shouldBeCalled(); + $dummyPricePropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_FLOAT, true)) + ->withDescription('A dummy price.') + ->withReadable(true) + ->withWritable(true) + ->withRequired(false); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyPrice')->willReturn($dummyPricePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $actual = $apiPlatformParser->parse([ + 'class' => sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), + ]); + + $this->assertEquals([ + 'id' => [ + 'dataType' => DataTypes::INTEGER, + 'required' => true, + 'description' => 'The id.', + 'readonly' => true, + ], + 'name' => [ + 'dataType' => DataTypes::STRING, + 'required' => true, + 'description' => 'The dummy name.', + 'readonly' => false, + ], + 'dummyPrice' => [ + 'dataType' => DataTypes::FLOAT, + 'required' => false, + 'description' => 'A dummy price.', + 'readonly' => false, + ], + ], $actual); + } + + public function testParseDateTime() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata())->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn(new PropertyNameCollection([ + 'dummyDate', + ]))->shouldBeCalled(); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $dummyDatePropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)) + ->withDescription('A dummy date.') + ->withReadable(true) + ->withWritable(true) + ->withRequired(false); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate')->willReturn($dummyDatePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $actual = $apiPlatformParser->parse([ + 'class' => sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), + ]); + + $this->assertEquals([ + 'dummyDate' => [ + 'dataType' => DataTypes::DATETIME, + 'required' => false, + 'description' => 'A dummy date.', + 'readonly' => false, + 'format' => sprintf('{DateTime %s}', \DateTime::RFC3339), + ], + ], $actual); + } + + public function testParseRelation() + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata())->shouldBeCalled(); + $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn(new PropertyNameCollection([ + 'relatedDummy', + 'relatedDummies', + ]))->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class, Argument::cetera())->willReturn(new PropertyNameCollection([ + 'id', + 'name', + ]))->shouldBeCalled(); + $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $relatedDummyPropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_OBJECT, true, RelatedDummy::class)) + ->withDescription('A related dummy.') + ->withReadable(true) + ->withWritable(true) + ->withRequired(false); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy')->willReturn($relatedDummyPropertyMetadata)->shouldBeCalled(); + $relatedDummiesPropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class))) + ->withDescription('Several dummies.') + ->withReadable(true) + ->withWritable(true) + ->withReadableLink(true) + ->withRequired(false); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies')->willReturn($relatedDummiesPropertyMetadata)->shouldBeCalled(); + $idPropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_INT, false)) + ->withReadable(true) + ->withWritable(false) + ->withRequired(true); + $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id')->willReturn($idPropertyMetadata)->shouldBeCalled(); + $namePropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_STRING, false)) + ->withDescription('A name.') + ->withReadable(true) + ->withWritable(true) + ->withRequired(false); + $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name')->willReturn($namePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); + + $apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory); + + $actual = $apiPlatformParser->parse([ + 'class' => sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class), + ]); + + $this->assertEquals([ + 'relatedDummy' => [ + 'dataType' => null, + 'required' => false, + 'description' => 'A related dummy.', + 'readonly' => false, + 'actualType' => DataTypes::MODEL, + 'subType' => RelatedDummy::class, + 'children' => [ + 'id' => [ + 'dataType' => DataTypes::INTEGER, + 'required' => true, + 'description' => null, + 'readonly' => true, + ], + 'name' => [ + 'dataType' => DataTypes::STRING, + 'required' => false, + 'description' => 'A name.', + 'readonly' => false, + ], + ], + ], + 'relatedDummies' => [ + 'dataType' => 'array of IRIs', + 'required' => false, + 'description' => 'Several dummies.', + 'readonly' => false, + 'actualType' => DataTypes::COLLECTION, + 'subType' => DataTypes::STRING, + ], + ], $actual); + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index df05a28f001..077d7f78221 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -138,6 +138,20 @@ public function testEnableFosUser() $this->extension->load(array_merge_recursive(self::DEFAULT_CONFIG, ['api_platform' => ['enable_fos_user' => true]]), $containerBuilder); } + public function testEnableNelmioApiDoc() + { + $containerBuilderProphecy = $this->getContainerBuilderProphecy(); + $containerBuilderProphecy->getParameter('kernel.bundles')->willReturn([ + 'DoctrineBundle' => 'Doctrine\Bundle\DoctrineBundle\DoctrineBundle', + 'NelmioApiDocBundle' => 'Nelmio\ApiDocBundle\NelmioApiDocBundle', + ])->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('api_platform.nelmio_api_doc.annotations_provider', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilderProphecy->setDefinition('api_platform.nelmio_api_doc.parser', Argument::type(Definition::class))->shouldBeCalled(); + $containerBuilder = $containerBuilderProphecy->reveal(); + + $this->extension->load(array_merge_recursive(self::DEFAULT_CONFIG, ['api_platform' => ['enable_nelmio_api_doc' => true]]), $containerBuilder); + } + private function getContainerBuilderProphecy() { $definitionArgument = Argument::that(function ($argument) { diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 76ddcaa9633..b7c0d329d6e 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -36,6 +36,7 @@ public function testDefaultConfig() 'supported_formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], 'name_converter' => null, 'enable_fos_user' => false, + 'enable_nelmio_api_doc' => true, 'collection' => [ 'order' => null, 'order_parameter_name' => 'order',