diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php
index f55fb76b5e0..1790ddead56 100644
--- a/features/bootstrap/FeatureContext.php
+++ b/features/bootstrap/FeatureContext.php
@@ -609,9 +609,11 @@ public function thereIsAnAnswerToTheQuestion(string $a, string $q)
$question = new Question();
$question->setContent($q);
$question->setAnswer($answer);
+ $answer->addRelatedQuestion($question);
$this->manager->persist($answer);
$this->manager->persist($question);
+
$this->manager->flush();
}
diff --git a/features/main/subresource.feature b/features/main/subresource.feature
index ab6ccba3c6e..66ac56ffd5d 100644
--- a/features/main/subresource.feature
+++ b/features/main/subresource.feature
@@ -12,12 +12,39 @@ Feature: Subresource support
And the JSON should be equal to:
"""
{
- "@context": "\/contexts\/Answer",
- "@id": "\/answers\/1",
- "@type": "Answer",
- "id": 1,
- "content": "42",
- "question": "\/questions\/1"
+ "@context": "/contexts/Answer",
+ "@id": "/answers/1",
+ "@type": "Answer",
+ "id": 1,
+ "content": "42",
+ "question": "/questions/1",
+ "relatedQuestions": [
+ "/questions/1"
+ ]
+ }
+ """
+
+ Scenario: Get subresource one to one relation
+ When I send a "GET" request to "/questions/1/answer/related_questions"
+ And print last JSON response
+ And the response status code should be 200
+ And the response should be in JSON
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/Question",
+ "@id": "/questions/1/answer/related_questions",
+ "@type": "hydra:Collection",
+ "hydra:member": [
+ {
+ "@id": "/questions/1",
+ "@type": "Question",
+ "content": "What's the answer to the Ultimate Question of Life, the Universe and Everything?",
+ "id": 1,
+ "answer": "/answers/1"
+ }
+ ],
+ "hydra:totalItems": 1
}
"""
diff --git a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
index 50b95d0f69b..df99468f832 100644
--- a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
+++ b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterEagerLoadingExtension;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
@@ -105,7 +106,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array
$qb = $manager->createQueryBuilder();
$alias = $queryNameGenerator->generateJoinAlias($identifier);
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type'];
- $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);
+ $normalizedIdentifiers = isset($identifiers[$identifier]) ? $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass) : [];
switch ($relationType) {
//MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved
@@ -169,6 +170,11 @@ public function getSubresource(string $resourceClass, array $identifiers, array
if (true === $context['collection']) {
foreach ($this->collectionExtensions as $extension) {
+ // We don't need this anymore because we already made sub queries to ensure correct results
+ if ($extension instanceof FilterEagerLoadingExtension) {
+ continue;
+ }
+
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
diff --git a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
index a4f9ca17fc2..df7d5bfeac0 100644
--- a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
+++ b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
@@ -117,6 +117,8 @@ private function getContext(Request $request, Documentation $documentation): arr
$swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, $swaggerData['shortName']);
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, $swaggerData['shortName']);
+ } elseif (null !== $subresourceOperationContext = $request->attributes->get('_api_subresource_context')) {
+ $swaggerData['operationId'] = $subresourceOperationContext['operationId'];
}
list($swaggerData['path'], $swaggerData['method']) = $this->getPathAndMethod($swaggerData);
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index 95641d5dfaa..2b4837337dd 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -43,8 +43,15 @@ public function getConfigTreeBuilder()
->scalarNode('title')->defaultValue('')->info('The title of the API.')->end()
->scalarNode('description')->defaultValue('')->info('The description of the API.')->end()
->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end()
- ->scalarNode('default_operation_path_resolver')->defaultValue('api_platform.operation_path_resolver.underscore')->info('Specify the default operation path resolver to use for generating resources operations path.')->end()
+ ->scalarNode('default_operation_path_resolver')
+ ->beforeNormalization()->always(function ($v) {
+ if (isset($v['default_operation_path_resolver'])) {
+ @trigger_error('The use of the `default_operation_path_resolver` has been deprecated in 2.1 and will be removed in 3.0. Use `path_segment_name_generator` instead.', E_USER_DEPRECATED);
+ }
+ })->end()
+ ->defaultValue('api_platform.operation_path_resolver.underscore')->info('Specify the default operation path resolver to use for generating resources operations path.')->end()
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
+ ->scalarNode('path_segment_name_generator')->defaultValue('api_platform.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end()
->scalarNode('api_resources_directory')->defaultValue('Entity')->info('The name of the directory within the bundles that contains the api resources.')->end()
->arrayNode('eager_loading')
->canBeDisabled()
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index 43fde27e12d..528e7747211 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -36,8 +36,7 @@
%api_platform.formats%
%api_platform.resource_class_directories%
-
-
+
@@ -91,14 +90,31 @@
+
+
-
-
+
+
+
+
+
+ The "%service_id%" service is deprecated since ApiPlatform 2.1 and will be removed in 3.0. Use PathSegmentNameGenerator instead.
+
+
+
+ The "%service_id%" service is deprecated since ApiPlatform 2.1 and will be removed in 3.0. Use PathSegmentNameGenerator instead.
+
+
+
+
+
+
+
@@ -194,6 +210,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -203,6 +233,10 @@
+
+
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
index a8e188f2079..914d7661ad7 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
@@ -22,6 +22,7 @@
%api_platform.oauth.tokenUrl%
%api_platform.oauth.authorizationUrl%
%api_platform.oauth.scopes%
+
diff --git a/src/Bridge/Symfony/Routing/ApiLoader.php b/src/Bridge/Symfony/Routing/ApiLoader.php
index 7c1c86b3a3d..856f1934ab6 100644
--- a/src/Bridge/Symfony/Routing/ApiLoader.php
+++ b/src/Bridge/Symfony/Routing/ApiLoader.php
@@ -16,10 +16,9 @@
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Exception\InvalidResourceException;
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\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\Loader;
@@ -42,19 +41,17 @@ final class ApiLoader extends Loader
*/
const ROUTE_NAME_PREFIX = 'api_';
const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
- const SUBRESOURCE_SUFFIX = '_get_subresource';
private $fileLoader;
- private $propertyNameCollectionFactory;
- private $propertyMetadataFactory;
private $resourceNameCollectionFactory;
private $resourceMetadataFactory;
private $operationPathResolver;
private $container;
private $formats;
private $resourceClassDirectories;
+ private $subresourceOperationFactory;
- public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory = null, PropertyMetadataFactoryInterface $propertyMetadataFactory = null)
+ public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null)
{
$this->fileLoader = new XmlFileLoader(new FileLocator($kernel->locateResource('@ApiPlatformBundle/Resources/config/routing')));
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
@@ -63,8 +60,7 @@ public function __construct(KernelInterface $kernel, ResourceNameCollectionFacto
$this->container = $container;
$this->formats = $formats;
$this->resourceClassDirectories = $resourceClassDirectories;
- $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
- $this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->subresourceOperationFactory = $subresourceOperationFactory;
}
/**
@@ -99,90 +95,35 @@ public function load($data, $type = null): RouteCollection
}
}
- $this->computeSubresourceOperations($routeCollection, $resourceClass);
- }
-
- return $routeCollection;
- }
-
- /**
- * Handles subresource operations recursively and declare their corresponding routes.
- *
- * @param RouteCollection $routeCollection
- * @param string $resourceClass
- * @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
- * @param array $parentOperation the previous call operation
- */
- private function computeSubresourceOperations(RouteCollection $routeCollection, string $resourceClass, string $rootResourceClass = null, array $parentOperation = null, array $visited = [])
- {
- if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
- return;
- }
-
- if (null === $rootResourceClass) {
- $rootResourceClass = $resourceClass;
- }
-
- foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
- $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
-
- if (!$propertyMetadata->hasSubresource()) {
- continue;
- }
-
- $subresource = $propertyMetadata->getSubresource();
-
- $operation = [
- 'property' => $property,
- 'collection' => $subresource->isCollection(),
- ];
-
- $visiting = "$rootResourceClass $resourceClass $property {$subresource->isCollection()} {$subresource->getResourceClass()}";
-
- if (in_array($visiting, $visited, true)) {
+ if (null === $this->subresourceOperationFactory) {
continue;
}
- $visited[] = $visiting;
-
- if (null === $parentOperation) {
- $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
- $rootShortname = $rootResourceMetadata->getShortName();
-
- $operation['identifiers'] = [['id', $rootResourceClass]];
- $operation['route_name'] = RouteNameGenerator::generate('get', $rootShortname, OperationType::SUBRESOURCE, $operation);
- $operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE, $operation['route_name']);
- } else {
- $operation['identifiers'] = $parentOperation['identifiers'];
- $operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
- $operation['route_name'] = str_replace('get'.RouteNameGenerator::SUBRESOURCE_SUFFIX, RouteNameGenerator::routeNameResolver($property, $operation['collection']).'_get'.RouteNameGenerator::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
- $operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE, $operation['route_name']);
- }
-
- $route = new Route(
- $operation['path'],
- [
- '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
- '_format' => null,
- '_api_resource_class' => $subresource->getResourceClass(),
- '_api_subresource_operation_name' => $operation['route_name'],
- '_api_subresource_context' => [
- 'property' => $operation['property'],
- 'identifiers' => $operation['identifiers'],
- 'collection' => $subresource->isCollection(),
+ foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) {
+ $routeCollection->add($operation['route_name'], new Route(
+ $operation['path'],
+ [
+ '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
+ '_format' => null,
+ '_api_resource_class' => $operation['resource_class'],
+ '_api_subresource_operation_name' => $operation['route_name'],
+ '_api_subresource_context' => [
+ 'property' => $operation['property'],
+ 'identifiers' => $operation['identifiers'],
+ 'collection' => $operation['collection'],
+ 'operationId' => $operationId,
+ ],
],
- ],
- [],
- [],
- '',
- [],
- ['GET']
- );
-
- $routeCollection->add($operation['route_name'], $route);
-
- $this->computeSubresourceOperations($routeCollection, $subresource->getResourceClass(), $rootResourceClass, $operation, $visited);
+ [],
+ [],
+ '',
+ [],
+ ['GET']
+ ));
+ }
}
+
+ return $routeCollection;
}
/**
diff --git a/src/Bridge/Symfony/Routing/RouteNameGenerator.php b/src/Bridge/Symfony/Routing/RouteNameGenerator.php
index 3f596c9b7ce..e325b6174ca 100644
--- a/src/Bridge/Symfony/Routing/RouteNameGenerator.php
+++ b/src/Bridge/Symfony/Routing/RouteNameGenerator.php
@@ -25,10 +25,9 @@
*
* @author Baptiste Meyer
*/
-class RouteNameGenerator
+final class RouteNameGenerator
{
const ROUTE_NAME_PREFIX = 'api_';
- const SUBRESOURCE_SUFFIX = '_subresource';
private function __construct()
{
@@ -40,33 +39,21 @@ private function __construct()
* @param string $operationName
* @param string $resourceShortName
* @param string|bool $operationType
- * @param array $subresourceContext
*
* @throws InvalidArgumentException
*
* @return string
*/
- public static function generate(string $operationName, string $resourceShortName, $operationType, array $subresourceContext = []): string
+ public static function generate(string $operationName, string $resourceShortName, $operationType): string
{
if (OperationType::SUBRESOURCE === $operationType = OperationTypeDeprecationHelper::getOperationType($operationType)) {
- if (!isset($subresourceContext['property'])) {
- throw new InvalidArgumentException('Missing "property" to generate a route name from a subresource');
- }
-
- return sprintf(
- '%s%s_%s_%s%s',
- static::ROUTE_NAME_PREFIX,
- self::routeNameResolver($resourceShortName),
- self::routeNameResolver($subresourceContext['property'], $subresourceContext['collection'] ?? false),
- $operationName,
- self::SUBRESOURCE_SUFFIX
- );
+ throw new InvalidArgumentException('Subresource operations are not supported by the RouteNameGenerator.');
}
return sprintf(
'%s%s_%s_%s',
static::ROUTE_NAME_PREFIX,
- self::routeNameResolver($resourceShortName),
+ self::inflector($resourceShortName),
$operationName,
$operationType
);
@@ -79,7 +66,7 @@ public static function generate(string $operationName, string $resourceShortName
*
* @return string A string that is a part of the route name
*/
- public static function routeNameResolver(string $name, bool $pluralize = true): string
+ public static function inflector(string $name, bool $pluralize = true): string
{
$name = Inflector::tableize($name);
diff --git a/src/Bridge/Symfony/Routing/RouterOperationPathResolver.php b/src/Bridge/Symfony/Routing/RouterOperationPathResolver.php
index 77618b8644f..d760b1e172e 100644
--- a/src/Bridge/Symfony/Routing/RouterOperationPathResolver.php
+++ b/src/Bridge/Symfony/Routing/RouterOperationPathResolver.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\Bridge\Symfony\Routing;
+use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
@@ -51,10 +52,14 @@ public function resolveOperationPath(string $resourceShortName, array $operation
if (isset($operation['route_name'])) {
$routeName = $operation['route_name'];
- } elseif (null !== $operationName) {
- $routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType, $operation);
+ } elseif (OperationType::SUBRESOURCE === $operationType) {
+ throw new InvalidArgumentException('Subresource operations are not supported by the RouterOperationPathResolver without a route name.');
} else {
- return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
+ if (null !== $operationName) {
+ $routeName = RouteNameGenerator::generate($operationName, $resourceShortName, $operationType);
+ } else {
+ return $this->deferred->resolveOperationPath($resourceShortName, $operation, OperationTypeDeprecationHelper::getOperationType($operationType), $operationName);
+ }
}
if (!$route = $this->router->getRouteCollection()->get($routeName)) {
diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php
index 590345bc5c7..4bc3ba0e741 100644
--- a/src/EventListener/ReadListener.php
+++ b/src/EventListener/ReadListener.php
@@ -133,8 +133,10 @@ private function getSubresourceData(Request $request, array $attributes)
}
$identifiers = [];
- foreach ($attributes['subresource_context']['identifiers'] as $key => list($id)) {
- $identifiers[$id] = $request->attributes->get($id);
+ foreach ($attributes['subresource_context']['identifiers'] as $key => list($id, $class, $hasIdentifier)) {
+ if (true === $hasIdentifier) {
+ $identifiers[$id] = $request->attributes->get($id);
+ }
}
$data = $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'], $attributes['subresource_operation_name']);
diff --git a/src/Operation/DashPathSegmentNameGenerator.php b/src/Operation/DashPathSegmentNameGenerator.php
new file mode 100644
index 00000000000..91042d9a396
--- /dev/null
+++ b/src/Operation/DashPathSegmentNameGenerator.php
@@ -0,0 +1,39 @@
+
+ *
+ * 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\Operation;
+
+use Doctrine\Common\Util\Inflector;
+
+/**
+ * Generate a path name with a dash separator according to a string and whether it's a collection or not.
+ *
+ * @author Antoine Bluchet
+ */
+final class DashPathSegmentNameGenerator implements PathSegmentNameGeneratorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getSegmentName(string $name, bool $collection = true): string
+ {
+ $name = $this->dashize($name);
+
+ return $collection ? Inflector::pluralize($name) : $name;
+ }
+
+ private function dashize(string $string): string
+ {
+ return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $string));
+ }
+}
diff --git a/src/Operation/Factory/CachedSubresourceOperationFactory.php b/src/Operation/Factory/CachedSubresourceOperationFactory.php
new file mode 100644
index 00000000000..7c615066444
--- /dev/null
+++ b/src/Operation/Factory/CachedSubresourceOperationFactory.php
@@ -0,0 +1,59 @@
+
+ *
+ * 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\Operation\Factory;
+
+use Psr\Cache\CacheException;
+use Psr\Cache\CacheItemPoolInterface;
+
+/**
+ * @internal
+ */
+final class CachedSubresourceOperationFactory implements SubresourceOperationFactoryInterface
+{
+ const CACHE_KEY_PREFIX = 'subresource_operations_';
+
+ private $cacheItemPool;
+ private $decorated;
+
+ public function __construct(CacheItemPoolInterface $cacheItemPool, SubresourceOperationFactoryInterface $decorated)
+ {
+ $this->cacheItemPool = $cacheItemPool;
+ $this->decorated = $decorated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(string $resourceClass): array
+ {
+ $cacheKey = self::CACHE_KEY_PREFIX.str_replace('\\', '', $resourceClass);
+
+ try {
+ $cacheItem = $this->cacheItemPool->getItem($cacheKey);
+
+ if ($cacheItem->isHit()) {
+ return $cacheItem->get();
+ }
+ } catch (CacheException $e) {
+ return $this->decorated->create($resourceClass);
+ }
+
+ $subresourceOperations = $this->decorated->create($resourceClass);
+
+ $cacheItem->set($subresourceOperations);
+ $this->cacheItemPool->save($cacheItem);
+
+ return $subresourceOperations;
+ }
+}
diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php
new file mode 100644
index 00000000000..71f032bc799
--- /dev/null
+++ b/src/Operation/Factory/SubresourceOperationFactory.php
@@ -0,0 +1,144 @@
+
+ *
+ * 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\Operation\Factory;
+
+use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface;
+
+/**
+ * @internal
+ */
+final class SubresourceOperationFactory implements SubresourceOperationFactoryInterface
+{
+ const SUBRESOURCE_SUFFIX = '_subresource';
+ const FORMAT_SUFFIX = '.{_format}';
+
+ private $resourceMetadataFactory;
+ private $propertyNameCollectionFactory;
+ private $propertyMetadataFactory;
+ private $pathSegmentNameGenerator;
+
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator)
+ {
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
+ $this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->pathSegmentNameGenerator = $pathSegmentNameGenerator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(string $resourceClass): array
+ {
+ $tree = [];
+ $this->computeSubresourceOperations($resourceClass, $tree);
+
+ return $tree;
+ }
+
+ /**
+ * Handles subresource operations recursively and declare their corresponding routes.
+ *
+ * @param string $resourceClass
+ * @param array $tree
+ * @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
+ * @param array $parentOperation the previous call operation
+ */
+ private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null)
+ {
+ if (null === $rootResourceClass) {
+ $rootResourceClass = $resourceClass;
+ }
+
+ foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
+ $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
+
+ if (!$propertyMetadata->hasSubresource()) {
+ continue;
+ }
+
+ $subresource = $propertyMetadata->getSubresource();
+ $subresourceClass = $subresource->getResourceClass();
+ $subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass);
+
+ if (null === $parentOperation) {
+ $visiting = "$rootResourceClass-$property-$subresourceClass";
+ } else {
+ $prefix = '';
+ $visiting = "{$parentOperation['property']}-{$parentOperation['resource_class']}-$property-$subresourceClass";
+
+ foreach ($parentOperation['identifiers'] as $key => list($param, $class)) {
+ $prefix .= 0 === $key ? $class : "-$param-$class";
+ }
+
+ if (false !== strpos($prefix, $visiting)) {
+ continue;
+ }
+
+ $visiting = $prefix.'-'.$visiting;
+ }
+
+ $operationName = 'get';
+ $operation = [
+ 'property' => $property,
+ 'collection' => $subresource->isCollection(),
+ 'resource_class' => $subresourceClass,
+ 'shortNames' => [$subresourceMetadata->getShortName()],
+ ];
+
+ if (null === $parentOperation) {
+ $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
+ $rootShortname = $rootResourceMetadata->getShortName();
+ $operation['identifiers'] = [['id', $rootResourceClass, true]];
+ $operation['route_name'] = sprintf(
+ '%s%s_%s_%s%s',
+ RouteNameGenerator::ROUTE_NAME_PREFIX,
+ RouteNameGenerator::inflector($rootShortname),
+ RouteNameGenerator::inflector($operation['property'], $operation['collection'] ?? false),
+ $operationName,
+ self::SUBRESOURCE_SUFFIX
+ );
+
+ $operation['path'] = sprintf(
+ '/%s/{id}/%s%s',
+ $this->pathSegmentNameGenerator->getSegmentName($rootShortname, true),
+ $this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']),
+ self::FORMAT_SUFFIX
+ );
+
+ $operation['shortNames'][] = $rootShortname;
+ } else {
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ $operation['identifiers'] = $parentOperation['identifiers'];
+ $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $parentOperation['collection']];
+ $operation['route_name'] = str_replace('get'.self::SUBRESOURCE_SUFFIX, RouteNameGenerator::inflector($property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
+ $operation['shortNames'][] = $resourceMetadata->getShortName();
+
+ $operation['path'] = str_replace(self::FORMAT_SUFFIX, '', $parentOperation['path']);
+ if ($parentOperation['collection']) {
+ list($key) = end($operation['identifiers']);
+ $operation['path'] .= sprintf('/{%s}', $key);
+ }
+ $operation['path'] .= sprintf('/%s%s', $this->pathSegmentNameGenerator->getSegmentName($property, $operation['collection']), self::FORMAT_SUFFIX);
+ }
+
+ $tree[$visiting] = $operation;
+ $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation);
+ }
+ }
+}
diff --git a/src/Operation/Factory/SubresourceOperationFactoryInterface.php b/src/Operation/Factory/SubresourceOperationFactoryInterface.php
new file mode 100644
index 00000000000..5a3460cb866
--- /dev/null
+++ b/src/Operation/Factory/SubresourceOperationFactoryInterface.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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\Operation\Factory;
+
+/**
+ * Computes subresource operation for a given resource.
+ *
+ * @author Antoine Bluchet
+ */
+interface SubresourceOperationFactoryInterface
+{
+ /**
+ * Creates subresource operations.
+ *
+ * @param string $resourceClass
+ *
+ * @return array
+ */
+ public function create(string $resourceClass): array;
+}
diff --git a/src/Operation/PathSegmentNameGeneratorInterface.php b/src/Operation/PathSegmentNameGeneratorInterface.php
new file mode 100644
index 00000000000..d777cee7e67
--- /dev/null
+++ b/src/Operation/PathSegmentNameGeneratorInterface.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Operation;
+
+/**
+ * Generates a path name according to a string and whether it's a collection or not.
+ *
+ * @author Antoine Bluchet
+ */
+interface PathSegmentNameGeneratorInterface
+{
+ /**
+ * Transforms a given string to a valid path name which can be pluralized (eg. for collections).
+ *
+ * @param string $name usually a ResourceMetadata shortname
+ * @param bool $collection
+ *
+ * @return string A string that is a part of the route name
+ */
+ public function getSegmentName(string $name, bool $collection = true): string;
+}
diff --git a/src/Operation/UnderscorePathSegmentNameGenerator.php b/src/Operation/UnderscorePathSegmentNameGenerator.php
new file mode 100644
index 00000000000..9e39329b66c
--- /dev/null
+++ b/src/Operation/UnderscorePathSegmentNameGenerator.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\Operation;
+
+use Doctrine\Common\Util\Inflector;
+
+/**
+ * Generate a path name with an underscore separator according to a string and whether it's a collection or not.
+ *
+ * @author Antoine Bluchet
+ */
+final class UnderscorePathSegmentNameGenerator implements PathSegmentNameGeneratorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getSegmentName(string $name, bool $collection = true): string
+ {
+ $name = Inflector::tableize($name);
+
+ return $collection ? Inflector::pluralize($name) : $name;
+ }
+}
diff --git a/src/PathResolver/DashOperationPathResolver.php b/src/PathResolver/DashOperationPathResolver.php
index 28df5dd1572..b9d13414770 100644
--- a/src/PathResolver/DashOperationPathResolver.php
+++ b/src/PathResolver/DashOperationPathResolver.php
@@ -13,12 +13,10 @@
namespace ApiPlatform\Core\PathResolver;
-use ApiPlatform\Core\Api\OperationType;
-use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Operation\DashPathSegmentNameGenerator;
/**
- * Generates a path with words separated by dashes.
+ * Generates a path with words separated by underscores.
*
* @author Paul Le Corre
*/
@@ -29,35 +27,14 @@ final class DashOperationPathResolver implements OperationPathResolverInterface
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
- if (func_num_args() < 4) {
- @trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
- }
-
- $operationType = OperationTypeDeprecationHelper::getOperationType($operationType);
+ @trigger_error(sprintf('The use of %s is deprecated since 2.1. Please use PathSegmentNameGenerator instead.', __CLASS__), E_USER_DEPRECATED);
- if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {
- $path = str_replace('.{_format}', '', $resourceShortName);
+ if (func_num_args() >= 4) {
+ $operationName = func_get_arg(3);
} else {
- $path = '/'.Inflector::pluralize($this->dashize($resourceShortName));
- }
-
- if ($operationType === OperationType::ITEM) {
- $path .= '/{id}';
- }
-
- if ($operationType === OperationType::SUBRESOURCE) {
- list($key) = end($operation['identifiers']);
- $property = true === $operation['collection'] ? Inflector::pluralize($this->dashize($operation['property'])) : $this->dashize($operation['property']);
- $path .= sprintf('/{%s}/%s', $key, $property);
+ $operationName = null;
}
- $path .= '.{_format}';
-
- return $path;
- }
-
- private function dashize(string $string): string
- {
- return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $string));
+ return (new OperationPathResolver(new DashPathSegmentNameGenerator()))->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
}
}
diff --git a/src/PathResolver/OperationPathResolver.php b/src/PathResolver/OperationPathResolver.php
new file mode 100644
index 00000000000..668be472c6a
--- /dev/null
+++ b/src/PathResolver/OperationPathResolver.php
@@ -0,0 +1,60 @@
+
+ *
+ * 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\PathResolver;
+
+use ApiPlatform\Core\Api\OperationType;
+use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
+use ApiPlatform\Core\Exception\InvalidArgumentException;
+use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface;
+
+/**
+ * Generates an operation path.
+ *
+ * @author Antoine Bluchet
+ */
+final class OperationPathResolver implements OperationPathResolverInterface
+{
+ private $pathSegmentNameGenerator;
+
+ public function __construct(PathSegmentNameGeneratorInterface $pathSegmentNameGenerator)
+ {
+ $this->pathSegmentNameGenerator = $pathSegmentNameGenerator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
+ {
+ if (func_num_args() < 4) {
+ @trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
+ }
+
+ $operationType = OperationTypeDeprecationHelper::getOperationType($operationType);
+
+ if (OperationType::SUBRESOURCE === $operationType) {
+ throw new InvalidArgumentException('Subresource operations are not supported by the OperationPathResolver.');
+ }
+
+ $path = '/'.$this->pathSegmentNameGenerator->getSegmentName($resourceShortName, true);
+
+ if ($operationType === OperationType::ITEM) {
+ $path .= '/{id}';
+ }
+
+ $path .= '.{_format}';
+
+ return $path;
+ }
+}
diff --git a/src/PathResolver/UnderscoreOperationPathResolver.php b/src/PathResolver/UnderscoreOperationPathResolver.php
index c676cbdf1ef..712be0dc8c6 100644
--- a/src/PathResolver/UnderscoreOperationPathResolver.php
+++ b/src/PathResolver/UnderscoreOperationPathResolver.php
@@ -13,9 +13,7 @@
namespace ApiPlatform\Core\PathResolver;
-use ApiPlatform\Core\Api\OperationType;
-use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
-use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator;
+use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator;
/**
* Generates a path with words separated by underscores.
@@ -29,30 +27,14 @@ final class UnderscoreOperationPathResolver implements OperationPathResolverInte
*/
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
- if (func_num_args() < 4) {
- @trigger_error(sprintf('Method %s() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED);
- }
-
- $operationType = OperationTypeDeprecationHelper::getOperationType($operationType);
+ @trigger_error(sprintf('The use of %s is deprecated since 2.1. Please use PathSegmentNameGenerator instead.', __CLASS__), E_USER_DEPRECATED);
- if ($operationType === OperationType::SUBRESOURCE && 1 < count($operation['identifiers'])) {
- $path = str_replace('.{_format}', '', $resourceShortName);
+ if (func_num_args() >= 4) {
+ $operationName = func_get_arg(3);
} else {
- $path = '/'.RouteNameGenerator::routeNameResolver($resourceShortName, true);
- }
-
- if ($operationType === OperationType::ITEM) {
- $path .= '/{id}';
+ $operationName = null;
}
- if ($operationType === OperationType::SUBRESOURCE) {
- list($key) = end($operation['identifiers']);
- $property = RouteNameGenerator::routeNameResolver($operation['property'], $operation['collection']);
- $path .= sprintf('/{%s}/%s', $key, $property);
- }
-
- $path .= '.{_format}';
-
- return $path;
+ return (new OperationPathResolver(new UnderscorePathSegmentNameGenerator()))->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
}
}
diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php
index 9d1133765db..c0f4f1d5093 100644
--- a/src/Swagger/Serializer/DocumentationNormalizer.php
+++ b/src/Swagger/Serializer/DocumentationNormalizer.php
@@ -25,6 +25,7 @@
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\PropertyInfo\Type;
@@ -58,11 +59,12 @@ final class DocumentationNormalizer implements NormalizerInterface
private $oauthTokenUrl;
private $oauthAuthorizationUrl;
private $oauthScopes;
+ private $subresourceOperationFactory;
/**
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
*/
- public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [])
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null)
{
if ($urlGenerator) {
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
@@ -83,6 +85,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
$this->oauthTokenUrl = $oauthTokenUrl;
$this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
$this->oauthScopes = $oauthScopes;
+ $this->subresourceOperationFactory = $subresourceOperationFactory;
}
/**
@@ -100,6 +103,44 @@ public function normalize($object, $format = null, array $context = [])
$this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
$this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
+
+ if (null === $this->subresourceOperationFactory) {
+ continue;
+ }
+
+ foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
+ $operationName = 'get';
+ $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $resourceMetadata, $operationName);
+ $responseDefinitionKey = $this->getDefinition($definitions, $this->resourceMetadataFactory->create($subresourceOperation['resource_class']), $subresourceOperation['resource_class'], $serializerContext);
+
+ $pathOperation = new \ArrayObject([]);
+ $pathOperation['tags'] = $subresourceOperation['shortNames'];
+ $pathOperation['operationId'] = $operationId;
+ $pathOperation['produces'] = $mimeTypes;
+ $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
+ $pathOperation['responses'] = [
+ '200' => $subresourceOperation['collection'] ? [
+ 'description' => sprintf('%s colletion response', $subresourceOperation['shortNames'][0]),
+ 'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]],
+ ] : [
+ 'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
+ 'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
+ ],
+ '404' => ['description' => 'Resource not found'],
+ ];
+
+ if ($parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) {
+ $pathOperation['parameters'] = $parameters;
+ }
+
+ foreach ($subresourceOperation['identifiers'] as list($identifier, $class, $hasIdentifier)) {
+ if (true === $hasIdentifier) {
+ $pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'type' => 'string'];
+ }
+ }
+
+ $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
+ }
}
$definitions->ksort();
diff --git a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
index f0383f24ac4..fccc7f9244e 100644
--- a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
+++ b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
@@ -19,6 +19,7 @@
/**
* @author Amrouche Hamza
+ * @group legacy
*/
class SwaggerCommandTest extends KernelTestCase
{
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index d7932a1b09d..82fa70c9625 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -341,6 +341,7 @@ private function getContainerBuilderProphecy()
'api_platform.cache.identifiers_extractor',
'api_platform.cache.metadata.resource',
'api_platform.cache.route_name_resolver',
+ 'api_platform.cache.subresource_operation_factory',
'api_platform.collection_data_provider',
'api_platform.doctrine.listener.view.write',
'api_platform.doctrine.metadata_factory',
@@ -438,7 +439,10 @@ private function getContainerBuilderProphecy()
'api_platform.operation_path_resolver.custom',
'api_platform.operation_path_resolver.dash',
'api_platform.operation_path_resolver.router',
+ 'api_platform.operation_path_resolver.generator',
'api_platform.operation_path_resolver.underscore',
+ 'api_platform.path_segment_name_generator.underscore',
+ 'api_platform.path_segment_name_generator.dash',
'api_platform.problem.encoder',
'api_platform.problem.normalizer.constraint_violation_list',
'api_platform.problem.normalizer.error',
@@ -453,6 +457,8 @@ private function getContainerBuilderProphecy()
'api_platform.serializer.group_filter',
'api_platform.serializer.normalizer.item',
'api_platform.subresource_data_provider',
+ 'api_platform.subresource_operation_factory',
+ 'api_platform.subresource_operation_factory.cached',
'api_platform.swagger.action.ui',
'api_platform.swagger.command.swagger_command',
'api_platform.swagger.normalizer.documentation',
@@ -480,6 +486,7 @@ private function getContainerBuilderProphecy()
'api_platform.metadata.resource.name_collection_factory' => 'api_platform.metadata.resource.name_collection_factory.xml',
'api_platform.operation_path_resolver' => 'api_platform.operation_path_resolver.router',
'api_platform.operation_path_resolver.default' => 'api_platform.operation_path_resolver.underscore',
+ 'api_platform.path_segment_name_generator' => 'api_platform.path_segment_name_generator.underscore',
'api_platform.property_accessor' => 'property_accessor',
'api_platform.property_info' => 'property_info',
'api_platform.serializer' => 'serializer',
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index 798f3e37de0..443d541a7d9 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -68,6 +68,7 @@ public function testDefaultConfig()
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
],
'default_operation_path_resolver' => 'api_platform.operation_path_resolver.underscore',
+ 'path_segment_name_generator' => 'api_platform.path_segment_name_generator.underscore',
'name_converter' => null,
'enable_fos_user' => false,
'enable_nelmio_api_doc' => false,
diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php
index d3d7e136895..d4781fcb338 100644
--- a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php
+++ b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php
@@ -23,8 +23,10 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory;
+use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator;
use ApiPlatform\Core\PathResolver\CustomOperationPathResolver;
-use ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver;
+use ApiPlatform\Core\PathResolver\OperationPathResolver;
use ApiPlatform\Core\Tests\Fixtures\DummyEntity;
use ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity;
use Prophecy\Argument;
@@ -89,7 +91,7 @@ public function testApiLoader()
);
$this->assertEquals(
- $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class]], 'collection' => true]),
+ $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true]], 'collection' => true, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\DummyEntity-subresource-ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity']),
$routeCollection->get('api_dummies_subresources_get_subresource')
);
}
@@ -158,32 +160,32 @@ public function testRecursiveSubresource()
$routeCollection = $this->getApiLoaderWithResourceMetadata($resourceMetadata, true)->load(null);
$this->assertEquals(
- $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class]], 'collection' => true]),
+ $this->getSubresourceRoute('/dummies/{id}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_dummies_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', DummyEntity::class, true]], 'collection' => true, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\DummyEntity-subresource-ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity']),
$routeCollection->get('api_dummies_subresources_get_subresource')
);
$this->assertEquals(
- $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource/{recursivesubresource}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_recursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class], ['recursivesubresource', DummyEntity::class]], 'collection' => true]),
+ $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_recursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true], ['recursivesubresource', DummyEntity::class, false]], 'collection' => true, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity-recursivesubresource-ApiPlatform\Core\Tests\Fixtures\DummyEntity-subresource-ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity']),
$routeCollection->get('api_related_dummies_recursivesubresource_subresources_get_subresource')
);
$this->assertEquals(
- $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class]], 'collection' => false]),
+ $this->getSubresourceRoute('/related_dummies/{id}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity-recursivesubresource-ApiPlatform\Core\Tests\Fixtures\DummyEntity']),
$routeCollection->get('api_related_dummies_recursivesubresource_get_subresource')
);
$this->assertEquals(
- $this->getSubresourceRoute('/dummies/{id}/subresources/{subresource}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_dummies_subresources_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', DummyEntity::class], ['subresource', RelatedDummyEntity::class]], 'collection' => false]),
+ $this->getSubresourceRoute('/dummies/{id}/subresources/{subresource}/recursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_dummies_subresources_recursivesubresource_get_subresource', ['property' => 'recursivesubresource', 'identifiers' => [['id', DummyEntity::class, true], ['subresource', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\DummyEntity-subresource-ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity-recursivesubresource-ApiPlatform\Core\Tests\Fixtures\DummyEntity']),
$routeCollection->get('api_dummies_subresources_recursivesubresource_get_subresource')
);
$this->assertEquals(
- $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource/{secondrecursivesubresource}/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class], ['secondrecursivesubresource', DummyEntity::class]], 'collection' => true]),
+ $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource/subresources.{_format}', 'api_platform.action.get_subresource', RelatedDummyEntity::class, 'api_related_dummies_secondrecursivesubresource_subresources_get_subresource', ['property' => 'subresource', 'identifiers' => [['id', RelatedDummyEntity::class, true], ['secondrecursivesubresource', DummyEntity::class, false]], 'collection' => true, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity-secondrecursivesubresource-ApiPlatform\Core\Tests\Fixtures\DummyEntity-subresource-ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity']),
$routeCollection->get('api_related_dummies_secondrecursivesubresource_subresources_get_subresource')
);
$this->assertEquals(
- $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_secondrecursivesubresource_get_subresource', ['property' => 'secondrecursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class]], 'collection' => false]),
+ $this->getSubresourceRoute('/related_dummies/{id}/secondrecursivesubresource.{_format}', 'api_platform.action.get_subresource', DummyEntity::class, 'api_related_dummies_secondrecursivesubresource_get_subresource', ['property' => 'secondrecursivesubresource', 'identifiers' => [['id', RelatedDummyEntity::class, true]], 'collection' => false, 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity-secondrecursivesubresource-ApiPlatform\Core\Tests\Fixtures\DummyEntity']),
$routeCollection->get('api_related_dummies_secondrecursivesubresource_get_subresource')
);
}
@@ -248,9 +250,13 @@ private function getApiLoaderWithResourceMetadata(ResourceMetadata $resourceMeta
$propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subresource')->willReturn($subResourcePropertyMetadata);
- $operationPathResolver = new CustomOperationPathResolver(new UnderscoreOperationPathResolver());
+ $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
- $apiLoader = new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal());
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+
+ $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), new UnderscorePathSegmentNameGenerator());
+
+ $apiLoader = new ApiLoader($kernelProphecy->reveal(), $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $operationPathResolver, $containerProphecy->reveal(), ['jsonld' => ['application/ld+json']], [], $subresourceOperationFactory);
return $apiLoader;
}
diff --git a/tests/Bridge/Symfony/Routing/RouteNameGeneratorTest.php b/tests/Bridge/Symfony/Routing/RouteNameGeneratorTest.php
index 81ac0a1280a..3b866de99b4 100644
--- a/tests/Bridge/Symfony/Routing/RouteNameGeneratorTest.php
+++ b/tests/Bridge/Symfony/Routing/RouteNameGeneratorTest.php
@@ -36,13 +36,12 @@ public function testLegacyGenerate()
$this->assertEquals('api_foos_get_collection', RouteNameGenerator::generate('get', 'Foo', true));
}
+ /**
+ * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
+ * @expectedMessage Subresource operations are not supported by the RouteNameGenerator.
+ */
public function testGenerateWithSubresource()
{
- $this->assertEquals('api_foos_bar_get_subresource', RouteNameGenerator::generate('get', 'Foo', OperationType::SUBRESOURCE, ['property' => 'bar', 'collection' => false]));
- }
-
- public function testGenerateWithSubresourceCollection()
- {
- $this->assertEquals('api_foos_bars_get_subresource', RouteNameGenerator::generate('get', 'Foo', OperationType::SUBRESOURCE, ['property' => 'bar', 'collection' => true]));
+ $this->assertEquals('api_foos_bar_get_subresource', RouteNameGenerator::generate('get', 'Foo', OperationType::SUBRESOURCE));
}
}
diff --git a/tests/Bridge/Symfony/Routing/RouterOperationPathResolverTest.php b/tests/Bridge/Symfony/Routing/RouterOperationPathResolverTest.php
index d687b92ae76..60e5f8999b8 100644
--- a/tests/Bridge/Symfony/Routing/RouterOperationPathResolverTest.php
+++ b/tests/Bridge/Symfony/Routing/RouterOperationPathResolverTest.php
@@ -40,17 +40,17 @@ public function testResolveOperationPath()
$this->assertEquals('/foos', $operationPathResolver->resolveOperationPath('Foo', ['route_name' => 'foos'], OperationType::COLLECTION, 'get'));
}
+ /**
+ * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
+ * @expectedMessage Subresource operations are not supported by the RouterOperationPathResolver.
+ */
public function testResolveOperationPathWithSubresource()
{
- $routeCollection = new RouteCollection();
- $routeCollection->add('api_foos_bars_get_subresource', new Route('/foos/{id}/bars'));
-
$routerProphecy = $this->prophesize(RouterInterface::class);
- $routerProphecy->getRouteCollection()->willReturn($routeCollection)->shouldBeCalled();
$operationPathResolver = new RouterOperationPathResolver($routerProphecy->reveal(), $this->prophesize(OperationPathResolverInterface::class)->reveal());
- $this->assertEquals('/foos/{id}/bars', $operationPathResolver->resolveOperationPath('Foo', ['property' => 'bar', 'collection' => true], OperationType::SUBRESOURCE, 'get'));
+ $operationPathResolver->resolveOperationPath('Foo', ['property' => 'bar', 'collection' => true, 'resource_class' => 'Foo'], OperationType::SUBRESOURCE, 'get');
}
public function testResolveOperationPathWithRouteNameGeneration()
diff --git a/tests/EventListener/ReadListenerTest.php b/tests/EventListener/ReadListenerTest.php
index e38abf3b9b0..e647db165c8 100644
--- a/tests/EventListener/ReadListenerTest.php
+++ b/tests/EventListener/ReadListenerTest.php
@@ -145,9 +145,9 @@ public function testRetrieveSubresource()
$data = [new \stdClass()];
$subresourceDataProvider = $this->prophesize(SubresourceDataProviderInterface::class);
- $subresourceDataProvider->getSubresource('Foo', ['id' => 1], ['identifiers' => [['id', 'Bar']], 'property' => 'bar'], 'get')->willReturn($data)->shouldBeCalled();
+ $subresourceDataProvider->getSubresource('Foo', ['id' => 1], ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar'], 'get')->willReturn($data)->shouldBeCalled();
- $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar']], 'property' => 'bar']]);
+ $request = new Request([], [], ['id' => 1, '_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'json', '_api_mime_type' => 'application/json', '_api_subresource_context' => ['identifiers' => [['id', 'Bar', true]], 'property' => 'bar']]);
$request->setMethod(Request::METHOD_GET);
$event = $this->prophesize(GetResponseEvent::class);
diff --git a/tests/Fixtures/TestBundle/Entity/Answer.php b/tests/Fixtures/TestBundle/Entity/Answer.php
index f850310bc74..b68da8ff8f7 100644
--- a/tests/Fixtures/TestBundle/Entity/Answer.php
+++ b/tests/Fixtures/TestBundle/Entity/Answer.php
@@ -14,6 +14,8 @@
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Annotation\ApiSubresource;
+use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
@@ -47,6 +49,18 @@ class Answer
*/
private $question;
+ /**
+ * @ORM\OneToMany(targetEntity="Question", mappedBy="answer")
+ * @Serializer\Groups({"foobar"})
+ * @ApiSubresource
+ */
+ private $relatedQuestions;
+
+ public function __construct()
+ {
+ $this->relatedQuestions = new ArrayCollection();
+ }
+
/**
* Get id.
*
@@ -104,4 +118,19 @@ public function getQuestion()
{
return $this->question;
}
+
+ /**
+ * Get related question.
+ *
+ * @return ArrayCollection
+ */
+ public function getRelatedQuestions()
+ {
+ return $this->relatedQuestions;
+ }
+
+ public function addRelatedQuestion(Question $question)
+ {
+ $this->relatedQuestions->add($question);
+ }
}
diff --git a/tests/Operation/Factory/CachedSubresourceOperationFactoryTest.php b/tests/Operation/Factory/CachedSubresourceOperationFactoryTest.php
new file mode 100644
index 00000000000..c69ebfff902
--- /dev/null
+++ b/tests/Operation/Factory/CachedSubresourceOperationFactoryTest.php
@@ -0,0 +1,86 @@
+
+ *
+ * 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\Operation\Factory;
+
+use ApiPlatform\Core\Operation\Factory\CachedSubresourceOperationFactory;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use Psr\Cache\CacheException;
+use Psr\Cache\CacheItemInterface;
+use Psr\Cache\CacheItemPoolInterface;
+
+/**
+ * @author Antoine Bluchet
+ */
+class CachedSubresourceOperationFactoryTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreateWithItemHit()
+ {
+ $cacheItem = $this->prophesize(CacheItemInterface::class);
+ $cacheItem->isHit()->willReturn(true)->shouldBeCalled();
+ $cacheItem->get()->willReturn(['foo' => 'bar'])->shouldBeCalled();
+
+ $cacheItemPool = $this->prophesize(CacheItemPoolInterface::class);
+ $cacheItemPool->getItem($this->generateCacheKey())->willReturn($cacheItem->reveal())->shouldBeCalled();
+
+ $decoratedSubresourceOperationFactory = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $decoratedSubresourceOperationFactory->create()->shouldNotBeCalled();
+
+ $cachedSubresourceOperationFactory = new CachedSubresourceOperationFactory($cacheItemPool->reveal(), $decoratedSubresourceOperationFactory->reveal());
+ $resultedSubresourceOperation = $cachedSubresourceOperationFactory->create(Dummy::class);
+
+ $this->assertEquals(['foo' => 'bar'], $resultedSubresourceOperation);
+ }
+
+ public function testCreateWithItemNotHit()
+ {
+ $cacheItem = $this->prophesize(CacheItemInterface::class);
+ $cacheItem->isHit()->willReturn(false)->shouldBeCalled();
+ $cacheItem->set(['foo' => 'bar'])->willReturn($cacheItem->reveal())->shouldBeCalled();
+
+ $cacheItemPool = $this->prophesize(CacheItemPoolInterface::class);
+ $cacheItemPool->getItem($this->generateCacheKey())->willReturn($cacheItem->reveal())->shouldBeCalled();
+ $cacheItemPool->save($cacheItem->reveal())->willReturn(true)->shouldBeCalled();
+
+ $decoratedSubresourceOperationFactory = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $decoratedSubresourceOperationFactory->create(Dummy::class)->shouldBeCalled()->willReturn(['foo' => 'bar']);
+
+ $cachedSubresourceOperationFactory = new CachedSubresourceOperationFactory($cacheItemPool->reveal(), $decoratedSubresourceOperationFactory->reveal());
+ $resultedSubresourceOperation = $cachedSubresourceOperationFactory->create(Dummy::class);
+
+ $this->assertEquals(['foo' => 'bar'], $resultedSubresourceOperation);
+ }
+
+ public function testCreateWithGetCacheItemThrowsCacheException()
+ {
+ $cacheException = $this->prophesize(CacheException::class);
+ $cacheException->willExtend(\Exception::class);
+
+ $cacheItemPool = $this->prophesize(CacheItemPoolInterface::class);
+ $cacheItemPool->getItem($this->generateCacheKey())->willThrow($cacheException->reveal())->shouldBeCalled();
+
+ $decoratedSubresourceOperationFactory = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $decoratedSubresourceOperationFactory->create(Dummy::class)->shouldBeCalled()->willReturn(['foo' => 'bar']);
+
+ $cachedSubresourceOperationFactory = new CachedSubresourceOperationFactory($cacheItemPool->reveal(), $decoratedSubresourceOperationFactory->reveal());
+ $resultedSubresourceOperation = $cachedSubresourceOperationFactory->create(Dummy::class);
+
+ $this->assertEquals(['foo' => 'bar'], $resultedSubresourceOperation);
+ }
+
+ private function generateCacheKey(string $resourceClass = Dummy::class)
+ {
+ return CachedSubresourceOperationFactory::CACHE_KEY_PREFIX.str_replace('\\', '', $resourceClass);
+ }
+}
diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php
new file mode 100644
index 00000000000..8aa364c4418
--- /dev/null
+++ b/tests/Operation/Factory/SubresourceOperationFactoryTest.php
@@ -0,0 +1,221 @@
+
+ *
+ * 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\Operation\Factory;
+
+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\Property\SubresourceMetadata;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory;
+use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface;
+use ApiPlatform\Core\Tests\Fixtures\DummyEntity;
+use ApiPlatform\Core\Tests\Fixtures\RelatedDummyEntity;
+
+/**
+ * @author Antoine Bluchet
+ */
+class SubresourceOperationFactoryTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreate()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('relatedDummyEntity'));
+ $resourceMetadataFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new ResourceMetadata('dummyEntity'));
+
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(DummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['foo', 'subresource', 'subcollection']));
+ $propertyNameCollectionFactoryProphecy->create(RelatedDummyEntity::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['bar', 'anotherSubresource']));
+
+ $subresourceMetadata = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(RelatedDummyEntity::class));
+ $subresourceMetadataCollection = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(RelatedDummyEntity::class, true));
+ $anotherSubresourceMetadata = (new PropertyMetadata())->withSubresource(new SubresourceMetadata(DummyEntity::class, false));
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'foo')->shouldBeCalled()->willReturn(new PropertyMetadata());
+ $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subresource')->shouldBeCalled()->willReturn($subresourceMetadata);
+ $propertyMetadataFactoryProphecy->create(DummyEntity::class, 'subcollection')->shouldBeCalled()->willReturn($subresourceMetadataCollection);
+ $propertyMetadataFactoryProphecy->create(RelatedDummyEntity::class, 'bar')->shouldBeCalled()->willReturn(new PropertyMetadata());
+ $propertyMetadataFactoryProphecy->create(RelatedDummyEntity::class, 'anotherSubresource')->shouldBeCalled()->willReturn($anotherSubresourceMetadata);
+
+ $pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class);
+ $pathSegmentNameGeneratorProphecy->getSegmentName('subresource', false)->shouldBeCalled()->willReturn('subresource');
+ $pathSegmentNameGeneratorProphecy->getSegmentName('subcollection', true)->shouldBeCalled()->willReturn('subcollections');
+ $pathSegmentNameGeneratorProphecy->getSegmentName('dummyEntity', true)->shouldBeCalled()->willReturn('dummy_entities');
+ $pathSegmentNameGeneratorProphecy->getSegmentName('anotherSubresource', false)->shouldBeCalled()->willReturn('another_subresource');
+
+ $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $pathSegmentNameGeneratorProphecy->reveal());
+
+ $this->assertEquals([
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subresource',
+ 'collection' => false,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity' => [
+ 'property' => 'anotherSubresource',
+ 'collection' => false,
+ 'resource_class' => DummyEntity::class,
+ 'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subresource', RelatedDummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_another_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource/another_subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subresource',
+ 'collection' => false,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subresource', RelatedDummyEntity::class, false],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_another_subresource_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource/another_subresource/subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subcollection',
+ 'collection' => true,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subresource', RelatedDummyEntity::class, false],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity' => [
+ 'property' => 'anotherSubresource',
+ 'collection' => false,
+ 'resource_class' => DummyEntity::class,
+ 'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subresource', RelatedDummyEntity::class, false],
+ ['anotherSubresource', DummyEntity::class, false],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_another_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections/{subcollection}/another_subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subresource',
+ 'collection' => false,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subresource', RelatedDummyEntity::class, false],
+ ['anotherSubresource', DummyEntity::class, false],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subresource_another_subresource_subcollections_another_subresource_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subresource/another_subresource/subcollections/{subcollection}/another_subresource/subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subcollection',
+ 'collection' => true,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity' => [
+ 'property' => 'anotherSubresource',
+ 'collection' => false,
+ 'resource_class' => DummyEntity::class,
+ 'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_another_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subresource',
+ 'collection' => false,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity' => [
+ 'property' => 'anotherSubresource',
+ 'collection' => false,
+ 'resource_class' => DummyEntity::class,
+ 'shortNames' => ['dummyEntity', 'relatedDummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ['anotherSubresource', DummyEntity::class, false],
+ ['subresource', RelatedDummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_another_subresource_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subresource/another_subresource.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subresource-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subcollection',
+ 'collection' => true,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ['anotherSubresource', DummyEntity::class, false],
+ ['subresource', RelatedDummyEntity::class, false],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subresource_another_subresource_subcollections_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subresource/another_subresource/subcollections.{_format}',
+ ],
+ 'ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity-anotherSubresource-ApiPlatform\\Core\\Tests\\Fixtures\\DummyEntity-subcollection-ApiPlatform\\Core\\Tests\\Fixtures\\RelatedDummyEntity' => [
+ 'property' => 'subcollection',
+ 'collection' => true,
+ 'resource_class' => RelatedDummyEntity::class,
+ 'shortNames' => ['relatedDummyEntity', 'dummyEntity'],
+ 'identifiers' => [
+ ['id', DummyEntity::class, true],
+ ['subcollection', RelatedDummyEntity::class, true],
+ ['anotherSubresource', DummyEntity::class, false],
+ ],
+ 'route_name' => 'api_dummy_entities_subcollections_another_subresource_subcollections_get_subresource',
+ 'path' => '/dummy_entities/{id}/subcollections/{subcollection}/another_subresource/subcollections.{_format}',
+ ],
+ ], $subresourceOperationFactory->create(DummyEntity::class));
+ }
+}
diff --git a/tests/PathResolver/CustomOperationPathResolverTest.php b/tests/PathResolver/CustomOperationPathResolverTest.php
index 12ee5e08f11..5d0fdaa8b9f 100644
--- a/tests/PathResolver/CustomOperationPathResolverTest.php
+++ b/tests/PathResolver/CustomOperationPathResolverTest.php
@@ -41,7 +41,6 @@ public function testResolveOperationPathWithDeferred()
/**
* @group legacy
- * @expectedDeprecation Method ApiPlatform\Core\PathResolver\CustomOperationPathResolver::resolveOperationPath() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.
* @expectedDeprecation Using a boolean for the Operation Type is deprecrated since API Platform 2.1 and will not be possible anymore in API Platform 3
*/
public function testLegacyResolveOperationPath()
diff --git a/tests/PathResolver/DashOperationPathResolverTest.php b/tests/PathResolver/DashOperationPathResolverTest.php
index 7fd7effb073..c746b7f1b19 100644
--- a/tests/PathResolver/DashOperationPathResolverTest.php
+++ b/tests/PathResolver/DashOperationPathResolverTest.php
@@ -18,6 +18,7 @@
/**
* @author Guilhem N.
+ * @group legacy
*/
class DashOperationPathResolverTest extends \PHPUnit_Framework_TestCase
{
@@ -35,22 +36,18 @@ public function testResolveItemOperationPath()
$this->assertSame('/short-names/{id}.{_format}', $dashOperationPathResolver->resolveOperationPath('ShortName', [], OperationType::ITEM, 'get'));
}
+ /**
+ * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
+ * @expectedMessage Subresource operations are not supported by the OperationPathResolver.
+ */
public function testResolveSubresourceOperationPath()
{
$dashOperationPathResolver = new DashOperationPathResolver();
- $path = $dashOperationPathResolver->resolveOperationPath('ShortName', ['property' => 'relatedFoo', 'identifiers' => [['id', 'class']], 'collection' => true], OperationType::SUBRESOURCE, 'get');
-
- $this->assertSame('/short-names/{id}/related-foos.{_format}', $path);
-
- $next = $dashOperationPathResolver->resolveOperationPath($path, ['property' => 'bar', 'identifiers' => [['id', 'class'], ['relatedId', 'class']], 'collection' => false], OperationType::SUBRESOURCE, 'get');
-
- $this->assertSame('/short-names/{id}/related-foos/{relatedId}/bar.{_format}', $next);
+ $dashOperationPathResolver->resolveOperationPath('ShortName', ['property' => 'bar', 'identifiers' => [['id', 'class'], ['relatedId', 'class']], 'collection' => false], OperationType::SUBRESOURCE, 'get');
}
/**
- * @group legacy
- * @expectedDeprecation Method ApiPlatform\Core\PathResolver\DashOperationPathResolver::resolveOperationPath() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.
* @expectedDeprecation Using a boolean for the Operation Type is deprecrated since API Platform 2.1 and will not be possible anymore in API Platform 3
*/
public function testLegacyResolveOperationPath()
diff --git a/tests/PathResolver/UnderscoreOperationPathResolverTest.php b/tests/PathResolver/UnderscoreOperationPathResolverTest.php
index 12475b70af5..c46d6d1a061 100644
--- a/tests/PathResolver/UnderscoreOperationPathResolverTest.php
+++ b/tests/PathResolver/UnderscoreOperationPathResolverTest.php
@@ -18,6 +18,7 @@
/**
* @author Guilhem N.
+ * @group legacy
*/
class UnderscoreOperationPathResolverTest extends \PHPUnit_Framework_TestCase
{
@@ -35,22 +36,18 @@ public function testResolveItemOperationPath()
$this->assertSame('/short_names/{id}.{_format}', $underscoreOperationPathResolver->resolveOperationPath('ShortName', [], OperationType::ITEM, 'get'));
}
+ /**
+ * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
+ * @expectedMessage Subresource operations are not supported by the OperationPathResolver.
+ */
public function testResolveSubresourceOperationPath()
{
$dashOperationPathResolver = new UnderscoreOperationPathResolver();
- $path = $dashOperationPathResolver->resolveOperationPath('ShortName', ['property' => 'relatedFoo', 'identifiers' => [['id', 'class']], 'collection' => true], OperationType::SUBRESOURCE, 'get');
-
- $this->assertSame('/short_names/{id}/related_foos.{_format}', $path);
-
- $next = $dashOperationPathResolver->resolveOperationPath($path, ['property' => 'bar', 'identifiers' => [['id', 'class'], ['relatedId', 'class']], 'collection' => false], OperationType::SUBRESOURCE, 'get');
-
- $this->assertSame('/short_names/{id}/related_foos/{relatedId}/bar.{_format}', $next);
+ $dashOperationPathResolver->resolveOperationPath('ShortName', ['property' => 'relatedFoo', 'identifiers' => [['id', 'class']], 'collection' => true], OperationType::SUBRESOURCE, 'get');
}
/**
- * @group legacy
- * @expectedDeprecation Method ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver::resolveOperationPath() will have a 4th `string $operationName` argument in version 3.0. Not defining it is deprecated since 2.1.
* @expectedDeprecation Using a boolean for the Operation Type is deprecrated since API Platform 2.1 and will not be possible anymore in API Platform 3
*/
public function testLegacyResolveOperationPath()
diff --git a/tests/Swagger/Serializer/DocumentationNormalizerTest.php b/tests/Swagger/Serializer/DocumentationNormalizerTest.php
index ba2f398fad1..60224b7c093 100644
--- a/tests/Swagger/Serializer/DocumentationNormalizerTest.php
+++ b/tests/Swagger/Serializer/DocumentationNormalizerTest.php
@@ -17,23 +17,34 @@
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Api\UrlGeneratorInterface;
+use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver;
use ApiPlatform\Core\Documentation\Documentation;
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\Property\SubresourceMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory;
+use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator;
use ApiPlatform\Core\PathResolver\CustomOperationPathResolver;
+use ApiPlatform\Core\PathResolver\OperationPathResolver;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
use ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver;
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
use ApiPlatform\Core\Tests\Fixtures\DummyFilter;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use Prophecy\Argument;
use Psr\Container\ContainerInterface;
use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
@@ -61,6 +72,10 @@ public function testLegacyConstruct()
$this->assertInstanceOf(DocumentationNormalizer::class, $normalizer);
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalize()
{
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3', ['jsonld' => ['application/ld+json']]);
@@ -262,6 +277,10 @@ public function testNormalize()
$this->assertEquals($expected, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['base_url' => '/app_dev.php/']));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalizeWithNameConverter()
{
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Dummy API', 'This is a dummy API', '1.2.3', ['jsonld' => ['application/ld+json']]);
@@ -373,6 +392,10 @@ public function testNormalizeWithNameConverter()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalizeWithOnlyNormalizationGroups()
{
$title = 'Test API';
@@ -558,6 +581,10 @@ public function testNormalizeWithOnlyNormalizationGroups()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalizeWithOnlyDenormalizationGroups()
{
$title = 'Test API';
@@ -740,6 +767,10 @@ public function testNormalizeWithOnlyDenormalizationGroups()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalizeWithNormalizationAndDenormalizationGroups()
{
$title = 'Test API';
@@ -925,6 +956,10 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testFilters()
{
$filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
@@ -1072,6 +1107,10 @@ public function testNoOperations()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testWithCustomMethod()
{
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), '', '', '0.0.0', ['jsonld' => ['application/ld+json']]);
@@ -1128,6 +1167,10 @@ public function testWithCustomMethod()
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
+ */
public function testNormalizeWithNestedNormalizationGroups()
{
$title = 'Test API';
@@ -1441,4 +1484,143 @@ private function normalizeWithFilters($filterLocator)
$this->assertEquals($expected, $normalizer->normalize($documentation));
}
+
+ public function testNormalizeWithSubResource()
+ {
+ $documentation = new Documentation(new ResourceNameCollection([Question::class]), 'Test API', 'This is a test API.', '1.2.3', ['jsonld' => ['application/ld+json']]);
+
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(Question::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['answer']));
+ $propertyNameCollectionFactoryProphecy->create(Answer::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['content']));
+
+ $questionMetadata = new ResourceMetadata('Question', 'This is a question.', 'http://schema.example.com/Question', ['get' => ['method' => 'GET']]);
+ $answerMetadata = new ResourceMetadata('Answer', 'This is an answer.', 'http://schema.example.com/Answer', [], ['get' => ['method' => 'GET']]);
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Question::class)->shouldBeCalled()->willReturn($questionMetadata);
+ $resourceMetadataFactoryProphecy->create(Answer::class)->shouldBeCalled()->willReturn($answerMetadata);
+
+ $subresourceMetadata = new SubresourceMetadata(Answer::class, false);
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Question::class, 'answer')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, false, Question::class, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Answer::class)), 'This is a name.', true, true, true, true, false, false, null, null, [], $subresourceMetadata));
+
+ $propertyMetadataFactoryProphecy->create(Answer::class, 'content')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, false, Question::class, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Answer::class)), 'This is a name.', true, true, true, true, false, false, null, null, []));
+ $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
+ $resourceClassResolverProphecy->isResourceClass(Question::class)->willReturn(true);
+ $resourceClassResolverProphecy->isResourceClass(Answer::class)->willReturn(true);
+
+ $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class);
+ $operationMethodResolverProphecy->getItemOperationMethod(Question::class, 'get')->shouldBeCalled()->willReturn('GET');
+
+ $routeCollection = new RouteCollection();
+ $routeCollection->add('api_questions_answer_get_subresource', new Route('/api/questions/{id}/answer.{_format}'));
+ $routeCollection->add('api_questions_get_item', new Route('/api/questions/{id}.{_format}'));
+
+ $routerProphecy = $this->prophesize(RouterInterface::class);
+ $routerProphecy->getRouteCollection()->shouldBeCalled()->willReturn($routeCollection);
+
+ $operationPathResolver = new RouterOperationPathResolver($routerProphecy->reveal(), new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())));
+
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+ $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
+ $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+
+ $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator());
+
+ $normalizer = new DocumentationNormalizer(
+ $resourceMetadataFactory,
+ $propertyNameCollectionFactory,
+ $propertyMetadataFactory,
+ $resourceClassResolverProphecy->reveal(),
+ $operationMethodResolverProphecy->reveal(),
+ $operationPathResolver,
+ null, null, null, false, '', '', '', '', [],
+ $subresourceOperationFactory
+ );
+
+ $expected = [
+ 'swagger' => '2.0',
+ 'basePath' => '/',
+ 'info' => [
+ 'title' => 'Test API',
+ 'description' => 'This is a test API.',
+ 'version' => '1.2.3',
+ ],
+ 'paths' => new \ArrayObject([
+ '/api/questions/{id}' => [
+ 'get' => new \ArrayObject([
+ 'tags' => ['Question'],
+ 'operationId' => 'getQuestionItem',
+ 'produces' => ['application/ld+json'],
+ 'summary' => 'Retrieves a Question resource.',
+ 'parameters' => [
+ [
+ 'name' => 'id',
+ 'in' => 'path',
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ ],
+ 'responses' => [
+ 200 => [
+ 'description' => 'Question resource response',
+ 'schema' => ['$ref' => '#/definitions/Question'],
+ ],
+ 404 => ['description' => 'Resource not found'],
+ ],
+ ]),
+ ],
+ '/api/questions/{id}/answer' => new \ArrayObject([
+ 'get' => new \ArrayObject([
+ 'tags' => ['Answer', 'Question'],
+ 'operationId' => 'ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question-answer-ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer',
+ 'produces' => ['application/ld+json'],
+ 'summary' => 'Retrieves a Answer resource.',
+ 'responses' => [
+ 200 => [
+ 'description' => 'Answer resource response',
+ 'schema' => ['$ref' => '#/definitions/Answer'],
+ ],
+ 404 => ['description' => 'Resource not found'],
+ ],
+ 'parameters' => [
+ [
+ 'name' => 'id',
+ 'in' => 'path',
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ ],
+ ]),
+ ]),
+ ]),
+ 'definitions' => new \ArrayObject([
+ 'Question' => new \ArrayObject([
+ 'type' => 'object',
+ 'description' => 'This is a question.',
+ 'externalDocs' => ['url' => 'http://schema.example.com/Question'],
+ 'properties' => [
+ 'answer' => new \ArrayObject([
+ 'type' => 'array',
+ 'description' => 'This is a name.',
+ 'items' => ['$ref' => '#/definitions/Answer'],
+ ]),
+ ],
+ ]),
+ 'Answer' => new \ArrayObject([
+ 'type' => 'object',
+ 'description' => 'This is an answer.',
+ 'externalDocs' => ['url' => 'http://schema.example.com/Answer'],
+ 'properties' => [
+ 'content' => new \ArrayObject([
+ 'type' => 'array',
+ 'description' => 'This is a name.',
+ 'items' => ['$ref' => '#/definitions/Answer'],
+ ]),
+ ],
+ ]),
+ ]),
+ ];
+
+ $this->assertEquals($expected, $normalizer->normalize($documentation));
+ }
}