From 27d38a115f9c0cd80f946108e52ff53bd730066a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 24 Jan 2021 18:18:56 +0100 Subject: [PATCH 01/10] feat: rename master brand to main --- README.md | 7 +++---- composer.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1fcbdb4704e..1a1f9f86358 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,10 @@ It natively supports popular open formats including [JSON for Linked Data (JSON- Build a working and fully-featured CRUD API in minutes. Leverage the awesome features of the tool to develop complex and high performance API-first projects. Extend or override everything you want. -[![GitHub Actions](https://github.com/api-platform/core/workflows/CI/badge.svg?branch=master)](https://github.com/api-platform/core/actions?query=workflow%3ACI+branch%3Amaster) -[![AppVeyor](https://ci.appveyor.com/api/projects/status/grwuyprts3wdqx5l/branch/master?svg=true)](https://ci.appveyor.com/project/dunglas/dunglasapibundle/branch/master) -[![Codecov](https://codecov.io/gh/api-platform/core/branch/master/graph/badge.svg)](https://codecov.io/gh/api-platform/core/branch/master) +[![GitHub Actions](https://github.com/api-platform/core/workflows/CI/badge.svg?branch=main)](https://github.com/api-platform/core/actions?query=workflow%3ACI+branch%3Amain) +[![Codecov](https://codecov.io/gh/api-platform/core/branch/main/graph/badge.svg)](https://codecov.io/gh/api-platform/core/branch/main) [![SymfonyInsight](https://insight.symfony.com/projects/92d78899-946c-4282-89a3-ac92344f9a93/mini.svg)](https://insight.symfony.com/projects/92d78899-946c-4282-89a3-ac92344f9a93) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/api-platform/core/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/api-platform/core/?branch=master) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/api-platform/core/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/api-platform/core/?branch=main) ## Documentation diff --git a/composer.json b/composer.json index 83d4a7dc37f..a4a2e4fa763 100644 --- a/composer.json +++ b/composer.json @@ -124,7 +124,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.6.x-dev" + "dev-main": "2.7.x-dev" }, "symfony": { "require": "^3.4 || ^4.4 || ^5.1" From 41842c430e1eaf23b18d72a7cbc4744324347be4 Mon Sep 17 00:00:00 2001 From: GrimpEreau <47628187+GrimpEreau@users.noreply.github.com> Date: Sat, 30 Jan 2021 09:48:50 +0100 Subject: [PATCH 02/10] Fix: allow to disable OpenAPI, Swagger UI (#3968) * fix: load swagger even when conf is false * fix: change aliases and definitions for swagger disabled in tests * fix: manage necessary services loading * fix typo --- .../DependencyInjection/ApiPlatformExtension.php | 11 ++++++++--- src/Bridge/Symfony/Bundle/Resources/config/api.xml | 2 +- .../DependencyInjection/ApiPlatformExtensionTest.php | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index b6b84eb7387..8110b5bb07c 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -375,13 +375,18 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array { $container->setParameter('api_platform.swagger.versions', $config['swagger']['versions']); - if (empty($config['swagger']['versions'])) { - return; + if (!$config['enable_swagger'] && $config['enable_swagger_ui']) { + throw new RuntimeException('You can not enable the Swagger UI without enabling Swagger, fix this by enabling swagger via the configuration "enable_swagger: true".'); } $loader->load('json_schema.xml'); - $loader->load('swagger.xml'); + + if (!$config['enable_swagger']) { + return; + } + $loader->load('openapi.xml'); + $loader->load('swagger.xml'); $loader->load('swagger-ui.xml'); if (!$config['enable_swagger_ui'] && !$config['enable_re_doc']) { diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 3dac225ca80..4742de28199 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -242,7 +242,7 @@ %api_platform.version% null %api_platform.swagger.versions% - + diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index a9b1ae66bc5..aea8afcb566 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1240,6 +1240,9 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.http_cache.listener.response.configure', 'api_platform.http_cache.purger.varnish_client', 'api_platform.http_cache.purger.varnish', + 'api_platform.json_schema.json_schema_generate_command', + 'api_platform.json_schema.type_factory', + 'api_platform.json_schema.schema_factory', 'api_platform.listener.view.validate', 'api_platform.listener.view.validate_query_parameters', 'api_platform.mercure.listener.response.add_link_header', @@ -1305,9 +1308,6 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo $definitions[] = 'api_platform.swagger.command.swagger_command'; $definitions[] = 'api_platform.swagger.normalizer.api_gateway'; $definitions[] = 'api_platform.swagger.normalizer.documentation'; - $definitions[] = 'api_platform.json_schema.type_factory'; - $definitions[] = 'api_platform.json_schema.schema_factory'; - $definitions[] = 'api_platform.json_schema.json_schema_generate_command'; $definitions[] = 'api_platform.openapi.options'; $definitions[] = 'api_platform.openapi.normalizer'; $definitions[] = 'api_platform.openapi.normalizer.api_gateway'; @@ -1363,6 +1363,8 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo NumericFilter::class => 'api_platform.doctrine.orm.numeric_filter', ExistsFilter::class => 'api_platform.doctrine.orm.exists_filter', GraphQlSerializerContextBuilderInterface::class => 'api_platform.graphql.serializer.context_builder', + TypeFactoryInterface::class => 'api_platform.json_schema.type_factory', + SchemaFactoryInterface::class => 'api_platform.json_schema.schema_factory', ]; if (\in_array('odm', $doctrineIntegrationsToLoad, true)) { @@ -1383,8 +1385,6 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo // Only when swagger is enabled if ($hasSwagger) { $aliases += [ - TypeFactoryInterface::class => 'api_platform.json_schema.type_factory', - SchemaFactoryInterface::class => 'api_platform.json_schema.schema_factory', Options::class => 'api_platform.openapi.options', OpenApiNormalizer::class => 'api_platform.openapi.normalizer', OpenApiFactoryInterface::class => 'api_platform.openapi.factory', From d4abacbcf8f6757d6ae985a60ba42a308fe17b0d Mon Sep 17 00:00:00 2001 From: CheapHasz Date: Thu, 11 Feb 2021 11:26:33 +0100 Subject: [PATCH 03/10] Add mongodb date_immutable filter support (#3940) --- .github/workflows/ci.yml | 1 - CHANGELOG.md | 4 ++ composer.json | 4 +- features/doctrine/date_filter.feature | 1 - .../Doctrine/MongoDbOdm/Filter/DateFilter.php | 1 + .../MongoDbOdm/Filter/SearchFilter.php | 1 + .../PropertyInfo/DoctrineExtractor.php | 2 + tests/Behat/DoctrineContext.php | 11 +++- .../PropertyInfo/DoctrineExtractorTest.php | 2 + .../PropertyInfo/Fixtures/DoctrineDummy.php | 5 ++ .../Document/DummyImmutableDate.php | 53 +++++++++++++++++++ tests/Fixtures/app/config/config_mongodb.yml | 4 ++ 12 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Document/DummyImmutableDate.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d268056ee9..ab21915d65f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -522,7 +522,6 @@ jobs: - name: Update project dependencies run: | composer update --no-interaction --no-progress --ansi - composer why doctrine/reflection # - name: Require Symfony Uid # run: composer require symfony/uid --dev --no-interaction --no-progress --ansi - name: Install PHPUnit diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4e2ed20f5..5145a3c46e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.7.0 + +* MongoDB: `date_immutable` support (#3940) + ## 2.6.1 * Fix defaults when using attributes (#3978) diff --git a/composer.json b/composer.json index a4a2e4fa763..93f236ea7ad 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "doctrine/common": "^2.11 || ^3.0", "doctrine/data-fixtures": "^1.2.2", "doctrine/doctrine-bundle": "^1.12 || ^2.0", - "doctrine/mongodb-odm": "^2.0", + "doctrine/mongodb-odm": "^2.1", "doctrine/mongodb-odm-bundle": "^4.0", "doctrine/orm": "^2.6.4 || ^3.0", "elasticsearch/elasticsearch": "^6.0 || ^7.0", @@ -86,7 +86,7 @@ }, "conflict": { "doctrine/common": "<2.7", - "doctrine/mongodb-odm": "<2.0", + "doctrine/mongodb-odm": "<2.1", "doctrine/persistence": "<1.3" }, "suggest": { diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature index 067fbb14745..7d8d1a906f8 100644 --- a/features/doctrine/date_filter.feature +++ b/features/doctrine/date_filter.feature @@ -506,7 +506,6 @@ Feature: Date filter on collections And the JSON node "hydra:member[0].dateIncludeNullBeforeAndAfter" should be equal to "2015-04-02T00:00:00+00:00" And the JSON node "hydra:member[1].dateIncludeNullBeforeAndAfter" should be null - @!mongodb @createSchema Scenario: Get collection filtered by date that is an immutable date variant Given there are 30 dummyimmutabledate objects with dummyDate diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php index 275a153e369..8904bf570b7 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php @@ -34,6 +34,7 @@ class DateFilter extends AbstractFilter implements DateFilterInterface public const DOCTRINE_DATE_TYPES = [ MongoDbType::DATE => true, + MongoDbType::DATE_IMMUTABLE => true, ]; /** diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php index 91cb23dc3cf..b75b2b310ea 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php @@ -202,6 +202,7 @@ protected function getType(string $doctrineType): string case MongoDbType::BOOLEAN: return 'bool'; case MongoDbType::DATE: + case MongoDbType::DATE_IMMUTABLE: return \DateTimeInterface::class; case MongoDbType::FLOAT: return 'float'; diff --git a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php index 3325392b5da..6538bce9cf0 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php +++ b/src/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractor.php @@ -92,6 +92,8 @@ public function getTypes($class, $property, array $context = []) switch ($typeOfField) { case MongoDbType::DATE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; + case MongoDbType::DATE_IMMUTABLE: + return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; case MongoDbType::HASH: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case MongoDbType::COLLECTION: diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 3d5754c4f34..db2405e5daa 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -45,6 +45,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputSameClass as DummyDtoOutputSameClassDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyImmutableDate as DummyImmutableDateDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyMercure as DummyMercureDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyOffer as DummyOfferDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyProduct as DummyProductDocument; @@ -1270,7 +1271,7 @@ public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb) { for ($i = 1; $i <= $nb; ++$i) { $date = new \DateTimeImmutable(sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')); - $dummy = new DummyImmutableDate(); + $dummy = $this->buildDummyImmutableDate(); $dummy->dummyDate = $date; $this->manager->persist($dummy); @@ -1801,6 +1802,14 @@ private function buildDummyDate() return $this->isOrm() ? new DummyDate() : new DummyDateDocument(); } + /** + * @return DummyImmutableDate|DummyImmutableDateDocument + */ + private function buildDummyImmutableDate() + { + return $this->isOrm() ? new DummyImmutableDate() : new DummyImmutableDateDocument(); + } + /** * @return DummyDifferentGraphQlSerializationGroup|DummyDifferentGraphQlSerializationGroupDocument */ diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php index 36505c4dc9c..1638b933ea9 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php @@ -52,6 +52,7 @@ public function testGetProperties(): void 'binUuidRfc4122', 'timestamp', 'date', + 'dateImmutable', 'float', 'bool', 'boolean', @@ -141,6 +142,7 @@ public function typesProvider(): array ['binUuidRfc4122', [new Type(Type::BUILTIN_TYPE_STRING)]], ['timestamp', [new Type(Type::BUILTIN_TYPE_STRING)]], ['date', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], + ['dateImmutable', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], ['float', [new Type(Type::BUILTIN_TYPE_FLOAT)]], ['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]], ['boolean', [new Type(Type::BUILTIN_TYPE_BOOL)]], diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php index 64a34ed4f65..e54438a4190 100644 --- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php +++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php @@ -91,6 +91,11 @@ class DoctrineDummy */ private $date; + /** + * @Field(type="date_immutable") + */ + private $dateImmutable; + /** * @Field(type="float") */ diff --git a/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php new file mode 100644 index 00000000000..6bc16ea97c4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Dummy Date Immutable. + * + * @ApiResource(attributes={ + * "filters"={"my_dummy_immutable_date.mongodb.date"} + * }) + * + * @ODM\Document + */ +class DummyImmutableDate +{ + /** + * @var int The id + * + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @var \DateTimeImmutable The dummy date + * + * @ODM\Field(type="date_immutable") + */ + public $dummyDate; + + /** + * Get id. + * + * @return int + */ + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index 15565a806d3..747bd44724e 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -60,6 +60,10 @@ services: parent: 'api_platform.doctrine_mongodb.odm.date_filter' arguments: [ { 'dummyDate': ~ } ] tags: [ { name: 'api_platform.filter', id: 'my_dummy_date.mongodb.date' } ] + app.my_dummy_immutable_date_resource.mongodb.date_filter: + parent: 'api_platform.doctrine_mongodb.odm.date_filter' + arguments: [ { 'dummyDate': ~ } ] + tags: [ { name: 'api_platform.filter', id: 'my_dummy_immutable_date.mongodb.date' } ] app.related_dummy_to_friend_resource.mongodb.search_filter: parent: 'api_platform.doctrine_mongodb.odm.search_filter' arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ] From e68be3bb78b0644df04e748bbf1ce005665aa293 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Tue, 16 Feb 2021 15:53:43 +0100 Subject: [PATCH 04/10] [DoctrineBridge][UX] Better Doctrine exceptions (#3965) --- CHANGELOG.md | 1 + .../Common/Util/IdentifierManagerTrait.php | 18 +++++++- src/Bridge/Doctrine/Orm/ItemDataProvider.php | 4 +- .../Doctrine/Orm/SubresourceDataProvider.php | 4 +- .../Bundle/Resources/config/doctrine_orm.xml | 2 + .../Util/IdentifierManagerTraitTest.php | 37 +++++++++------- .../Doctrine/Orm/ItemDataProviderTest.php | 30 +++++++------ .../Orm/SubresourceDataProviderTest.php | 42 ++++++++++--------- 8 files changed, 86 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0714fcb2e39..5a645b4e61b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.7.0 +* Doctrine: Better exception to find which resource is linked to an exception (#3965) * MongoDB: `date_immutable` support (#3940) ## 2.6.3 diff --git a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php index f33eca72ffa..d164fce4a7e 100644 --- a/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php +++ b/src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php @@ -15,6 +15,7 @@ use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\PropertyNotFoundException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ODM\MongoDB\DocumentManager; @@ -29,6 +30,7 @@ trait IdentifierManagerTrait { private $propertyNameCollectionFactory; private $propertyMetadataFactory; + private $resourceMetadataFactory; /** * Transform and check the identifier, composite or not. @@ -74,7 +76,13 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou $identifier = null === $identifiersMap ? $identifierValues[$i] ?? null : $identifiersMap[$propertyName] ?? null; if (null === $identifier) { - throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" was not found.', $id, $propertyName)); + $exceptionMessage = sprintf('Invalid identifier "%s", "%s" was not found', $id, $propertyName); + if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $exceptionMessage .= sprintf(' for resource "%s"', $resourceMetadata->getShortName()); + } + + throw new PropertyNotFoundException($exceptionMessage.'.'); } $doctrineTypeName = $doctrineClassMetadata->getTypeOfField($propertyName); @@ -87,7 +95,13 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou $identifier = MongoDbType::getType($doctrineTypeName)->convertToPHPValue($identifier); } } catch (ConversionException $e) { - throw new InvalidIdentifierException(sprintf('Invalid value "%s" provided for an identifier.', $propertyName), $e->getCode(), $e); + $exceptionMessage = sprintf('Invalid value "%s" provided for an identifier', $propertyName); + if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $exceptionMessage .= sprintf(' for resource "%s"', $resourceMetadata->getShortName()); + } + + throw new InvalidIdentifierException($exceptionMessage.'.', $e->getCode(), $e); } $identifiers[$propertyName] = $identifier; diff --git a/src/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Bridge/Doctrine/Orm/ItemDataProvider.php index 9535b162427..e36cd8c79c8 100644 --- a/src/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -23,6 +23,7 @@ use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -45,11 +46,12 @@ class ItemDataProvider implements DenormalizedIdentifiersAwareItemDataProviderIn /** * @param QueryItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $itemExtensions = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->managerRegistry = $managerRegistry; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->itemExtensions = $itemExtensions; } diff --git a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php index e3571d9880b..264cf65d84c 100644 --- a/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php +++ b/src/Bridge/Doctrine/Orm/SubresourceDataProvider.php @@ -26,6 +26,7 @@ use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\QueryBuilder; @@ -48,11 +49,12 @@ final class SubresourceDataProvider implements SubresourceDataProviderInterface * @param QueryCollectionExtensionInterface[] $collectionExtensions * @param QueryItemExtensionInterface[] $itemExtensions */ - public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = []) + public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->managerRegistry = $managerRegistry; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->resourceMetadataFactory = $resourceMetadataFactory; $this->collectionExtensions = $collectionExtensions; $this->itemExtensions = $itemExtensions; } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml index f60b7d564af..e18d751f2b6 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -25,6 +25,7 @@ + @@ -33,6 +34,7 @@ + diff --git a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php index 99c9d3d5abc..6e8fe0b97d9 100644 --- a/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php +++ b/tests/Bridge/Doctrine/Common/Util/IdentifierManagerTraitTest.php @@ -20,6 +20,8 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\ProphecyTrait; @@ -39,17 +41,18 @@ class IdentifierManagerTraitTest extends TestCase { use ProphecyTrait; - private function getIdentifierManagerTraitImpl(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) + private function getIdentifierManagerTraitImpl(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory) { - return new class($propertyNameCollectionFactory, $propertyMetadataFactory) { + return new class($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory) { use IdentifierManagerTrait { IdentifierManagerTrait::normalizeIdentifiers as public; } - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory) { $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->resourceMetadataFactory = $resourceMetadataFactory; } }; } @@ -59,7 +62,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName */ public function testSingleIdentifier() { - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'id', ]); $objectManager = $this->getEntityManager(Dummy::class, [ @@ -68,7 +71,7 @@ public function testSingleIdentifier() ], ]); - $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory); $this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, Dummy::class), ['id' => 1]); } @@ -79,7 +82,7 @@ public function testSingleIdentifier() */ public function testSingleDocumentIdentifier() { - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(DummyDocument::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(DummyDocument::class, [ 'id', ]); $objectManager = $this->getDocumentManager(DummyDocument::class, [ @@ -88,7 +91,7 @@ public function testSingleDocumentIdentifier() ], ]); - $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory); $this->assertEquals($identifierManager->normalizeIdentifiers(1, $objectManager, DummyDocument::class), ['id' => 1]); } @@ -98,7 +101,7 @@ public function testSingleDocumentIdentifier() */ public function testCompositeIdentifier() { - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'ida', 'idb', ]); @@ -111,7 +114,7 @@ public function testCompositeIdentifier() ], ]); - $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory); $this->assertEquals($identifierManager->normalizeIdentifiers('ida=1;idb=2', $objectManager, Dummy::class), ['ida' => 1, 'idb' => 2]); } @@ -122,9 +125,9 @@ public function testCompositeIdentifier() public function testInvalidIdentifier() { $this->expectException(PropertyNotFoundException::class); - $this->expectExceptionMessage('Invalid identifier "idbad=1;idb=2", "ida" was not found.'); + $this->expectExceptionMessage('Invalid identifier "idbad=1;idb=2", "ida" was not found for resource "dummy".'); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'ida', 'idb', ]); @@ -137,7 +140,7 @@ public function testInvalidIdentifier() ], ]); - $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory); $identifierManager->normalizeIdentifiers('idbad=1;idb=2', $objectManager, Dummy::class); } @@ -149,6 +152,7 @@ private function getMetadataFactories(string $resourceClass, array $identifiers) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $nameCollection = ['foobar']; @@ -164,8 +168,9 @@ private function getMetadataFactories(string $resourceClass, array $identifiers) $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); + $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy')); - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; + return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()]; } /** @@ -218,9 +223,9 @@ public function testInvalidIdentifierConversion() DBALType::addType('uuid', UuidType::class); $this->expectException(InvalidIdentifierException::class); - $this->expectExceptionMessage('Invalid value "ida" provided for an identifier.'); + $this->expectExceptionMessage('Invalid value "ida" provided for an identifier for resource "dummy".'); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'ida', ]); $objectManager = $this->getEntityManager(Dummy::class, [ @@ -229,7 +234,7 @@ public function testInvalidIdentifierConversion() ], ]); - $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory); + $identifierManager = $this->getIdentifierManagerTraitImpl($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory); $identifierManager->normalizeIdentifiers('notanuuid', $objectManager, Dummy::class); } diff --git a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php index e356de369ae..a17dd694fdb 100644 --- a/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/ItemDataProviderTest.php @@ -24,6 +24,8 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\ProphecyTrait; use Doctrine\DBAL\Connection; @@ -69,7 +71,7 @@ public function testGetItemSingleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'id', ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, [ @@ -81,7 +83,7 @@ public function testGetItemSingleIdentifier() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['id' => 1], 'foo', $context)->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory); $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); } @@ -109,7 +111,7 @@ public function testGetItemDoubleIdentifier() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'ida', 'idb', ]); @@ -126,7 +128,7 @@ public function testGetItemDoubleIdentifier() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory); $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['ida' => 1, 'idb' => 2], 'foo', $context)); } @@ -138,7 +140,7 @@ public function testGetItemWrongCompositeIdentifier() { $this->expectException(PropertyNotFoundException::class); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'ida', 'idb', ]); @@ -151,7 +153,7 @@ public function testGetItemWrongCompositeIdentifier() ], ], $this->prophesize(QueryBuilder::class)->reveal()); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [], $resourceMetadataFactory); $dataProvider->getItem(Dummy::class, 'ida=1;', 'foo'); } @@ -171,7 +173,7 @@ public function testQueryResultExtension() $queryBuilder = $queryBuilderProphecy->reveal(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'id', ]); $managerRegistry = $this->getManagerRegistry(Dummy::class, [ @@ -186,7 +188,7 @@ public function testQueryResultExtension() $extensionProphecy->supportsResult(Dummy::class, 'foo', $context)->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($queryBuilder, Dummy::class, 'foo', $context)->willReturn([])->shouldBeCalled(); - $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory); $this->assertEquals([], $dataProvider->getItem(Dummy::class, ['id' => 1], 'foo', $context)); } @@ -198,11 +200,11 @@ public function testUnsupportedClass() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'id', ]); - $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory); $this->assertFalse($dataProvider->supports(Dummy::class, 'foo')); } @@ -233,11 +235,11 @@ public function testCannotCreateQueryBuilder() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataFactories(Dummy::class, [ 'id', ]); - $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $itemDataProvider = new ItemDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], $resourceMetadataFactory); $itemDataProvider->getItem(Dummy::class, ['id' => 1234], null, [IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]); } @@ -248,6 +250,7 @@ private function getMetadataFactories(string $resourceClass, array $identifiers) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $nameCollection = ['foobar']; @@ -263,8 +266,9 @@ private function getMetadataFactories(string $resourceClass, array $identifiers) $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); + $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy')); - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; + return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()]; } /** diff --git a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php index d9e49037bef..cf8791f18ce 100644 --- a/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/SubresourceDataProviderTest.php @@ -23,6 +23,8 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedOwningDummy; @@ -67,6 +69,7 @@ private function getMetadataProphecies(array $resourceClassesIdentifiers) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); foreach ($resourceClassesIdentifiers as $resourceClass => $identifiers) { $nameCollection = ['foobar']; @@ -81,11 +84,12 @@ private function getMetadataProphecies(array $resourceClassesIdentifiers) //random property to prevent the use of non-identifiers metadata while looping $propertyMetadataFactoryProphecy->create($resourceClass, 'foobar')->willReturn(new PropertyMetadata()); + $resourceMetadataFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadata('dummy')); $propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection($nameCollection)); } - return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()]; + return [$propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()]; } private function getManagerRegistryProphecy(QueryBuilder $queryBuilder, array $identifiers, string $resourceClass) @@ -112,11 +116,11 @@ public function testNotASubresource() $this->expectExceptionMessage('The given resource class is not a subresource.'); $identifiers = ['id']; - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); $queryBuilder = $this->prophesize(QueryBuilder::class)->reveal(); $managerRegistry = $this->getManagerRegistryProphecy($queryBuilder, $identifiers, Dummy::class); - $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, []); + $dataProvider = new SubresourceDataProvider($managerRegistry, $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -170,9 +174,9 @@ public function testGetSubresource() $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $context = ['property' => 'relatedDummies', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -264,9 +268,9 @@ public function testGetSubSubresourceItem() $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $context = ['property' => 'thirdLevel', 'identifiers' => ['id' => [Dummy::class, 'id'], 'relatedDummies' => [RelatedDummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -320,9 +324,9 @@ public function testGetSubresourceOneToOneOwningRelation() $managerRegistryProphecy->getManagerForClass(RelatedOwningDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $context = ['property' => 'ownedDummy', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -375,14 +379,14 @@ public function testQueryResultExtension() $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); $managerRegistryProphecy->getManagerForClass(Dummy::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); $extensionProphecy = $this->prophesize(QueryResultCollectionExtensionInterface::class); $extensionProphecy->applyToCollection($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), RelatedDummy::class, null, Argument::type('array'))->shouldBeCalled(); $extensionProphecy->supportsResult(RelatedDummy::class, null, Argument::type('array'))->willReturn(true)->shouldBeCalled(); $extensionProphecy->getResult($queryBuilder, RelatedDummy::class, null, Argument::type('array'))->willReturn([])->shouldBeCalled(); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()]); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [$extensionProphecy->reveal()], [], $resourceMetadataFactory); $context = ['property' => 'relatedDummies', 'identifiers' => ['id' => [Dummy::class, 'id']], 'collection' => true, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; @@ -403,9 +407,9 @@ public function testCannotCreateQueryBuilder() $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($managerProphecy->reveal())->shouldBeCalled(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -417,9 +421,9 @@ public function testThrowResourceClassNotSupportedException() $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn(null)->shouldBeCalled(); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $dataProvider->getSubresource(Dummy::class, ['id' => 1], []); } @@ -514,9 +518,9 @@ public function testGetSubSubresourceItemLegacy() $managerRegistryProphecy->getManagerForClass(ThirdLevel::class)->shouldBeCalled()->willReturn($managerProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $context = ['property' => 'thirdLevel', 'identifiers' => [['id', Dummy::class], ['relatedDummies', RelatedDummy::class]], 'collection' => false]; @@ -604,9 +608,9 @@ public function testGetSubresourceCollectionItem() $rDummyManagerProphecy->getRepository(RelatedDummy::class)->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); - [$propertyNameCollectionFactory, $propertyMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); + [$propertyNameCollectionFactory, $propertyMetadataFactory, $resourceMetadataFactory] = $this->getMetadataProphecies([Dummy::class => $identifiers, RelatedDummy::class => $identifiers]); - $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory); + $dataProvider = new SubresourceDataProvider($managerRegistryProphecy->reveal(), $propertyNameCollectionFactory, $propertyMetadataFactory, [], [], $resourceMetadataFactory); $context = ['property' => 'id', 'identifiers' => ['id' => [Dummy::class, 'id', true], 'relatedDummies' => [RelatedDummy::class, 'id', true]], 'collection' => false, IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER => true]; From 959f0829747eb733f496b5ddc6508b433b4f6792 Mon Sep 17 00:00:00 2001 From: Minh Vuong <38932626+vuongxuongminh@users.noreply.github.com> Date: Tue, 16 Feb 2021 22:16:19 +0700 Subject: [PATCH 05/10] Allow mixed type value when apply to DateFilter (#3870) --- CHANGELOG.md | 1 + .../Doctrine/Common/Filter/DateFilterTrait.php | 14 ++++++++++++++ .../Doctrine/MongoDbOdm/Filter/DateFilter.php | 8 +++++++- src/Bridge/Doctrine/Orm/Filter/DateFilter.php | 8 +++++++- .../Doctrine/Common/Filter/DateFilterTestTrait.php | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a645b4e61b..129c0dadcf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.7.0 * Doctrine: Better exception to find which resource is linked to an exception (#3965) +* Doctrine: Allow mixed type value for date filter (notice if invalid) (#3870) * MongoDB: `date_immutable` support (#3940) ## 2.6.3 diff --git a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php index 82e5db681f4..fbbd3bf7f2e 100644 --- a/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php +++ b/src/Bridge/Doctrine/Common/Filter/DateFilterTrait.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Common\Filter; use ApiPlatform\Core\Bridge\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Core\Exception\InvalidArgumentException; /** * Trait for filtering the collection by date intervals. @@ -79,4 +80,17 @@ protected function getFilterDescription(string $property, string $period): array ], ]; } + + private function normalizeValue($value, string $operator): ?string + { + if (false === \is_string($value)) { + $this->getLogger()->notice('Invalid filter ignored', [ + 'exception' => new InvalidArgumentException(sprintf('Invalid value for "[%s]", expected string', $operator)), + ]); + + return null; + } + + return $value; + } } diff --git a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php index 8904bf570b7..6c6abf3ccf7 100644 --- a/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/MongoDbOdm/Filter/DateFilter.php @@ -108,8 +108,14 @@ protected function filterProperty(string $property, $values, Builder $aggregatio /** * Adds the match stage according to the chosen null management. */ - private function addMatch(Builder $aggregationBuilder, string $field, string $operator, string $value, string $nullManagement = null): void + private function addMatch(Builder $aggregationBuilder, string $field, string $operator, $value, string $nullManagement = null): void { + $value = $this->normalizeValue($value, $operator); + + if (null === $value) { + return; + } + try { $value = new \DateTime($value); } catch (\Exception $e) { diff --git a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php index 0b619be243e..9970eadf3b4 100644 --- a/src/Bridge/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Bridge/Doctrine/Orm/Filter/DateFilter.php @@ -128,9 +128,15 @@ protected function filterProperty(string $property, $values, QueryBuilder $query * * @param string|DBALType $type */ - protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, string $value, string $nullManagement = null, $type = null) + protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, string $operator, $value, string $nullManagement = null, $type = null) { $type = (string) $type; + $value = $this->normalizeValue($value, $operator); + + if (null === $value) { + return; + } + try { $value = false === strpos($type, '_immutable') ? new \DateTime($value) : new \DateTimeImmutable($value); } catch (\Exception $e) { diff --git a/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php b/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php index 490960d9bb8..7d680d26d54 100644 --- a/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php +++ b/tests/Bridge/Doctrine/Common/Filter/DateFilterTestTrait.php @@ -220,6 +220,7 @@ private function provideApplyTestArguments(): array [ 'dummyDate' => [ 'after' => '1932iur123ufqe', + 'before' => [], ], ], ], From 3557329f15da2ae3d61cc328a28e45929697d89d Mon Sep 17 00:00:00 2001 From: Toby Griffiths Date: Wed, 17 Feb 2021 17:51:36 +0000 Subject: [PATCH 06/10] Add traversable paginator (#3783) --- CHANGELOG.md | 1 + src/DataProvider/TraversablePaginator.php | 90 +++++++++++++++++++ .../DataProvider/TraversablePaginatorTest.php | 56 ++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/DataProvider/TraversablePaginator.php create mode 100644 tests/DataProvider/TraversablePaginatorTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 129c0dadcf7..f2ee9d43f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Doctrine: Better exception to find which resource is linked to an exception (#3965) * Doctrine: Allow mixed type value for date filter (notice if invalid) (#3870) * MongoDB: `date_immutable` support (#3940) +* Add `TraversablePaginator` (#3783) ## 2.6.3 diff --git a/src/DataProvider/TraversablePaginator.php b/src/DataProvider/TraversablePaginator.php new file mode 100644 index 00000000000..91635bc1fb4 --- /dev/null +++ b/src/DataProvider/TraversablePaginator.php @@ -0,0 +1,90 @@ + + * + * 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\DataProvider; + +final class TraversablePaginator implements \IteratorAggregate, PaginatorInterface +{ + private $traversable; + private $currentPage; + private $itemsPerPage; + private $totalItems; + + public function __construct(\Traversable $iterator, float $currentPage, float $itemsPerPage, float $totalItems) + { + $this->traversable = $iterator; + $this->currentPage = $currentPage; + $this->itemsPerPage = $itemsPerPage; + $this->totalItems = $totalItems; + } + + /** + * {@inheritdoc} + */ + public function getCurrentPage(): float + { + return $this->currentPage; + } + + /** + * {@inheritdoc} + */ + public function getLastPage(): float + { + if (0. >= $this->itemsPerPage) { + return 1.; + } + + return max(ceil($this->totalItems / $this->itemsPerPage) ?: 1., 1.); + } + + /** + * {@inheritdoc} + */ + public function getItemsPerPage(): float + { + return $this->itemsPerPage; + } + + /** + * {@inheritdoc} + */ + public function getTotalItems(): float + { + return $this->totalItems; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + if ($this->getCurrentPage() < $this->getLastPage()) { + return (int) ceil($this->itemsPerPage); + } + + if (0. >= $this->itemsPerPage) { + return (int) ceil($this->totalItems); + } + + return $this->totalItems % $this->itemsPerPage; + } + + /** + * {@inheritdoc} + */ + public function getIterator(): \Traversable + { + return $this->traversable; + } +} diff --git a/tests/DataProvider/TraversablePaginatorTest.php b/tests/DataProvider/TraversablePaginatorTest.php new file mode 100644 index 00000000000..ac96262a102 --- /dev/null +++ b/tests/DataProvider/TraversablePaginatorTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\DataProvider; + +use ApiPlatform\Core\DataProvider\TraversablePaginator; +use ArrayIterator; +use PHPUnit\Framework\TestCase; + +class TraversablePaginatorTest extends TestCase +{ + /** + * @dataProvider initializeProvider + */ + public function testInitialize( + array $results, + float $currentPage, + float $perPage, + float $totalItems, + float $lastPage, + float $currentItems + ): void { + $traversable = new ArrayIterator($results); + + $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); + + self::assertEquals($totalItems, $paginator->getTotalItems()); + self::assertEquals($currentPage, $paginator->getCurrentPage()); + self::assertEquals($lastPage, $paginator->getLastPage()); + self::assertEquals($perPage, $paginator->getItemsPerPage()); + self::assertEquals($currentItems, $paginator->count()); + + self::assertSame($results, iterator_to_array($paginator)); + } + + public function initializeProvider(): array + { + return [ + 'First of three pages of 3 items each' => [[0, 1, 2, 3, 4, 5, 6], 1, 3, 7, 3, 3], + 'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 2, 3, 5, 2, 2], + 'Empty results' => [[], 1, 2, 0, 1, 0], + '0 items per page' => [[0, 1, 2, 3], 1, 0, 4, 1, 4], + 'Total items less than items per page' => [[0, 1, 2], 1, 4, 3, 1, 3], + ]; + } +} From f3492dcf18d6b2ee5b292959cb5943ad0a75e9d0 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 18 Dec 2020 13:57:20 +0100 Subject: [PATCH 07/10] Resource definition --- docs/adr/0000-subresources-definition.md | 2 +- docs/adr/0002-resource-definition.md | 208 +++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0002-resource-definition.md diff --git a/docs/adr/0000-subresources-definition.md b/docs/adr/0000-subresources-definition.md index 72cccb17e03..062711279fe 100644 --- a/docs/adr/0000-subresources-definition.md +++ b/docs/adr/0000-subresources-definition.md @@ -1,6 +1,6 @@ # Subresource Definition -* Status: proposed +* Status: superseded by [0002-resource-definition](0002-resource-definition.md)] * Deciders: @dunglas, @vincentchalamon, @soyuka, @GregoireHebert, @Deuchnord ## Context and Problem Statement diff --git a/docs/adr/0002-resource-definition.md b/docs/adr/0002-resource-definition.md new file mode 100644 index 00000000000..7cdfd846950 --- /dev/null +++ b/docs/adr/0002-resource-definition.md @@ -0,0 +1,208 @@ +# Resource definition + +* Status: proposed +* Deciders: @dunglas @soyuka @vincentchalamon @GregoireHebert + +## Context and Problem Statement + +The API Platform `@ApiResource` annotation was initially created to represent a Resource as defined in [Roy Fiedling's dissertation about REST](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_2_1_1) in corelation with [RFC 7231 about HTTP Semantics](https://httpwg.org/specs/rfc7231.html#resources). This annotation brings some confusion as it mixes concepts of resources and operations. Here we discussed how we could revamp API Platform's resource definition using PHP8 attributes, beeing as close as we can to Roy Fiedling's thesis vocabulary. + +## Considered Options + +* Declare multiple ApiResource on a PHP Class [see Subresources definition](./0000-subresources-definition.md) +* Declare operations in conjunction with resources using two attributes: `Resource` and `Operation` +* Use HTTP Verb to represent operations with a syntax sugar for collections (`CGET`?) + +## Decision Outcome + +As Roy Fiedling's thesis states: + +> REST uses a resource identifier to identify the particular resource involved in an interaction between components. REST connectors provide a generic interface for accessing and manipulating the value set of a resource, regardless of how the membership function is defined or the type of software that is handling the request. + +In API Platform, this resource identifier is also named [IRI (Internationalized Resource Identifiers)](https://tools.ietf.org/html/rfc3987). Following these recommandation, applied to PHP, we came up with the following [PHP 8 attributes](https://www.php.net/manual/en/language.attributes.php): + +```php + + + + Code + + + Operations + + + + +
+#[Get]
+class User {}
+            
+ + +
  • GET /users/{id}
+ + + + +
+#[GetCollection]
+class User {}
+            
+ + +
  • GET /users
+ + + + +
+#[Get("/users")]
+class User {}
+            
+ + +
  • GET /users
+ + + + +
+#[Post("/users")]
+#[Patch("/users/{id}")]
+class User {}
+            
+ + +
  • POST /users
  • +
  • PATCH /users/{id}
  • + + + + +
    +// See these as aliases to the `/users/{id}` Resource:
    +#[Get("/companies/{companyId}/users/{id}")]
    +#[Delete("/companies/{companyId}/users/{id}")]
    +class User {}
    +            
    + + +
    • GET /companies/{companyId}/users/{id}
    • +
    • DELETE /companies/{companyId}/users/{id}
    • + + + + +To link these operations with identifiers, refer to [Resource Identifiers decision record](./0001-resource-identifiers), for example: + +```php + [Company::class, "id"], "id" => [User::class, "id"]]])] +class User { + #[ApiProperty(identifier=true)] + public $id; + public Company $company; +} +``` + +The `Resource` attribute could be used to set defaults properties on operations: + +```php + Date: Sat, 20 Feb 2021 13:46:44 +0100 Subject: [PATCH 08/10] test: fix mongodb deprecation --- tests/Fixtures/TestBundle/Document/DummyImmutableDate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php index 6bc16ea97c4..d9893b94a3e 100644 --- a/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php +++ b/tests/Fixtures/TestBundle/Document/DummyImmutableDate.php @@ -30,7 +30,7 @@ class DummyImmutableDate /** * @var int The id * - * @ODM\Id(strategy="INCREMENT", type="integer") + * @ODM\Id(strategy="INCREMENT", type="int") */ private $id; From 930c930debe0f14fd7c6a0a46034d9e858f17846 Mon Sep 17 00:00:00 2001 From: Chernosov Denis Date: Tue, 23 Feb 2021 13:34:33 +0400 Subject: [PATCH 09/10] Add `swagger_ui_extra_configuration` to Swagger configuration (#3731) --- CHANGELOG.md | 3 ++- .../Symfony/Bundle/Action/SwaggerUiAction.php | 5 ++++- .../DependencyInjection/ApiPlatformExtension.php | 1 + .../Bundle/DependencyInjection/Configuration.php | 16 ++++++++++++++++ .../Bundle/Resources/config/swagger-ui.xml | 2 ++ .../Bundle/Resources/public/init-swagger-ui.js | 4 ++-- .../Symfony/Bundle/SwaggerUi/SwaggerUiAction.php | 1 + .../Bundle/SwaggerUi/SwaggerUiContext.php | 9 ++++++++- .../Bundle/Action/SwaggerUiActionTest.php | 3 +++ .../ApiPlatformExtensionTest.php | 2 ++ .../DependencyInjection/ConfigurationTest.php | 2 ++ .../Bundle/SwaggerUi/SwaggerUiActionTest.php | 3 +++ 12 files changed, 46 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f1944990e..e0bd71c6aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ * Doctrine: Better exception to find which resource is linked to an exception (#3965) * Doctrine: Allow mixed type value for date filter (notice if invalid) (#3870) * MongoDB: `date_immutable` support (#3940) -* Add `TraversablePaginator` (#3783) +* DataProvider: Add `TraversablePaginator` (#3783) +* Swagger UI: Add `swagger_ui_extra_configuration` to Swagger / OpenAPI configuration (#3731) ## 2.6.3 diff --git a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php index d84a1b2214a..5a03d2baffd 100644 --- a/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php +++ b/src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php @@ -62,12 +62,13 @@ final class SwaggerUiAction private $swaggerVersions; private $swaggerUiAction; private $assetPackage; + private $swaggerUiExtraConfiguration; /** * @param int[] $swaggerVersions * @param mixed|null $assetPackage */ - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3], OpenApiSwaggerUiAction $swaggerUiAction = null, $assetPackage = null) + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3], OpenApiSwaggerUiAction $swaggerUiAction = null, $assetPackage = null, array $swaggerUiExtraConfiguration = []) { $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; $this->resourceMetadataFactory = $resourceMetadataFactory; @@ -93,6 +94,7 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName $this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled; $this->swaggerVersions = $swaggerVersions; $this->swaggerUiAction = $swaggerUiAction; + $this->swaggerUiExtraConfiguration = $swaggerUiExtraConfiguration; $this->assetPackage = $assetPackage; if (null === $this->swaggerUiAction) { @@ -157,6 +159,7 @@ private function getContext(Request $request, Documentation $documentation): arr $swaggerData = [ 'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']), 'spec' => $this->normalizer->normalize($documentation, 'json', $swaggerContext), + 'extraConfiguration' => $this->swaggerUiExtraConfiguration, ]; $swaggerData['oauth'] = [ diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index c330c90905f..8eee94a3990 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -397,6 +397,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']); $container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); + $container->setParameter('api_platform.swagger_ui.extra_configuration', $config['openapi']['swagger_ui_extra_configuration'] ?? $config['swagger']['swagger_ui_extra_configuration']); if (true === $config['openapi']['backward_compatibility_layer']) { $container->getDefinition('api_platform.swagger.normalizer.documentation')->addArgument($container->getDefinition('api_platform.openapi.normalizer')); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index de9a01c80a8..d14ec4533ab 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -339,6 +339,14 @@ private function addSwaggerSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() + ->variableNode('swagger_ui_extra_configuration') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { return false === \is_array($v); }) + ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.') + ->end() + ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.') + ->end() ->end() ->end() ->end(); @@ -492,6 +500,14 @@ private function addOpenApiSection(ArrayNodeDefinition $rootNode): void ->scalarNode('url')->defaultNull()->info('URL to the license used for the API. MUST be in the format of a URL.')->end() ->end() ->end() + ->variableNode('swagger_ui_extra_configuration') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { return false === \is_array($v); }) + ->thenInvalid('The swagger_ui_extra_configuration parameter must be an array.') + ->end() + ->info('To pass extra configuration to Swagger UI, like docExpansion or filter.') + ->end() ->end() ->end() ->end(); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml index c56d3a3440c..f882312a68d 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml @@ -38,6 +38,7 @@ %api_platform.swagger.versions% %api_platform.asset_package% + %api_platform.swagger_ui.extra_configuration% @@ -48,6 +49,7 @@ %api_platform.graphql.graphiql.enabled% %api_platform.graphql.graphql_playground.enabled% %api_platform.asset_package% + %api_platform.swagger_ui.extra_configuration% diff --git a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js index 95f2b63ed5e..f248f92bbc2 100644 --- a/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js +++ b/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js @@ -41,7 +41,7 @@ window.onload = function() { }).observe(document, {childList: true, subtree: true}); const data = JSON.parse(document.getElementById('swagger-data').innerText); - const ui = SwaggerUIBundle({ + const ui = SwaggerUIBundle(Object.assign({ spec: data.spec, dom_id: '#swagger-ui', validatorUrl: null, @@ -54,7 +54,7 @@ window.onload = function() { SwaggerUIBundle.plugins.DownloadUrl, ], layout: 'StandaloneLayout', - }); + }, data.extraConfiguration)); if (data.oauth.enabled) { ui.initOAuth({ diff --git a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php index 81bf60ebb39..c88c89993e1 100644 --- a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php +++ b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php @@ -86,6 +86,7 @@ public function __invoke(Request $request) 'clientId' => $this->oauthClientId, 'clientSecret' => $this->oauthClientSecret, ], + 'extraConfiguration' => $this->swaggerUiContext->getExtraConfiguration(), ]; if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) { diff --git a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php index 29aaf205600..d37f51fcfbe 100644 --- a/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php +++ b/src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php @@ -22,8 +22,9 @@ final class SwaggerUiContext private $graphiQlEnabled; private $graphQlPlaygroundEnabled; private $assetPackage; + private $extraConfiguration; - public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = true, bool $reDocEnabled = false, bool $graphQlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, $assetPackage = null) + public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = true, bool $reDocEnabled = false, bool $graphQlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, $assetPackage = null, array $extraConfiguration = []) { $this->swaggerUiEnabled = $swaggerUiEnabled; $this->showWebby = $showWebby; @@ -32,6 +33,7 @@ public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = tr $this->graphiQlEnabled = $graphiQlEnabled; $this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled; $this->assetPackage = $assetPackage; + $this->extraConfiguration = $extraConfiguration; } public function isSwaggerUiEnabled(): bool @@ -68,4 +70,9 @@ public function getAssetPackage(): ?string { return $this->assetPackage; } + + public function getExtraConfiguration(): array + { + return $this->extraConfiguration; + } } diff --git a/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php b/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php index ae1c6d3391d..ef9e674cc24 100644 --- a/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php +++ b/tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php @@ -89,6 +89,7 @@ public function getInvokeParameters() 'swagger_data' => [ 'url' => '/url', 'spec' => self::SPEC, + 'extraConfiguration' => [], 'oauth' => [ 'enabled' => false, 'clientId' => '', @@ -123,6 +124,7 @@ public function getInvokeParameters() 'swagger_data' => [ 'url' => '/url', 'spec' => self::SPEC, + 'extraConfiguration' => [], 'oauth' => [ 'enabled' => false, 'clientId' => '', @@ -180,6 +182,7 @@ public function testDoNotRunCurrentRequest(Request $request) 'swagger_data' => [ 'url' => '/url', 'spec' => self::SPEC, + 'extraConfiguration' => [], 'oauth' => [ 'enabled' => false, 'clientId' => '', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 38f379dd726..0b000383d13 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -244,6 +244,7 @@ public function testSetNameConverter() $containerBuilderProphecy->hasParameter('kernel.debug')->willReturn(true); $containerBuilderProphecy->getParameter('kernel.debug')->willReturn(false); $containerBuilderProphecy->setAlias('api_platform.name_converter', $nameConverterId)->shouldBeCalled(); + $containerBuilderProphecy->setParameter('api_platform.swagger_ui.extra_configuration', [])->shouldBeCalled(); $containerBuilder = $containerBuilderProphecy->reveal(); @@ -1172,6 +1173,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo if ($hasSwagger) { $parameters['api_platform.swagger.versions'] = [2, 3]; $parameters['api_platform.swagger.api_keys'] = []; + $parameters['api_platform.swagger_ui.extra_configuration'] = []; } else { $parameters['api_platform.swagger.versions'] = []; } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 1df2208bb80..4f544652f20 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -150,6 +150,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'swagger' => [ 'versions' => [2, 3], 'api_keys' => [], + 'swagger_ui_extra_configuration' => [], ], 'eager_loading' => [ 'enabled' => true, @@ -219,6 +220,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'url' => null, ], 'backward_compatibility_layer' => true, + 'swagger_ui_extra_configuration' => [], ], ], $config); } diff --git a/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php b/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php index 126a3b8bc10..d00df4ba2ec 100644 --- a/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php +++ b/tests/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php @@ -102,6 +102,7 @@ public function getInvokeParameters() 'authorizationUrl' => '', 'scopes' => [], ], + 'extraConfiguration' => [], 'shortName' => 'F', 'operationId' => 'getFCollection', 'id' => null, @@ -136,6 +137,7 @@ public function getInvokeParameters() 'authorizationUrl' => '', 'scopes' => [], ], + 'extraConfiguration' => [], 'shortName' => 'F', 'operationId' => 'getFItem', 'id' => null, @@ -188,6 +190,7 @@ public function testDoNotRunCurrentRequest(Request $request) 'authorizationUrl' => '', 'scopes' => [], ], + 'extraConfiguration' => [], ], ])->shouldBeCalled()->willReturn(''); From 07631bcf409f12d14e8cef2fe0fb2c243c937c0a Mon Sep 17 00:00:00 2001 From: Damian Date: Thu, 25 Feb 2021 18:11:27 +0100 Subject: [PATCH 10/10] Allow unset PathItem method After disabling itemOperation `GET` ``` itemOperations: [ 'get' => [ 'controller' => ApiPlatform\Core\Action\NotFoundAction::class, 'read' => false, 'output' => false, ], ] ``` there is not nice way to remove that path from OpenApi docs. Now to do that you have to write: ``` $pathItem = $openApi->getPaths()->getPath('/resource/{id}'); $openApi->getPaths()->addPath( '/resource/{id}', new Model\PathItem( $pathItem->getRef(), $pathItem->getSummary(), $pathItem->getDescription(), null, $pathItem->getPut(), $pathItem->getPost(), $pathItem->getDelete(), $pathItem->getOptions(), $pathItem->getHead(), $pathItem->getPatch(), $pathItem->getTrace(), $pathItem->getServers(), $pathItem->getParameters(), ) ); ``` After change it will be: ``` $pathItem = $openApi->getPaths()->getPath('/resource/{id}'); $openApi->getPaths()->addPath( '/resource/{id}', $pathItem->withGet(null) ); ``` --- src/OpenApi/Model/PathItem.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenApi/Model/PathItem.php b/src/OpenApi/Model/PathItem.php index 5c07c9006c2..fae2c59a0ff 100644 --- a/src/OpenApi/Model/PathItem.php +++ b/src/OpenApi/Model/PathItem.php @@ -138,7 +138,7 @@ public function withDescription(string $description): self return $clone; } - public function withGet(Operation $get): self + public function withGet(?Operation $get): self { $clone = clone $this; $clone->get = $get; @@ -146,7 +146,7 @@ public function withGet(Operation $get): self return $clone; } - public function withPut(Operation $put): self + public function withPut(?Operation $put): self { $clone = clone $this; $clone->put = $put; @@ -154,7 +154,7 @@ public function withPut(Operation $put): self return $clone; } - public function withPost(Operation $post): self + public function withPost(?Operation $post): self { $clone = clone $this; $clone->post = $post; @@ -162,7 +162,7 @@ public function withPost(Operation $post): self return $clone; } - public function withDelete(Operation $delete): self + public function withDelete(?Operation $delete): self { $clone = clone $this; $clone->delete = $delete; @@ -186,7 +186,7 @@ public function withHead(Operation $head): self return $clone; } - public function withPatch(Operation $patch): self + public function withPatch(?Operation $patch): self { $clone = clone $this; $clone->patch = $patch;