diff --git a/composer.json b/composer.json index a2fda85bbe8..a52e23e103e 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,12 @@ "doctrine/inflector": "^1.0", "psr/cache": "^1.0", - "symfony/http-foundation": "^2.7 || ^3.0", - "symfony/http-kernel": "^2.7 || ^3.0", + "psr/container": "^1.0", + "symfony/http-foundation": "^3.3@beta", + "symfony/http-kernel": "^3.3@beta", "symfony/property-access": "^2.7 || ^3.0", - "symfony/property-info": "^3.1", - "symfony/serializer": "^3.1", + "symfony/property-info": "^3.3@beta", + "symfony/serializer": "^3.3@beta", "willdurand/negotiation": "^2.0.3" }, "require-dev": { @@ -42,18 +43,23 @@ "phpunit/phpunit": "^5.6.8", "psr/log": "^1.0", "sensio/framework-extra-bundle": "^3.0", - "symfony/asset": "^2.7 || ^3.0", - "symfony/cache": "^3.1", - "symfony/config": "^3.2", - "symfony/dependency-injection": "^2.7 || ^3.0", + "symfony/asset": "^3.3@beta", + "symfony/cache": "^3.3@beta", + "symfony/config": "^3.3@beta", + "symfony/console": "^3.3@beta", + "symfony/dependency-injection": "^3.3@beta", "symfony/doctrine-bridge": "^2.8 || ^3.0", + "symfony/event-dispatcher": "^3.3@beta", "symfony/expression-language": "^2.8 || ^3.0", - "symfony/finder": "^2.7 || ^3.0", - "symfony/framework-bundle": "^3.1", + "symfony/finder": "^3.3@beta", + "symfony/form": "^3.3@beta", + "symfony/framework-bundle": "^3.3@beta", "symfony/phpunit-bridge": "^2.7 || ^3.0", + "symfony/routing": "^3.3@beta", "symfony/security": "^2.7 || ^3.0", "symfony/twig-bundle": "^2.8 || ^3.1", - "symfony/validator": "^2.7 || ^3.0" + "symfony/validator": "^3.3@beta", + "symfony/yaml": "^3.3@beta" }, "suggest": { "friendsofsymfony/user-bundle": "To use the FOSUserBundle bridge.", diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature index 7489a90ebf7..2eb5b993aeb 100644 --- a/features/doctrine/date_filter.feature +++ b/features/doctrine/date_filter.feature @@ -406,73 +406,67 @@ Feature: Date filter on collections }, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "\/dummies{?id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,order[id],order[name],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],dummyBoolean,dummyFloat,dummyPrice,description[exists],relatedDummy.name[exists],dummyBoolean[exists]}", + "hydra:template": "/dummies{?dummyBoolean,dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],dummyFloat,dummyPrice,order[id],order[name],order[relatedDummy.symfony],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { "@type": "IriTemplateMapping", - "variable": "id", - "property": "id", + "variable": "dummyBoolean", + "property": "dummyBoolean", "required": false }, { "@type": "IriTemplateMapping", - "variable": "id[]", - "property": "id", + "variable": "dummyDate[before]", + "property": "dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "name", - "property": "name", + "variable": "dummyDate[after]", + "property": "dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "alias", - "property": "alias", + "variable": "relatedDummy.dummyDate[before]", + "property": "relatedDummy.dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "description", - "property": "description", + "variable": "relatedDummy.dummyDate[after]", + "property": "relatedDummy.dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name", - "property": "relatedDummy.name", + "variable": "description[exists]", + "property": "description", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[]", + "variable": "relatedDummy.name[exists]", "property": "relatedDummy.name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummies", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies[]", - "property": "relatedDummies", + "variable": "dummyBoolean[exists]", + "property": "dummyBoolean", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummy", - "property": "dummy", + "variable": "dummyFloat", + "property": "dummyFloat", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummies.name", - "property": "relatedDummies.name", + "variable": "dummyPrice", + "property": "dummyPrice", "required": false }, { @@ -493,30 +487,6 @@ Feature: Date filter on collections "property": "relatedDummy.symfony", "required": false }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[after]", - "property": "relatedDummy.dummyDate", - "required": false - }, { "@type": "IriTemplateMapping", "variable": "dummyFloat[between]", @@ -579,38 +549,68 @@ Feature: Date filter on collections }, { "@type": "IriTemplateMapping", - "variable": "dummyBoolean", - "property": "dummyBoolean", + "variable": "id", + "property": "id", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyFloat", - "property": "dummyFloat", + "variable": "id[]", + "property": "id", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyPrice", - "property": "dummyPrice", + "variable": "name", + "property": "name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "description[exists]", + "variable": "alias", + "property": "alias", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "description", "property": "description", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[exists]", + "variable": "relatedDummy.name", "property": "relatedDummy.name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyBoolean[exists]", - "property": "dummyBoolean", + "variable": "relatedDummy.name[]", + "property": "relatedDummy.name", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies", + "property": "relatedDummies", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies[]", + "property": "relatedDummies", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "dummy", + "property": "dummy", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies.name", + "property": "relatedDummies.name", "required": false } ] diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index 991ddf405b8..0b416b441b5 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -235,12 +235,12 @@ Feature: Collections support And the JSON should be valid according to this schema: """ { - "@id":"\/dummies?page=3", + "@id":"/dummies?page=3", "@type":"hydra:PartialCollectionView", - "hydra:first":"\/dummies?page=1", - "hydra:last":"\/dummies?page=10", - "hydra:previous":"\/dummies?page=2", - "hydra:next":"\/dummies?page=4" + "hydra:first":"/dummies?page=1", + "hydra:last":"/dummies?page=10", + "hydra:previous":"/dummies?page=2", + "hydra:next":"/dummies?page=4" } """ Scenario: Filter with exact match diff --git a/features/main/crud.feature b/features/main/crud.feature index bacbfbcca9f..472ee1b47c5 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -123,73 +123,67 @@ Feature: Create-Retrieve-Update-Delete "hydra:totalItems": 1, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "/dummies{?id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,order[id],order[name],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],dummyBoolean,dummyFloat,dummyPrice,description[exists],relatedDummy.name[exists],dummyBoolean[exists]}", + "hydra:template": "/dummies{?dummyBoolean,dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],description[exists],relatedDummy.name[exists],dummyBoolean[exists],dummyFloat,dummyPrice,order[id],order[name],order[relatedDummy.symfony],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ { "@type": "IriTemplateMapping", - "variable": "id", - "property": "id", + "variable": "dummyBoolean", + "property": "dummyBoolean", "required": false }, { "@type": "IriTemplateMapping", - "variable": "id[]", - "property": "id", + "variable": "dummyDate[before]", + "property": "dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "name", - "property": "name", + "variable": "dummyDate[after]", + "property": "dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "alias", - "property": "alias", + "variable": "relatedDummy.dummyDate[before]", + "property": "relatedDummy.dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "description", - "property": "description", + "variable": "relatedDummy.dummyDate[after]", + "property": "relatedDummy.dummyDate", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name", - "property": "relatedDummy.name", + "variable": "description[exists]", + "property": "description", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[]", + "variable": "relatedDummy.name[exists]", "property": "relatedDummy.name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummies", - "property": "relatedDummies", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummies[]", - "property": "relatedDummies", + "variable": "dummyBoolean[exists]", + "property": "dummyBoolean", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummy", - "property": "dummy", + "variable": "dummyFloat", + "property": "dummyFloat", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummies.name", - "property": "relatedDummies.name", + "variable": "dummyPrice", + "property": "dummyPrice", "required": false }, { @@ -210,30 +204,6 @@ Feature: Create-Retrieve-Update-Delete "property": "relatedDummy.symfony", "required": false }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[before]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "dummyDate[after]", - "property": "dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[before]", - "property": "relatedDummy.dummyDate", - "required": false - }, - { - "@type": "IriTemplateMapping", - "variable": "relatedDummy.dummyDate[after]", - "property": "relatedDummy.dummyDate", - "required": false - }, { "@type": "IriTemplateMapping", "variable": "dummyFloat[between]", @@ -296,38 +266,68 @@ Feature: Create-Retrieve-Update-Delete }, { "@type": "IriTemplateMapping", - "variable": "dummyBoolean", - "property": "dummyBoolean", + "variable": "id", + "property": "id", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyFloat", - "property": "dummyFloat", + "variable": "id[]", + "property": "id", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyPrice", - "property": "dummyPrice", + "variable": "name", + "property": "name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "description[exists]", + "variable": "alias", + "property": "alias", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "description", "property": "description", "required": false }, { "@type": "IriTemplateMapping", - "variable": "relatedDummy.name[exists]", + "variable": "relatedDummy.name", "property": "relatedDummy.name", "required": false }, { "@type": "IriTemplateMapping", - "variable": "dummyBoolean[exists]", - "property": "dummyBoolean", + "variable": "relatedDummy.name[]", + "property": "relatedDummy.name", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies", + "property": "relatedDummies", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies[]", + "property": "relatedDummies", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "dummy", + "property": "dummy", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "relatedDummies.name", + "property": "relatedDummies.name", "required": false } ] diff --git a/features/swagger/docs.feature b/features/swagger/docs.feature index bba7d4f8f18..f0012d15d74 100644 --- a/features/swagger/docs.feature +++ b/features/swagger/docs.feature @@ -55,10 +55,10 @@ Feature: Documentation support And "id" property exists for the Swagger class "Dummy" And "name" property is required for Swagger class "Dummy" # Filters - And the JSON node "paths./dummies.get.parameters[0].name" should be equal to "id" + And the JSON node "paths./dummies.get.parameters[0].name" should be equal to "dummyBoolean" And the JSON node "paths./dummies.get.parameters[0].in" should be equal to "query" And the JSON node "paths./dummies.get.parameters[0].required" should be false - And the JSON node "paths./dummies.get.parameters[0].type" should be equal to "integer" + And the JSON node "paths./dummies.get.parameters[0].type" should be equal to "boolean" Scenario: Swagger UI is enabled for docs endpoint Given I add "Accept" header equal to "text/html" diff --git a/src/Api/FilterCollection.php b/src/Api/FilterCollection.php index 66bec766f95..b7833aab70c 100644 --- a/src/Api/FilterCollection.php +++ b/src/Api/FilterCollection.php @@ -13,11 +13,21 @@ namespace ApiPlatform\Core\Api; +use Psr\Container\ContainerInterface; + /** * A list of filters. * * @author Kévin Dunglas + * + * @deprecated since version 2.1, to be removed in 3.0. Use a service locator {@see \Psr\Container\ContainerInterface}. */ final class FilterCollection extends \ArrayObject { + public function __construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator') + { + @trigger_error(sprintf('The %s class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of %s instead.', self::class, ContainerInterface::class), E_USER_DEPRECATED); + + parent::__construct($input, $flags, $iterator_class); + } } diff --git a/src/Api/FilterCollectionFactory.php b/src/Api/FilterCollectionFactory.php new file mode 100644 index 00000000000..da546185fd6 --- /dev/null +++ b/src/Api/FilterCollectionFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Api; + +use Psr\Container\ContainerInterface; + +/** + * Filter collection factory. + * + * @author Baptiste Meyer + * + * @internal + */ +class FilterCollectionFactory +{ + private $filtersIds; + + /** + * @param string[] $filtersIds + */ + public function __construct(array $filtersIds) + { + $this->filtersIds = $filtersIds; + } + + /** + * Creates a filter collection from a filter locator. + * + * @param ContainerInterface $filterLocator + * + * @return FilterCollection + */ + public function createFilterCollectionFromLocator(ContainerInterface $filterLocator): FilterCollection + { + $filters = []; + + foreach ($this->filtersIds as $filterId) { + if ($filterLocator->has($filterId)) { + $filters[$filterId] = $filterLocator->get($filterId); + } + } + + return new FilterCollection($filters); + } +} diff --git a/src/Api/FilterLocatorTrait.php b/src/Api/FilterLocatorTrait.php new file mode 100644 index 00000000000..1a9f66b2a50 --- /dev/null +++ b/src/Api/FilterLocatorTrait.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Api; + +use ApiPlatform\Core\Exception\InvalidArgumentException; +use Psr\Container\ContainerInterface; + +/** + * Manipulates filters with a backward compatibility between the new filter locator and the deprecated filter collection. + * + * @author Baptiste Meyer + * + * @internal + */ +trait FilterLocatorTrait +{ + private $filterLocator; + + /** + * Sets a filter locator with a backward compatibility. + * + * @param ContainerInterface|FilterCollection|null $filterLocator + * @param bool $allowNull + */ + private function setFilterLocator($filterLocator = null, bool $allowNull = false) + { + if ($filterLocator instanceof ContainerInterface || $filterLocator instanceof FilterCollection || null === $filterLocator && $allowNull) { + if ($filterLocator instanceof FilterCollection) { + @trigger_error(sprintf('The %s class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of %s instead.', FilterCollection::class, ContainerInterface::class), E_USER_DEPRECATED); + } + + $this->filterLocator = $filterLocator; + } else { + throw new InvalidArgumentException(sprintf('The "$filterLocator" argument is expected to be an implementation of the "%s" interface%s.', ContainerInterface::class, $allowNull ? ' or null' : '')); + } + } + + /** + * Gets a filter with a backward compatibility. + * + * @param string $filterId + * + * @return FilterInterface|null + */ + private function getFilter(string $filterId) + { + if ($this->filterLocator instanceof ContainerInterface && $this->filterLocator->has($filterId)) { + return $this->filterLocator->get($filterId); + } + + if ($this->filterLocator instanceof FilterCollection && $this->filterLocator->offsetExists($filterId)) { + return $this->filterLocator->offsetGet($filterId); + } + } +} diff --git a/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php b/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php index 7a9f8b7d723..03599916b85 100644 --- a/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/FilterExtension.php @@ -14,10 +14,12 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\ORM\QueryBuilder; +use Psr\Container\ContainerInterface; /** * Applies filters on a resource query. @@ -27,13 +29,18 @@ */ final class FilterExtension implements QueryCollectionExtensionInterface { + use FilterLocatorTrait; + private $resourceMetadataFactory; - private $filters; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, FilterCollection $filters) + /** + * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection + */ + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, $filterLocator) { + $this->setFilterLocator($filterLocator); + $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->filters = $filters; } /** @@ -48,10 +55,12 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator return; } - foreach ($this->filters as $filterName => $filter) { - if ($filter instanceof FilterInterface && in_array($filterName, $resourceFilters, true)) { - $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + foreach ($resourceFilters as $filterId) { + if (!($filter = $this->getFilter($filterId)) instanceof FilterInterface) { + continue; } + + $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); } } } diff --git a/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php b/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php index 3c0f8c31ced..08e4e07779d 100644 --- a/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php +++ b/src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Bridge\NelmioApiDoc\Extractor\AnnotationsProvider; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Bridge\NelmioApiDoc\Parser\ApiPlatformParser; use ApiPlatform\Core\Bridge\Symfony\Routing\OperationMethodResolverInterface; use ApiPlatform\Core\Documentation\Documentation; @@ -22,6 +23,7 @@ use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -33,18 +35,23 @@ */ final class ApiPlatformProvider implements AnnotationsProviderInterface { + use FilterLocatorTrait; + private $resourceNameCollectionFactory; private $documentationNormalizer; private $resourceMetadataFactory; - private $filters; private $operationMethodResolver; - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, NormalizerInterface $documentationNormalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, FilterCollection $filters, OperationMethodResolverInterface $operationMethodResolver) + /** + * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection + */ + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, NormalizerInterface $documentationNormalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, $filterLocator, OperationMethodResolverInterface $operationMethodResolver) { + $this->setFilterLocator($filterLocator); + $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; $this->documentationNormalizer = $documentationNormalizer; $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->filters = $filters; $this->operationMethodResolver = $operationMethodResolver; } @@ -129,8 +136,8 @@ private function getApiDoc(bool $collection, string $resourceClass, ResourceMeta $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); $data['filters'] = []; - foreach ($this->filters as $filterName => $filter) { - if (in_array($filterName, $resourceFilters, true)) { + foreach ($resourceFilters as $filterId) { + if ($filter = $this->getFilter($filterId)) { foreach ($filter->getDescription($resourceClass) as $name => $definition) { $data['filters'][] = ['name' => $name] + $definition; } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index d70eb25230e..4cf84710ead 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -76,6 +76,7 @@ public function load(array $configs, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('api.xml'); $loader->load('data_provider.xml'); + $loader->load('filter.xml'); if (interface_exists(ValidatorInterface::class)) { $loader->load('validator.xml'); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPass.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPass.php index 3f4a8842d10..0fa13a95985 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPass.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPass.php @@ -45,6 +45,7 @@ public function process(ContainerBuilder $container) } } - $container->getDefinition('api_platform.filters')->addArgument($filters); + $container->getDefinition('api_platform.filter_locator')->addArgument($filters); + $container->getDefinition('api_platform.filter_collection_factory')->addArgument(array_keys($filters)); } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml b/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml index 98f0552862e..f6d338dc389 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml @@ -10,8 +10,6 @@ - - diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index 41e178f1ef0..44fccccd953 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -120,7 +120,7 @@ - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/filter.xml b/src/Bridge/Symfony/Bundle/Resources/config/filter.xml new file mode 100644 index 00000000000..61fce0c2d38 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Resources/config/filter.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + The "%service_id%" service is deprecated since 2.1 and will be removed in 3.0. Use the "api_platform.filter_locator" service instead. + + + + + + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml index dea8c0065cf..0138a6a065d 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml @@ -66,7 +66,7 @@ - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml b/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml index 980757408e2..50cfdc59d4e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml @@ -9,7 +9,7 @@ - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml index 4a33dcdea66..26c1a25ef82 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml @@ -14,7 +14,7 @@ - + %api_platform.oauth.enabled% %api_platform.oauth.type% diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index 2525e27dd75..045112d3cce 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -15,9 +15,11 @@ use ApiPlatform\Core\Api\FilterCollection; use ApiPlatform\Core\Api\FilterInterface; +use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\JsonLd\Serializer\JsonLdContextTrait; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use Psr\Container\ContainerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -29,18 +31,22 @@ final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface { use JsonLdContextTrait; + use FilterLocatorTrait; private $collectionNormalizer; private $resourceMetadataFactory; private $resourceClassResolver; - private $filters; - public function __construct(NormalizerInterface $collectionNormalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, FilterCollection $filters) + /** + * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection + */ + public function __construct(NormalizerInterface $collectionNormalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, $filterLocator) { + $this->setFilterLocator($filterLocator); + $this->collectionNormalizer = $collectionNormalizer; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceClassResolver = $resourceClassResolver; - $this->filters = $filters; } /** @@ -81,8 +87,8 @@ public function normalize($object, $format = null, array $context = []) } $currentFilters = []; - foreach ($this->filters as $filterName => $filter) { - if (in_array($filterName, $resourceFilters, true)) { + foreach ($resourceFilters as $filterId) { + if ($filter = $this->getFilter($filterId)) { $currentFilters[] = $filter; } } diff --git a/src/Metadata/Extractor/YamlExtractor.php b/src/Metadata/Extractor/YamlExtractor.php index 141629d0060..6d08dce63dd 100644 --- a/src/Metadata/Extractor/YamlExtractor.php +++ b/src/Metadata/Extractor/YamlExtractor.php @@ -32,7 +32,7 @@ final class YamlExtractor extends AbstractExtractor protected function extractPath(string $path) { try { - $resourcesYaml = Yaml::parse(file_get_contents($path)); + $resourcesYaml = Yaml::parse(file_get_contents($path), Yaml::PARSE_KEYS_AS_STRINGS); } catch (ParseException $e) { $e->setParsedFile($path); diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index c88cb36778c..e61edc7b239 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Swagger\Serializer; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterLocatorTrait; use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; @@ -26,6 +27,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; +use Psr\Container\ContainerInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -39,6 +41,8 @@ */ final class DocumentationNormalizer implements NormalizerInterface { + use FilterLocatorTrait; + const SWAGGER_VERSION = '2.0'; const FORMAT = 'json'; @@ -49,7 +53,6 @@ final class DocumentationNormalizer implements NormalizerInterface private $operationMethodResolver; private $operationPathResolver; private $urlGenerator; - private $filterCollection; private $nameConverter; private $oauthEnabled; private $oauthType; @@ -58,8 +61,13 @@ final class DocumentationNormalizer implements NormalizerInterface private $oauthAuthorizationUrl; private $oauthScopes; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator, FilterCollection $filterCollection = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = []) + /** + * @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, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = []) { + $this->setFilterLocator($filterLocator, true); + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; @@ -67,7 +75,6 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa $this->operationMethodResolver = $operationMethodResolver; $this->operationPathResolver = $operationPathResolver; $this->urlGenerator = $urlGenerator; - $this->filterCollection = $filterCollection; $this->nameConverter = $nameConverter; $this->oauthEnabled = $oauthEnabled; $this->oauthType = $oauthType; @@ -574,14 +581,14 @@ private function computeDoc(Documentation $documentation, \ArrayObject $definiti */ private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array { - if (null === $this->filterCollection) { + if (null === $this->filterLocator) { return []; } $parameters = []; $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); - foreach ($this->filterCollection as $filterName => $filter) { - if (!in_array($filterName, $resourceFilters, true)) { + foreach ($resourceFilters as $filterId) { + if (!$filter = $this->getFilter($filterId)) { continue; } diff --git a/tests/Api/FilterCollectionFactoryTest.php b/tests/Api/FilterCollectionFactoryTest.php new file mode 100644 index 00000000000..6665570c33f --- /dev/null +++ b/tests/Api/FilterCollectionFactoryTest.php @@ -0,0 +1,47 @@ + + * + * 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\Api; + +use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterCollectionFactory; +use ApiPlatform\Core\Api\FilterInterface; +use Psr\Container\ContainerInterface; + +/** + * @author Baptiste Meyer + */ +class FilterCollectionFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testCreateFilterCollectionFromLocator() + { + $filter = $this->prophesize(FilterInterface::class)->reveal(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('foo')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('foo')->willReturn($filter)->shouldBeCalled(); + $filterLocatorProphecy->has('bar')->willReturn(false)->shouldBeCalled(); + + $filterCollection = (new FilterCollectionFactory(['foo', 'bar']))->createFilterCollectionFromLocator($filterLocatorProphecy->reveal()); + + $this->assertInstanceOf(FilterCollection::class, $filterCollection); + $this->assertArrayNotHasKey('bar', $filterCollection); + $this->assertArrayHasKey('foo', $filterCollection); + $this->assertInstanceOf(FilterInterface::class, $filterCollection['foo']); + $this->assertEquals(new FilterCollection(['foo' => $filter]), $filterCollection); + } +} diff --git a/tests/Api/FilterCollectionTest.php b/tests/Api/FilterCollectionTest.php index f00c15a8c66..d409b9255e0 100644 --- a/tests/Api/FilterCollectionTest.php +++ b/tests/Api/FilterCollectionTest.php @@ -20,6 +20,10 @@ */ class FilterCollectionTest extends \PHPUnit_Framework_TestCase { + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ public function testIsArrayObject() { $filterCollection = new FilterCollection(); diff --git a/tests/Api/FilterLocatorTraitTest.php b/tests/Api/FilterLocatorTraitTest.php new file mode 100644 index 00000000000..d219bc71fed --- /dev/null +++ b/tests/Api/FilterLocatorTraitTest.php @@ -0,0 +1,155 @@ + + * + * 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\Api; + +use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterInterface; +use ApiPlatform\Core\Api\FilterLocatorTrait; +use Psr\Container\ContainerInterface; + +/** + * @author Baptiste Meyer + */ +class FilterLocatorTraitTest extends \PHPUnit_Framework_TestCase +{ + public function testSetFilterLocator() + { + $filterLocator = $this->prophesize(ContainerInterface::class)->reveal(); + + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator($filterLocator); + + $this->assertEquals($filterLocator, $filterLocatorTraitImpl->getFilterLocator()); + } + + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testSetFilterLocatorWithDeprecatedFilterCollection() + { + $filterCollection = new FilterCollection(); + + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator($filterCollection); + + $this->assertEquals($filterCollection, $filterLocatorTraitImpl->getFilterLocator()); + } + + public function testSetFilterLocatorWithNullAndNullAllowed() + { + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator(null, true); + + $this->assertEquals(null, $filterLocatorTraitImpl->getFilterLocator()); + } + + /** + * @group legacy + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface. + */ + public function testSetFilterLocatorWithNullAndNullNotAllowed() + { + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator(null); + } + + /** + * @group legacy + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface or null. + */ + public function testSetFilterLocatorWithInvalidFilterLocator() + { + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator(new \ArrayObject(), true); + } + + public function testGetFilter() + { + $filter = $this->prophesize(FilterInterface::class)->reveal(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('foo')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('foo')->willReturn($filter)->shouldBeCalled(); + + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator($filterLocatorProphecy->reveal()); + + $returnedFilter = $filterLocatorTraitImpl->getFilter('foo'); + + $this->assertInstanceOf(FilterInterface::class, $returnedFilter); + $this->assertEquals($filter, $returnedFilter); + } + + public function testGetFilterWithNonexistentFilterId() + { + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('foo')->willReturn(false)->shouldBeCalled(); + + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator($filterLocatorProphecy->reveal()); + + $filter = $filterLocatorTraitImpl->getFilter('foo'); + + $this->assertNull($filter); + } + + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testGetFilterWithDeprecatedFilterCollection() + { + $filter = $this->prophesize(FilterInterface::class)->reveal(); + + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator(new FilterCollection(['foo' => $filter])); + + $returnedFilter = $filterLocatorTraitImpl->getFilter('foo'); + + $this->assertInstanceOf(FilterInterface::class, $returnedFilter); + $this->assertEquals($filter, $returnedFilter); + } + + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testGetFilterWithNonexistentFilterIdAndDeprecatedFilterCollection() + { + $filterLocatorTraitImpl = $this->getFilterLocatorTraitImpl(); + $filterLocatorTraitImpl->setFilterLocator(new FilterCollection()); + + $filter = $filterLocatorTraitImpl->getFilter('foo'); + + $this->assertNull($filter); + } + + private function getFilterLocatorTraitImpl() + { + return new class() { + use FilterLocatorTrait { + FilterLocatorTrait::setFilterLocator as public; + FilterLocatorTrait::getFilter as public; + } + + public function getFilterLocator() + { + return $this->filterLocator; + } + }; + } +} diff --git a/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php b/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php index 846376e174c..9474bac3765 100644 --- a/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php +++ b/tests/Bridge/Doctrine/Orm/Extension/FilterExtensionTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Extension; use ApiPlatform\Core\Api\FilterCollection; +use ApiPlatform\Core\Api\FilterInterface as ApiFilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; @@ -21,6 +22,7 @@ use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Doctrine\ORM\QueryBuilder; +use Psr\Container\ContainerInterface; /** * @author Amrouche Hamza @@ -31,33 +33,66 @@ public function testApplyToCollectionWithValidFilters() { $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET', 'filters' => ['dummyFilter']], 'post' => ['method' => 'POST'], 'custom' => ['method' => 'GET', 'path' => '/foo'], 'custom2' => ['method' => 'POST', 'path' => '/foo']], []); + $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET', 'filters' => ['dummyFilter', 'dummyBadFilter']], 'post' => ['method' => 'POST'], 'custom' => ['method' => 'GET', 'path' => '/foo'], 'custom2' => ['method' => 'POST', 'path' => '/foo']], []); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); $queryBuilder = $queryBuilderProphecy->reveal(); - $filterProphecy = $this->prophesize(FilterInterface::class); - $filterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldBeCalled(); + $ormFilterProphecy = $this->prophesize(FilterInterface::class); + $ormFilterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldBeCalled(); - $orderExtensionTest = new FilterExtension($resourceMetadataFactoryProphecy->reveal(), new FilterCollection(['dummyFilter' => $filterProphecy->reveal()])); + $ordinaryFilterProphecy = $this->prophesize(ApiFilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummyFilter')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->has('dummyBadFilter')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummyFilter')->willReturn($ormFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->get('dummyBadFilter')->willReturn($ordinaryFilterProphecy->reveal())->shouldBeCalled(); + + $orderExtensionTest = new FilterExtension($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal()); $orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get'); } - public function testApplyToCollectionWithoutFilters() + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testApplyToCollectionWithValidFiltersAndDeprecatedFilterCollection() { $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET'], 'post' => ['method' => 'POST'], 'custom' => ['method' => 'GET', 'path' => '/foo'], 'custom2' => ['method' => 'POST', 'path' => '/foo']]); + $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET', 'filters' => ['dummyFilter']], 'post' => ['method' => 'POST'], 'custom' => ['method' => 'GET', 'path' => '/foo'], 'custom2' => ['method' => 'POST', 'path' => '/foo']], []); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); $queryBuilder = $queryBuilderProphecy->reveal(); $filterProphecy = $this->prophesize(FilterInterface::class); - $filterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldNotBeCalled(); + $filterProphecy->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get')->shouldBeCalled(); $orderExtensionTest = new FilterExtension($resourceMetadataFactoryProphecy->reveal(), new FilterCollection(['dummyFilter' => $filterProphecy->reveal()])); $orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, 'get'); } + + /** + * @group legacy + * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface. + */ + public function testConstructWithInvalidFilterLocator() + { + new FilterExtension($this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), new \ArrayObject()); + } + + public function testApplyToCollectionWithoutFilters() + { + $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET'], 'post' => ['method' => 'POST'], 'custom' => ['method' => 'GET', 'path' => '/foo'], 'custom2' => ['method' => 'POST', 'path' => '/foo']]); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); + + $orderExtensionTest = new FilterExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(ContainerInterface::class)->reveal()); + $orderExtensionTest->applyToCollection($this->prophesize(QueryBuilder::class)->reveal(), new QueryNameGenerator(), Dummy::class, 'get'); + } } diff --git a/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php b/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php index 24acd48f9dd..c7c168576c3 100644 --- a/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php +++ b/tests/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProviderTest.php @@ -26,6 +26,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; +use Psr\Container\ContainerInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -45,17 +46,72 @@ public function testConstruct() $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); - $filters = new FilterCollection(); + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocator = $filterLocatorProphecy->reveal(); $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); $operationMethodResolver = $operationMethodResolverProphecy->reveal(); - $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $documentationNormalizer, $resourceMetadataFactory, $filters, $operationMethodResolver); + $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $documentationNormalizer, $resourceMetadataFactory, $filterLocator, $operationMethodResolver); $this->assertInstanceOf(AnnotationsProviderInterface::class, $apiPlatformProvider); } public function testGetAnnotations() + { + $dummySearchFilterProphecy = $this->prophesize(FilterInterface::class); + $dummySearchFilterProphecy->getDescription(Dummy::class)->willReturn([ + 'name' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => 'false', + 'strategy' => 'partial', + ], + ])->shouldBeCalled(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('my_dummy.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('my_dummy.search')->willReturn($dummySearchFilterProphecy->reveal())->shouldBeCalled(); + + $this->extractAnnotations($filterLocatorProphecy->reveal()); + } + + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testGetAnnotationsWithDeprecatedFilterCollection() + { + $dummySearchFilterProphecy = $this->prophesize(FilterInterface::class); + $dummySearchFilterProphecy->getDescription(Dummy::class)->willReturn([ + 'name' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => 'false', + 'strategy' => 'partial', + ], + ])->shouldBeCalled(); + + $this->extractAnnotations(new FilterCollection(['my_dummy.search' => $dummySearchFilterProphecy->reveal()])); + } + + /** + * @group legacy + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface. + */ + public function testConstructWithInvalidFilterLocator() + { + new ApiPlatformProvider( + $this->prophesize(ResourceNameCollectionFactoryInterface::class)->reveal(), + $this->prophesize(NormalizerInterface::class)->reveal(), + $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), + new \ArrayObject(), + $this->prophesize(OperationMethodResolverInterface::class)->reveal() + ); + } + + private function extractAnnotations($filterLocator) { $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([Dummy::class]))->shouldBeCalled(); @@ -94,20 +150,6 @@ public function testGetAnnotations() $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn($dummyResourceMetadata)->shouldBeCalled(); $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); - $dummySearchFilterProphecy = $this->prophesize(FilterInterface::class); - $dummySearchFilterProphecy->getDescription(Dummy::class)->willReturn([ - 'name' => [ - 'property' => 'name', - 'type' => 'string', - 'required' => 'false', - 'strategy' => 'partial', - ], - ])->shouldBeCalled(); - $dummySearchFilter = $dummySearchFilterProphecy->reveal(); - $filters = new FilterCollection([ - 'my_dummy.search' => $dummySearchFilter, - ]); - $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'get')->willReturn('GET')->shouldBeCalled(); $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'post')->willReturn('POST')->shouldBeCalled(); @@ -121,7 +163,7 @@ public function testGetAnnotations() $operationMethodResolverProphecy->getItemOperationRoute(Dummy::class, 'delete')->willReturn((new Route('/dummies/{id}'))->setMethods(['DELETE']))->shouldBeCalled(); $operationMethodResolver = $operationMethodResolverProphecy->reveal(); - $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $apiDocumentationBuilder, $resourceMetadataFactory, $filters, $operationMethodResolver); + $apiPlatformProvider = new ApiPlatformProvider($resourceNameCollectionFactory, $apiDocumentationBuilder, $resourceMetadataFactory, $filterLocator, $operationMethodResolver); $actual = $apiPlatformProvider->getAnnotations(); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index b2de6482e95..8c95a86351d 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -21,7 +21,6 @@ use Prophecy\Argument; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Config\FileLocator; -use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; @@ -261,7 +260,7 @@ private function getContainerBuilderProphecy() $containerBuilderProphecy->setParameter($key, $value)->shouldBeCalled(); } - $containerBuilderProphecy->addResource(Argument::type(ResourceInterface::class))->shouldBeCalled(); + $containerBuilderProphecy->fileExists(Argument::type('string'))->shouldBeCalled(); $containerBuilderProphecy->hasExtension('http://symfony.com/schema/dic/services')->shouldBeCalled(); foreach (['yaml', 'xml'] as $format) { @@ -302,6 +301,8 @@ private function getContainerBuilderProphecy() 'api_platform.doctrine.orm.range_filter', 'api_platform.doctrine.orm.search_filter', 'api_platform.doctrine.orm.subresource_data_provider', + 'api_platform.filter_locator', + 'api_platform.filter_collection_factory', 'api_platform.filters', 'api_platform.doctrine.listener.view.write', 'api_platform.jsonld.normalizer.item', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPassTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPassTest.php index 85800d74bb6..652a793840d 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPassTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/FilterPassTest.php @@ -31,18 +31,20 @@ public function testProcess() $this->assertInstanceOf(CompilerPassInterface::class, $dataProviderPass); - $definitionProphecy = $this->prophesize(Definition::class); - $definitionProphecy->addArgument(Argument::that(function (array $arg) { + $filterLocatorDefinitionProphecy = $this->prophesize(Definition::class); + $filterLocatorDefinitionProphecy->addArgument(Argument::that(function (array $arg) { return !isset($arg['foo']) && isset($arg['my_id']) && $arg['my_id'] instanceof Reference; }))->shouldBeCalled(); - $definition = $definitionProphecy->reveal(); + + $filterCollectionFactoryDefinitionProphecy = $this->prophesize(Definition::class); + $filterCollectionFactoryDefinitionProphecy->addArgument(['my_id'])->shouldBeCalled(); $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); $containerBuilderProphecy->findTaggedServiceIds('api_platform.filter')->willReturn(['foo' => [], 'bar' => [['id' => 'my_id']]])->shouldBeCalled(); - $containerBuilderProphecy->getDefinition('api_platform.filters')->willReturn($definition)->shouldBeCalled(); - $containerBuilder = $containerBuilderProphecy->reveal(); + $containerBuilderProphecy->getDefinition('api_platform.filter_locator')->willReturn($filterLocatorDefinitionProphecy->reveal())->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.filter_collection_factory')->willReturn($filterCollectionFactoryDefinitionProphecy->reveal())->shouldBeCalled(); - $dataProviderPass->process($containerBuilder); + $dataProviderPass->process($containerBuilderProphecy->reveal()); } public function testIdNotExist() @@ -51,17 +53,19 @@ public function testIdNotExist() $this->assertInstanceOf(CompilerPassInterface::class, $dataProviderPass); - $definitionProphecy = $this->prophesize(Definition::class); - $definitionProphecy->addArgument(Argument::that(function (array $arg) { + $filterLocatorDefinitionProphecy = $this->prophesize(Definition::class); + $filterLocatorDefinitionProphecy->addArgument(Argument::that(function (array $arg) { return !isset($arg['foo']) && isset($arg['bar']) && $arg['bar'] instanceof Reference; }))->shouldBeCalled(); - $definition = $definitionProphecy->reveal(); + + $filterCollectionFactoryDefinitionProphecy = $this->prophesize(Definition::class); + $filterCollectionFactoryDefinitionProphecy->addArgument(['bar'])->shouldBeCalled(); $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); $containerBuilderProphecy->findTaggedServiceIds('api_platform.filter')->willReturn(['foo' => [], 'bar' => [['hi' => 'hello']]])->shouldBeCalled(); - $containerBuilderProphecy->getDefinition('api_platform.filters')->willReturn($definition)->shouldBeCalled(); - $containerBuilder = $containerBuilderProphecy->reveal(); + $containerBuilderProphecy->getDefinition('api_platform.filter_locator')->willReturn($filterLocatorDefinitionProphecy->reveal())->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.filter_collection_factory')->willReturn($filterCollectionFactoryDefinitionProphecy->reveal())->shouldBeCalled(); - $dataProviderPass->process($containerBuilder); + $dataProviderPass->process($containerBuilderProphecy->reveal()); } } diff --git a/tests/Fixtures/FileConfigurations/parse_exception.yml b/tests/Fixtures/FileConfigurations/parse_exception.yml index 87ff9d1e3bb..ac623fc2f8e 100644 --- a/tests/Fixtures/FileConfigurations/parse_exception.yml +++ b/tests/Fixtures/FileConfigurations/parse_exception.yml @@ -1,2 +1,2 @@ parse - exception + 'exception': diff --git a/tests/Fixtures/FileConfigurations/resources_empty.yml b/tests/Fixtures/FileConfigurations/resources_empty.yml new file mode 100644 index 00000000000..54bcf304652 --- /dev/null +++ b/tests/Fixtures/FileConfigurations/resources_empty.yml @@ -0,0 +1 @@ +~ diff --git a/tests/Fixtures/app/config/config.yml b/tests/Fixtures/app/config/config.yml index 9a09d518186..6bfe622d14e 100644 --- a/tests/Fixtures/app/config/config.yml +++ b/tests/Fixtures/app/config/config.yml @@ -10,7 +10,6 @@ framework: enable_annotations: true serializer: enable_annotations: true - trusted_proxies: ~ test: ~ session: storage_id: 'session.storage.mock_file' diff --git a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php b/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php index da650538062..5af18ed611a 100644 --- a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php +++ b/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use Psr\Container\ContainerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** @@ -29,18 +30,16 @@ class CollectionFiltersNormalizerTest extends \PHPUnit_Framework_TestCase { public function testSupportsNormalization() { - $decorated = $this->prophesize(NormalizerInterface::class); - $decorated->supportsNormalization('foo', 'abc')->willReturn(true)->shouldBeCalled(); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $decoratedProphecy = $this->prophesize(NormalizerInterface::class); + $decoratedProphecy->supportsNormalization('foo', 'abc')->willReturn(true)->shouldBeCalled(); $normalizer = new CollectionFiltersNormalizer( - $decorated->reveal(), - $resourceMetadataFactory->reveal(), - $resourceClassResolver->reveal(), - new FilterCollection() + $decoratedProphecy->reveal(), + $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), + $this->prophesize(ResourceClassResolverInterface::class)->reveal(), + $this->prophesize(ContainerInterface::class)->reveal() ); + $this->assertTrue($normalizer->supportsNormalization('foo', 'abc')); } @@ -48,19 +47,19 @@ public function testDoNothingIfSubLevel() { $dummy = new Dummy(); - $decorated = $this->prophesize(NormalizerInterface::class); - $decorated->normalize($dummy, null, ['api_sub_level' => true])->willReturn(['name' => 'foo'])->shouldBeCalled(); + $decoratedProphecy = $this->prophesize(NormalizerInterface::class); + $decoratedProphecy->normalize($dummy, null, ['api_sub_level' => true])->willReturn(['name' => 'foo'])->shouldBeCalled(); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->getResourceClass()->shouldNotBeCalled(); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass()->shouldNotBeCalled(); $normalizer = new CollectionFiltersNormalizer( - $decorated->reveal(), - $resourceMetadataFactory->reveal(), - $resourceClassResolver->reveal(), - new FilterCollection() + $decoratedProphecy->reveal(), + $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), + $resourceClassResolverProphecy->reveal(), + $this->prophesize(ContainerInterface::class)->reveal() ); + $this->assertEquals(['name' => 'foo'], $normalizer->normalize($dummy, null, ['api_sub_level' => true])); } @@ -68,21 +67,22 @@ public function testDoNothingIfNoFilter() { $dummy = new Dummy(); - $decorated = $this->prophesize(NormalizerInterface::class); - $decorated->normalize($dummy, null, ['collection_operation_name' => 'get'])->willReturn(['name' => 'foo'])->shouldBeCalled(); + $decoratedProphecy = $this->prophesize(NormalizerInterface::class); + $decoratedProphecy->normalize($dummy, null, ['collection_operation_name' => 'get'])->willReturn(['name' => 'foo'])->shouldBeCalled(); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], ['get' => []])); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], ['get' => []])); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $normalizer = new CollectionFiltersNormalizer( - $decorated->reveal(), - $resourceMetadataFactory->reveal(), - $resourceClassResolver->reveal(), - new FilterCollection() + $decoratedProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $this->prophesize(ContainerInterface::class)->reveal() ); + $this->assertEquals(['name' => 'foo'], $normalizer->normalize($dummy, null, ['collection_operation_name' => 'get'])); } @@ -90,62 +90,99 @@ public function testDoNothingIfNoRequestUri() { $dummy = new Dummy(); - $decorated = $this->prophesize(NormalizerInterface::class); - $decorated->normalize($dummy, null, [])->willReturn(['name' => 'foo'])->shouldBeCalled(); + $decoratedProphecy = $this->prophesize(NormalizerInterface::class); + $decoratedProphecy->normalize($dummy, null, [])->willReturn(['name' => 'foo'])->shouldBeCalled(); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], [], ['filters' => ['foo']])); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], [], ['filters' => ['foo']])); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $normalizer = new CollectionFiltersNormalizer( - $decorated->reveal(), - $resourceMetadataFactory->reveal(), - $resourceClassResolver->reveal(), - new FilterCollection() + $decoratedProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $this->prophesize(ContainerInterface::class)->reveal() ); + $this->assertEquals(['name' => 'foo'], $normalizer->normalize($dummy, null, [])); } public function testNormalize() { - $dummy = new Dummy(); + $filterProphecy = $this->prophesize(FilterInterface::class); + $filterProphecy->getDescription(Dummy::class)->willReturn(['a' => ['property' => 'name', 'required' => true]])->shouldBeCalled(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('foo')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('foo')->willReturn($filterProphecy->reveal())->shouldBeCalled(); - $decorated = $this->prophesize(NormalizerInterface::class); - $decorated->normalize($dummy, null, ['request_uri' => '/foo?bar=baz'])->willReturn(['name' => 'foo'])->shouldBeCalled(); + $this->normalize($filterLocatorProphecy->reveal()); + } + + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testNormalizeWithDeprecatedFilterCollection() + { + $filterProphecy = $this->prophesize(FilterInterface::class); + $filterProphecy->getDescription(Dummy::class)->willReturn(['a' => ['property' => 'name', 'required' => true]])->shouldBeCalled(); + + $this->normalize(new FilterCollection(['foo' => $filterProphecy->reveal()])); + } + + /** + * @group legacy + * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface. + */ + public function testConstructWithInvalidFilterLocator() + { + new CollectionFiltersNormalizer( + $this->prophesize(NormalizerInterface::class)->reveal(), + $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), + $this->prophesize(ResourceClassResolverInterface::class)->reveal(), + new \ArrayObject() + ); + } + + private function normalize($filterLocator) + { + $dummy = new Dummy(); - $resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], [], ['filters' => ['foo']])); + $decoratedProphecy = $this->prophesize(NormalizerInterface::class); + $decoratedProphecy->normalize($dummy, null, ['request_uri' => '/foo?bar=baz'])->willReturn(['name' => 'foo'])->shouldBeCalled(); - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('foo', '', null, [], [], ['filters' => ['foo']])); - $filter = $this->prophesize(FilterInterface::class); - $filter->getDescription(Dummy::class)->willReturn(['a' => ['property' => 'name', 'required' => true]])->shouldBeCalled(); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $normalizer = new CollectionFiltersNormalizer( - $decorated->reveal(), - $resourceMetadataFactory->reveal(), - $resourceClassResolver->reveal(), - new FilterCollection(['foo' => $filter->reveal()]) + $decoratedProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $filterLocator ); $this->assertEquals([ 'name' => 'foo', - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/foo{?a}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ - [ - '@type' => 'IriTemplateMapping', - 'variable' => 'a', - 'property' => 'name', - 'required' => true, - ], - ], - ], - ], $normalizer->normalize($dummy, null, ['request_uri' => '/foo?bar=baz'])); + 'hydra:search' => [ + '@type' => 'hydra:IriTemplate', + 'hydra:template' => '/foo{?a}', + 'hydra:variableRepresentation' => 'BasicRepresentation', + 'hydra:mapping' => [ + [ + '@type' => 'IriTemplateMapping', + 'variable' => 'a', + 'property' => 'name', + 'required' => true, + ], + ], + ], + ], $normalizer->normalize($dummy, null, ['request_uri' => '/foo?bar=baz'])); } } diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index d65ee86354c..ac715137275 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -28,4 +28,20 @@ public function testInvalidProperty() { (new YamlExtractor([__DIR__.'/../../Fixtures/FileConfigurations/badpropertytype.yml']))->getResources(); } + + /** + * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Unable to parse in ".+\/\.\.\/\.\.\/Fixtures\/FileConfigurations\/parse_exception.yml"/ + */ + public function testParseException() + { + (new YamlExtractor([__DIR__.'/../../Fixtures/FileConfigurations/parse_exception.yml']))->getResources(); + } + + public function testEmptyResources() + { + $resources = (new YamlExtractor([__DIR__.'/../../Fixtures/FileConfigurations/resources_empty.yml']))->getResources(); + + $this->assertEmpty($resources); + } } diff --git a/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php b/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php index 9d976f24502..2fbfc3a436c 100644 --- a/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php +++ b/tests/Metadata/Resource/Factory/ExtractorResourceMetadataFactoryTest.php @@ -19,7 +19,6 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceNameCollectionFactory; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; -use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy; /** @@ -38,7 +37,6 @@ public function testXmlCreateResourceMetadata($expectedResourceMetadata) $resourceMetadataFactory = new ExtractorResourceMetadataFactory(new XmlExtractor([$configPath])); $resourceMetadata = $resourceMetadataFactory->create(FileConfigDummy::class); - $resourceMetadataDummy = $resourceMetadataFactory->create(Dummy::class); $this->assertInstanceOf(ResourceMetadata::class, $resourceMetadata); $this->assertEquals($expectedResourceMetadata, $resourceMetadata); @@ -151,7 +149,6 @@ public function testXmlParentResourceMetadataFactory(ResourceMetadata $expectedR } /** - * @expectedDeprecation Configuring "%s" tags without using a parent "%ss" tag is deprecrated since API Platform 2.1 and will not be possible anymore in API Platform 3 * @dataProvider resourceMetadataProvider */ public function testXmlExistingParentResourceMetadataFactory(ResourceMetadata $expectedResourceMetadata) @@ -177,7 +174,6 @@ public function testYamlCreateResourceMetadata(ResourceMetadata $expectedResourc $resourceMetadataFactory = new ExtractorResourceMetadataFactory(new YamlExtractor([$configPath])); $resourceMetadata = $resourceMetadataFactory->create(FileConfigDummy::class); - $resourceMetadataDummy = $resourceMetadataFactory->create(Dummy::class); $this->assertInstanceOf(ResourceMetadata::class, $resourceMetadata); $this->assertEquals($expectedResourceMetadata, $resourceMetadata); diff --git a/tests/Swagger/Serializer/DocumentationNormalizerTest.php b/tests/Swagger/Serializer/DocumentationNormalizerTest.php index ae4e030b1e7..c61a792cc9a 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerTest.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerTest.php @@ -26,11 +26,13 @@ use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; use ApiPlatform\Core\PathResolver\CustomOperationPathResolver; +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\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use Psr\Container\ContainerInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -925,37 +927,40 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups() public function testFilters() { - $documentation = new Documentation(new ResourceNameCollection([Dummy::class]), '', '', '0.0.0', ['jsonld' => ['application/ld+json']]); - - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); - - $dummyMetadata = new ResourceMetadata( - 'Dummy', - 'This is a dummy.', - null, - [], - ['get' => ['method' => 'GET', 'filters' => ['f1', 'f2']]], - [] - ); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [])); - - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filters = [ + 'f1' => new DummyFilter(['name' => [ + 'property' => 'name', + 'type' => 'string', + 'required' => true, + 'strategy' => 'exact', + 'swagger' => ['x-foo' => 'bar'], + ]]), + 'f2' => new DummyFilter(['ha' => [ + 'property' => 'foo', + 'type' => 'int', + 'required' => false, + 'strategy' => 'partial', + ]]), + ]; - $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); - $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'get')->shouldBeCalled()->willReturn('GET'); + foreach ($filters as $filterId => $filter) { + $filterLocatorProphecy->has($filterId)->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get($filterId)->willReturn($filter)->shouldBeCalled(); + } - $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); - $urlGeneratorProphecy->generate('api_entrypoint')->willReturn('/')->shouldBeCalled(); + $filterLocatorProphecy->has('f3')->willReturn(false)->shouldBeCalled(); - $operationPathResolver = new CustomOperationPathResolver(new UnderscoreOperationPathResolver()); + $this->normalizeWithFilters($filterLocatorProphecy->reveal()); + } - $filters = new FilterCollection([ + /** + * @group legacy + * @expectedDeprecation The ApiPlatform\Core\Api\FilterCollection class is deprecated since version 2.1 and will be removed in 3.0. Provide an implementation of Psr\Container\ContainerInterface instead. + */ + public function testFiltersWithDeprecatedFilterCollection() + { + $this->normalizeWithFilters(new FilterCollection([ 'f1' => new DummyFilter(['name' => [ 'property' => 'name', 'type' => 'string', @@ -969,75 +974,26 @@ public function testFilters() 'required' => false, 'strategy' => 'partial', ]]), - ]); + ])); + } - $normalizer = new DocumentationNormalizer( - $resourceMetadataFactoryProphecy->reveal(), - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $operationMethodResolverProphecy->reveal(), - $operationPathResolver, - $urlGeneratorProphecy->reveal(), - $filters + /** + * @group legacy + * @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException + * @expectedExceptionMessage The "$filterLocator" argument is expected to be an implementation of the "Psr\Container\ContainerInterface" interface or null. + */ + public function testConstructWithInvalidFilterLocator() + { + new DocumentationNormalizer( + $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(), + $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), + $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), + $this->prophesize(ResourceClassResolverInterface::class)->reveal(), + $this->prophesize(OperationMethodResolverInterface::class)->reveal(), + $this->prophesize(OperationPathResolverInterface::class)->reveal(), + $this->prophesize(UrlGeneratorInterface::class)->reveal(), + new \ArrayObject() ); - - $expected = [ - 'swagger' => '2.0', - 'basePath' => '/', - 'info' => [ - 'title' => '', - 'version' => '0.0.0', - ], - 'paths' => new \ArrayObject([ - '/dummies' => [ - 'get' => new \ArrayObject([ - 'tags' => ['Dummy'], - 'operationId' => 'getDummyCollection', - 'produces' => ['application/ld+json'], - 'summary' => 'Retrieves the collection of Dummy resources.', - 'responses' => [ - 200 => [ - 'description' => 'Dummy collection response', - 'schema' => [ - 'type' => 'array', - 'items' => ['$ref' => '#/definitions/Dummy'], - ], - ], - ], - 'parameters' => [ - [ - 'x-foo' => 'bar', - 'name' => 'name', - 'in' => 'query', - 'required' => true, - 'type' => 'string', - ], - [ - 'name' => 'ha', - 'in' => 'query', - 'required' => false, - 'type' => 'integer', - ], - ], - ]), - ], - ]), - 'definitions' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ - 'type' => 'object', - 'description' => 'This is a dummy.', - 'properties' => [ - 'name' => new \ArrayObject([ - 'description' => 'This is a name.', - 'type' => 'string', - ]), - ], - ]), - ]), - ]; - - $this->assertEquals($expected, $normalizer->normalize($documentation)); } public function testSupports() @@ -1387,4 +1343,105 @@ public function testNormalizeWithNestedNormalizationGroups() $this->assertEquals($expected, $normalizer->normalize($documentation)); } + + private function normalizeWithFilters($filterLocator) + { + $documentation = new Documentation(new ResourceNameCollection([Dummy::class]), '', '', '0.0.0', ['jsonld' => ['application/ld+json']]); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); + + $dummyMetadata = new ResourceMetadata( + 'Dummy', + 'This is a dummy.', + null, + [], + ['get' => ['method' => 'GET', 'filters' => ['f1', 'f2', 'f3']]], + [] + ); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [])); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); + $operationMethodResolverProphecy->getCollectionOperationMethod(Dummy::class, 'get')->shouldBeCalled()->willReturn('GET'); + + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_entrypoint')->willReturn('/')->shouldBeCalled(); + + $operationPathResolver = new CustomOperationPathResolver(new UnderscoreOperationPathResolver()); + + $normalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $operationMethodResolverProphecy->reveal(), + $operationPathResolver, + $urlGeneratorProphecy->reveal(), + $filterLocator + ); + + $expected = [ + 'swagger' => '2.0', + 'basePath' => '/', + 'info' => [ + 'title' => '', + 'version' => '0.0.0', + ], + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'get' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'getDummyCollection', + 'produces' => ['application/ld+json'], + 'summary' => 'Retrieves the collection of Dummy resources.', + 'responses' => [ + 200 => [ + 'description' => 'Dummy collection response', + 'schema' => [ + 'type' => 'array', + 'items' => ['$ref' => '#/definitions/Dummy'], + ], + ], + ], + 'parameters' => [ + [ + 'x-foo' => 'bar', + 'name' => 'name', + 'in' => 'query', + 'required' => true, + 'type' => 'string', + ], + [ + 'name' => 'ha', + 'in' => 'query', + 'required' => false, + 'type' => 'integer', + ], + ], + ]), + ], + ]), + 'definitions' => new \ArrayObject([ + 'Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'properties' => [ + 'name' => new \ArrayObject([ + 'description' => 'This is a name.', + 'type' => 'string', + ]), + ], + ]), + ]), + ]; + + $this->assertEquals($expected, $normalizer->normalize($documentation)); + } }