diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3dfccfed4d..5dc3d174e07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,28 +13,38 @@
* OpenAPI: Add PHP default values to the documentation (#2386)
* Deprecate using a validation groups generator service not implementing `ApiPlatform\Core\Bridge\Symfony\Validator\ValidationGroupsGeneratorInterface` (#3346)
+## 2.5.6
+
+* Add support for Mercure 0.10 (#3584)
+* Allow objects without properties (#3544)
+* Fix Ramsey uuid denormalization (#3473)
+* Revert #3331 as it breaks backwards compatibility
+* Handle deprecations from Doctrine Inflector (#3564)
+* JSON Schema: Missing JSON-LD context from Data Transformers (#3479)
+* GraphQL: Resource with no operations should be available through relations (#3532)
+
## 2.5.5
-* Filter: Improve the RangeFilter query in case the values are equals using the between operator #3488
-* Pagination: Fix bug with large values #3451
-* Doctrine: use the correct type within `setParameter` of the SearchFilter #3331
-* Allow `\Traversable` resources #3463
-* Hydra: `hydra:writable` => `hydra:writeable` #3481
-* Hydra: Show `hydra:next` only when it's available #3457
-* Swagger UI: Missing default context argument #3443
-* Swagger UI: Fix API docs path in swagger ui #3475
-* OpenAPI: Export with unescaped slashes #3368
-* OpenAPI: OAuth flows fix #3333
-* JSON Schema: Fix metadata options #3425
-* JSON Schema: Allow decoration #3417
-* JSON Schema: Add DateInterval type #3351
-* JSON Schema: Correct schema generation for many types #3402
-* Validation: Use API Platform's `ValidationException` instead of Symfony's #3414
-* Validation: Fix a bug preventing to serialize validator's payload #3375
-* Subresources: Improve queries when there's only one level #3396
-* HTTP: Location header is only set on POST with a 201 or between 300 and 400 #3497
-* GraphQL: Do not allow empty cursor values on `before` or `after` #3360
-* Bump versions of Swagger UI, GraphiQL and GraphQL Playground #3510
+* Filter: Improve the RangeFilter query in case the values are equals using the between operator (#3488)
+* Pagination: Fix bug with large values (#3451)
+* Doctrine: use the correct type within `setParameter` of the SearchFilter (#3331)
+* Allow `\Traversable` resources (#3463)
+* Hydra: `hydra:writable` => `hydra:writeable` (#3481)
+* Hydra: Show `hydra:next` only when it's available (#3457)
+* Swagger UI: Missing default context argument (#3443)
+* Swagger UI: Fix API docs path in swagger ui (#3475)
+* OpenAPI: Export with unescaped slashes (#3368)
+* OpenAPI: OAuth flows fix (#3333)
+* JSON Schema: Fix metadata options (#3425)
+* JSON Schema: Allow decoration (#3417)
+* JSON Schema: Add DateInterval type (#3351)
+* JSON Schema: Correct schema generation for many types (#3402)
+* Validation: Use API Platform's `ValidationException` instead of Symfony's (#3414)
+* Validation: Fix a bug preventing to serialize validator's payload (#3375)
+* Subresources: Improve queries when there's only one level (#3396)
+* HTTP: Location header is only set on POST with a 201 or between 300 and 400 (#3497)
+* GraphQL: Do not allow empty cursor values on `before` or `after` (#3360)
+* Bump versions of Swagger UI, GraphiQL and GraphQL Playground (#3510)
## 2.5.4
@@ -60,7 +70,7 @@
* Compatibility with Symfony 5 beta
* Fix a notice in `SerializerContextBuilder`
* Fix dashed path segment generation
-* Fix support for custom filters without constructor in the `@ApiFilter` annotation
+* Fix support for custom filters without constructors in the `@ApiFilter` annotation
* Fix a bug that was preventing to disable Swagger/OpenAPI
* Return a `404` HTTP status code instead of `500` whe the identifier is invalid (e.g.: invalid UUID)
* Add links to the documentation in `@ApiResource` annotation's attributes to improve DX
@@ -88,9 +98,9 @@
* Allow to not declare GET item operation
* Add support for the Accept-Patch header
-* Make the the `maximum_items_per_page` attribute consistent with other attributes controlling pagination
+* Make the `maximum_items_per_page` attribute consistent with other attributes controlling pagination
* Allow to use a string instead of an array for serializer groups
-* Test: Add an helper method to find the IRI of a resource
+* Test: Add a helper method to find the IRI of a resource
* Test: Add assertions for testing response against JSON Schema from API resource
* GraphQL: Add support for multipart request so user can create custom file upload mutations (#3041)
* GraphQL: Add support for name converter (#2765)
@@ -103,7 +113,7 @@
* Add infrastructure to generate a JSON Schema from a Resource `ApiPlatform\Core\JsonSchema\SchemaFactoryInterface` (#2983)
* Replaces `access_control` by `security` and adds a `security_post_denormalize` attribute (#2992)
* Add basic infrastructure for cursor-based pagination (#2532)
-* Change ExistsFilter syntax to `exists[property]`, old syntax still supported see #2243, fixes it's behavior on GraphQL (also related #2640).
+* Change ExistsFilter syntax to `exists[property]`, old syntax still supported see #2243, fixes its behavior on GraphQL (also related #2640).
* Pagination with subresources (#2698)
* Improve search filter id's management (#1844)
* Add support of name converter in filters (#2751, #2897), filter signature in abstract methods has changed see b42dfd198b1644904fd6a684ab2cedaf530254e3
@@ -147,10 +157,10 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Varnish: Prevent cache miss by generating IRI for child related resources
* Messenger: Unwrap exception thrown in handler for Symfony Messenger 4.3
* Fix remaining Symfony 4.3 deprecation notices
-* Prevent cloning non clonable objects in `previous_data`
+* Prevent cloning non cloneable objects in `previous_data`
* Return a 415 HTTP status code instead of a 406 one when a faulty `Content-Type` is sent
* Fix `WriteListener` trying to generate IRI for non-resources
-* Allow to extract blank values from composite identifier
+* Allow extracting blank values from composite identifier
## 2.4.5
@@ -318,18 +328,18 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* OpenAPI: support generating documentation using [ReDoc](https://github.com/Rebilly/ReDoc)
* OpenAPI: basic hypermedia hints using OpenAPI v3 links
* OpenAPI: expose the pagination controls
-* Allow to use custom classes for input and output (DTO) with the `input_class` and `output_class` attributes
-* Allow to disable the input or the output by setting `input_class` and `output_class` to false
+* Allow using custom classes for input and output (DTO) with the `input_class` and `output_class` attributes
+* Allow disabling the input or the output by setting `input_class` and `output_class` to false
* Guess and automatically set the appropriate Schema.org IRIs for common validation constraints
-* Allow to set custom cache HTTP headers using the `cache_headers` attribute
-* Allow to set the HTTP status code to send to the client through the `status` attribute
+* Allow setting custom cache HTTP headers using the `cache_headers` attribute
+* Allow setting the HTTP status code to send to the client through the `status` attribute
* Add support for the `Sunset` HTTP header using the `sunset` attribute
* Set the `Content-Location` and `Location` headers when appropriate for better RFC7231 conformance
* Display the matching data provider and data persister in the debug panel
* GraphQL: improve performance by lazy loading types
* Add the `api_persist` request attribute to enable or disable the `WriteListener`
-* Allow to set a default context in all normalizers
-* Permit to use a string instead of an array when there is only one serialization group
+* Allow setting a default context in all normalizers
+* Permit using a string instead of an array when there is only one serialization group
* Add support for setting relations using the constructor of the resource classes
* Automatically set a [409 Conflict](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) HTTP status code when an `OptimisticLockException` is thrown
* Resolve Dependency Injection Container parameters in the XML and YAML files for the resource class configuration
@@ -354,7 +364,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* OpenAPI/Swagger: add a description for the `properties[]` filter
* OpenAPI/Swagger: Leverage advanced name converters
* JSON-LD: Prevent an error in `ItemNormalizer` when `$context['resource_class']` is not defined
-* Allow to pass a the serialization group to use a string instead of as an array of one element
+* Allow to pass the serialization group to use a string instead of as an array of one element
* Modernize the code base to use PHP 7.1 features when possible
* Bump minimal dependencies of the used Symfony components
* Improve the Packagist description
@@ -373,7 +383,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Throw an `InvalidArgumentException` when trying to get an item from a collection route
* Improve the debug bar panel visibility
* Take into account the `route_prefix` attribute in subresources
-* Allow to use multiple values with `NumericFilter`
+* Allow using multiple values with `NumericFilter`
* Improve exception handling in `ReadListener` by adding the previous exception
## 2.3.3
@@ -412,7 +422,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Add support for deprecating resources, operations and fields in GraphQL, Hydra and Swagger
* Add API Platform panels in the Symfony profiler and in the web debug toolbar
* Make resource class's constructor parameters writable
-* Add support for interface as a resource
+* Add support for interfaces as resources
* Add a shortcut syntax to define attributes at the root of `@ApiResource` and `@ApiProperty` annotations
* Throw an exception if a required filter isn't set
* Allow to specify the message when access is denied using the `access_control_message` attribute
@@ -487,7 +497,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
## 2.2.5
-* Fix a various issues preventing the metadata cache to work properly (performance fix)
+* Fix various issues preventing the metadata cache to work properly (performance fix)
* Fix a cache corruption issue when using subresources
* Fix non-standard outputs when using the HAL format
* Persist data in Doctrine DataPersister only if needed
@@ -505,7 +515,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
## 2.2.3
* Fix object state inconsistency after persistence
-* Allow to use multiple `@ApiFilter` annotations on the same class
+* Allow using multiple `@ApiFilter` annotations on the same class
* Fix a BC break when the serialization context builder depends of the retrieved data
* Fix a bug regarding collections handling in the GraphQL endpoint
@@ -540,8 +550,8 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Deprecate the `ApiPlatform\Core\Bridge\Doctrine\EventListener\WriteListener` class in favor of the new `ApiPlatform\Core\EventListener\WriteListener` class.
* Remove the `api_platform.doctrine.listener.view.write` event listener service.
* Add a data persistence layer with a new `ApiPlatform\Core\DataPersister\DataPersisterInterface` interface.
-* Add the a new configuration to disable the API entrypoint and the documentation
-* Allow to set maximum items per page at operation/resource level
+* Add a new configuration to disable the API entrypoint and the documentation
+* Allow setting maximum items per page at operation/resource level
* Add the ability to customize the message when configuring an access control rule trough the `access_control_message` attribute
* Allow empty operations in XML configs
@@ -560,9 +570,9 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Add support for the immutable date and time types introduced in Doctrine
* Fix the Doctrine query generated to retrieve nested subresources
* Fix several bugs in the automatic eager loading support
-* Fix a bug occurring when passing neither an IRI nor an array in an embedded relation
-* Allow to request `0` items per page in collections
-* Also copy the `Host` from the Symfony Router
+* Fix a bug occurring when passing neither an IRI, nor an array in an embedded relation
+* Allow requesting `0` items per page in collections
+* Copy the `Host` from the Symfony Router
* `Paginator::getLastPage()` now always returns a `float`
* Minor performance improvements
* Minor quality fixes
@@ -572,7 +582,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Symfony 3.4 and 4.0 compatibility
* Autowiring strict mode compatibility
* Fix a bug preventing to create resource classes in the global namespace
-* Fix Doctrine type conversion in filter's WHERE clauses
+* Fix Doctrine type conversion in filters WHERE clauses
* Fix filters when using eager loading and non-association composite identifier
* Fix Doctrine type resolution for identifiers (for custom DBALType)
* Add missing Symfony Routing options to operations configuration
@@ -627,9 +637,9 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Add a flag to disable all request listeners
* Add a default order option in the configuration
* Allow to disable all operations using the XML configuration format and deprecate the previous format
-* Allow upper cased property names
+* Allow upper-cased property names
* Improve the overall performance by optimizing `RequestAttributesExtractor`
-* Improve the performance of the filters subsystem by using a PSR-11 service locator and deprecate the `FilterCollection` class
+* Improve the performance of the filter subsystem by using a PSR-11 service locator and deprecate the `FilterCollection` class
* Add compatibility with Symfony Flex and Symfony 4
* Allow the Symfony Dependency Injection component to autoconfigure data providers and query extensions
* Allow to use service for dynamic validation groups
@@ -727,7 +737,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Fix the support of the Symfony's serializer @MaxDepth annotation
* Fix property range of relations in the Hydra doc when an IRI is used
* Fix an error "api:swagger:export" command when decorating the Swagger normalizer
-* Fix an an error in the Swagger documentation generator when a property has several serialization groups
+* Fix an error in the Swagger documentation generator when a property has several serialization groups
## 2.0.1
@@ -765,7 +775,7 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
* Add a range filter
* Search filter: add a case sensitivity setting
* Search filter: fix the behavior of the search filter when 0 is provided as value
-* Search filter: allow to use identifiers different than id
+* Search filter: allow using identifiers different from id
* Exclude tests from classmap
* Fix some deprecations and tests
@@ -811,4 +821,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link
## 1.0.0 beta 2
* Preserve indexes when normalizing and denormalizing associative arrays
-* Allow to set default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance
+* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a58587c9e6f..54d8d51964e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -90,11 +90,13 @@ Coverage will be available in `coverage/index.html`.
The command to launch Behat tests is:
- ./vendor/bin/behat --suite=default --stop-on-failure -vvv
+ php -d memory_limit=-1 ./vendor/bin/behat --suite=default --stop-on-failure --format=progress
If you want to launch Behat tests for MongoDB, the command is:
- APP_ENV=mongodb ./vendor/bin/behat --suite=mongodb --stop-on-failure -vvv
+ APP_ENV=mongodb php -d memory_limit=-1 ./vendor/bin/behat --suite=mongodb --stop-on-failure --format=progress
+
+To get more details about an error, replace `--format=progress` by `-vvv`.
## Squash your Commits
diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php
index 6ae697eeb29..f1adf28cc70 100644
--- a/features/bootstrap/DoctrineContext.php
+++ b/features/bootstrap/DoctrineContext.php
@@ -35,6 +35,8 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputFallbackToSameClass as DummyDtoOutputFallbackToSameClassDocument;
+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\DummyMercure as DummyMercureDocument;
@@ -72,7 +74,6 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
-use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Container;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedBoolean;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedDate;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ConvertedInteger;
@@ -91,6 +92,8 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyGroup;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
@@ -109,7 +112,6 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Greeting;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InternalUser;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
-use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Order;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
@@ -1053,25 +1055,6 @@ public function thereIsAnAnswerToTheQuestion(string $a, string $q)
$this->manager->clear();
}
- /**
- * @Given there are :nb nodes in a container :uuid
- */
- public function thereAreNodesInAContainer(int $nb, string $uuid)
- {
- $container = new Container();
- $container->setId($uuid);
- $this->manager->persist($container);
-
- for ($i = 0; $i < $nb; ++$i) {
- $node = new Node();
- $node->setContainer($container);
- $node->setSerial($i);
- $this->manager->persist($node);
- }
-
- $this->manager->flush();
- }
-
/**
* @Then the password :password for user :user should be hashed
*/
@@ -1381,6 +1364,32 @@ public function thereAreNbDummyDtoCustom($nb)
$this->manager->clear();
}
+ /**
+ * @Given there is a DummyDtoOutputSameClass
+ */
+ public function thereIsADummyDtoOutputSameClass()
+ {
+ $dto = $this->isOrm() ? new DummyDtoOutputSameClass() : new DummyDtoOutputSameClassDocument();
+ $dto->lorem = 'test';
+ $dto->ipsum = '1';
+ $this->manager->persist($dto);
+ $this->manager->flush();
+ $this->manager->clear();
+ }
+
+ /**
+ * @Given there is a DummyDtoOutputFallbackToSameClass
+ */
+ public function thereIsADummyDtoOutputFallbackToSameClass()
+ {
+ $dto = $this->isOrm() ? new DummyDtoOutputFallbackToSameClass() : new DummyDtoOutputFallbackToSameClassDocument();
+ $dto->lorem = 'test';
+ $dto->ipsum = '1';
+ $this->manager->persist($dto);
+ $this->manager->flush();
+ $this->manager->clear();
+ }
+
/**
* @Given there is an order with same customer and recipient
*/
@@ -1474,7 +1483,7 @@ public function thereIsTheFollowingProduct(PyStringNode $dataNode): void
$product = $this->isOrm() ? new Product() : new ProductDocument();
$product->setCode($data['code']);
if (isset($data['mainTaxon'])) {
- $mainTaxonCode = str_replace('/taxons/', '', $data['mainTaxon']);
+ $mainTaxonCode = str_replace('/taxa/', '', $data['mainTaxon']);
$mainTaxon = $this->manager->getRepository($this->isOrm() ? Taxon::class : TaxonDocument::class)->findOneBy([
'code' => $mainTaxonCode,
]);
diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature
index 62c34ae86e2..1867b588c7c 100644
--- a/features/graphql/introspection.feature
+++ b/features/graphql/introspection.feature
@@ -428,7 +428,7 @@ Feature: GraphQL introspection support
And the JSON node "data.typeCreatePayloadData.fields[3].type.name" should be equal to "createDummyGroupNestedPayload"
And the JSON node "data.typeCreateNestedPayload.fields[0].name" should be equal to "id"
- Scenario: Retrieve an item through a GraphQL query
+ Scenario: Retrieve a type name through a GraphQL query
Given there are 4 dummy objects with relatedDummy
When I send the following GraphQL request:
"""
@@ -449,3 +449,28 @@ Feature: GraphQL introspection support
And the JSON node "data.dummy.name" should be equal to "Dummy #3"
And the JSON node "data.dummy.relatedDummy.name" should be equal to "RelatedDummy #3"
And the JSON node "data.dummy.relatedDummy.__typename" should be equal to "RelatedDummy"
+
+ Scenario: Introspect a type available only through relations
+ When I send the following GraphQL request:
+ """
+ {
+ typeNotAvailable: __type(name: "VoDummyInspectionConnection") {
+ description
+ }
+ typeOwner: __type(name: "VoDummyCar") {
+ description,
+ fields {
+ name
+ type {
+ name
+ }
+ }
+ }
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionConnection" is not present in the types container'
+ And the JSON node "data.typeNotAvailable" should be null
+ And the JSON node "data.typeOwner.fields[3].type.name" should be equal to "VoDummyInspectionConnection"
diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature
index 12f1492f870..256eb610a22 100644
--- a/features/jsonld/input_output.feature
+++ b/features/jsonld/input_output.feature
@@ -83,6 +83,44 @@ Feature: JSON-LD DTO input and output
}
"""
+ @createSchema
+ Scenario: Get an item with same class as custom output
+ Given there is a DummyDtoOutputSameClass
+ When I send a "GET" request to "/dummy_dto_output_same_classes/1"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/DummyDtoOutputSameClass",
+ "@id": "/dummy_dto_output_same_classes/1",
+ "@type": "DummyDtoOutputSameClass",
+ "lorem": "test",
+ "ipsum": "modified",
+ "id": 1
+ }
+ """
+
+ @createSchema
+ Scenario: Get an item with a data transformer that will return the original class as a fallback
+ Given there is a DummyDtoOutputFallbackToSameClass
+ When I send a "GET" request to "/dummy_dto_output_fallback_to_same_classes/1"
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/DummyDtoOutputFallbackToSameClass",
+ "@id": "/dummy_dto_output_fallback_to_same_classes/1",
+ "@type": "DummyDtoOutputFallbackToSameClass",
+ "lorem": "test",
+ "ipsum": "modified",
+ "id": 1
+ }
+ """
+
@createSchema
Scenario: Create a DummyDtoCustom object without output
When I send a "POST" request to "/dummy_dto_custom_post_without_output" with body:
diff --git a/features/jsonld/interface_as_resource.feature b/features/jsonld/interface_as_resource.feature
index 29ce56cce70..7236ace7ae7 100644
--- a/features/jsonld/interface_as_resource.feature
+++ b/features/jsonld/interface_as_resource.feature
@@ -15,7 +15,7 @@ Feature: JSON-LD using interface as resource
"code": "WONDERFUL_TAXON"
}
"""
- When I send a "GET" request to "/taxons/WONDERFUL_TAXON"
+ When I send a "GET" request to "/taxa/WONDERFUL_TAXON"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
@@ -23,7 +23,7 @@ Feature: JSON-LD using interface as resource
"""
{
"@context": "/contexts/Taxon",
- "@id": "/taxons/WONDERFUL_TAXON",
+ "@id": "/taxa/WONDERFUL_TAXON",
"@type": "Taxon",
"code": "WONDERFUL_TAXON"
}
@@ -34,7 +34,7 @@ Feature: JSON-LD using interface as resource
"""
{
"code": "GREAT_PRODUCT",
- "mainTaxon": "/taxons/WONDERFUL_TAXON"
+ "mainTaxon": "/taxa/WONDERFUL_TAXON"
}
"""
When I send a "GET" request to "/products/GREAT_PRODUCT"
@@ -49,7 +49,7 @@ Feature: JSON-LD using interface as resource
"@type": "Product",
"code": "GREAT_PRODUCT",
"mainTaxon": {
- "@id": "/taxons/WONDERFUL_TAXON",
+ "@id": "/taxa/WONDERFUL_TAXON",
"@type": "Taxon",
"code": "WONDERFUL_TAXON"
}
diff --git a/features/main/uuid.feature b/features/main/uuid.feature
index ca5bc9ec9fc..9278cb15ee5 100644
--- a/features/main/uuid.feature
+++ b/features/main/uuid.feature
@@ -130,3 +130,17 @@ Feature: Using uuid identifier on resource
Then the response status code should be 404
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+
+ @!mongodb
+ @createSchema
+ Scenario: Create a resource identified by Ramsey\Uuid\Uuid
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/ramsey_uuid_dummies" with body:
+ """
+ {
+ "id": "41b29566-144b-11e6-a148-3e1d05defe78"
+ }
+ """
+ Then the response status code should be 201
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
diff --git a/src/Annotation/AttributesHydratorTrait.php b/src/Annotation/AttributesHydratorTrait.php
index aba28a5fedb..1211f73b6a1 100644
--- a/src/Annotation/AttributesHydratorTrait.php
+++ b/src/Annotation/AttributesHydratorTrait.php
@@ -14,7 +14,7 @@
namespace ApiPlatform\Core\Annotation;
use ApiPlatform\Core\Exception\InvalidArgumentException;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
/**
* Hydrates attributes from annotation's parameters.
diff --git a/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php
index 572722d5a70..a2ae6c593af 100644
--- a/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php
+++ b/src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php
@@ -41,6 +41,15 @@
*/
final class PublishMercureUpdatesListener
{
+ private const ALLOWED_KEYS = [
+ 'topics' => true,
+ 'data' => true,
+ 'private' => true,
+ 'id' => true,
+ 'type' => true,
+ 'retry' => true,
+ ];
+
use DispatchTrait;
use ResourceClassInfoTrait;
@@ -144,60 +153,75 @@ private function storeObjectToPublish($object, string $property): void
return;
}
- $value = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
- if (false === $value) {
+ $options = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
+ if (false === $options) {
return;
}
- if (\is_string($value)) {
+ if (\is_string($options)) {
if (null === $this->expressionLanguage) {
throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".');
}
- $value = $this->expressionLanguage->evaluate($value, ['object' => $object]);
+ $options = $this->expressionLanguage->evaluate($options, ['object' => $object]);
+ }
+
+ if (true === $options) {
+ $options = [];
}
- if (true === $value) {
- $value = [];
+ if (!\is_array($options)) {
+ throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of options or an expression returning this array, "%s" given.', $resourceClass, \gettype($options)));
}
- if (!\is_array($value)) {
- throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of targets or a valid expression, "%s" given.', $resourceClass, \gettype($value)));
+ foreach ($options as $key => $value) {
+ if (0 === $key) {
+ if (method_exists(Update::class, 'isPrivate')) {
+ throw new \InvalidArgumentException('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead or downgrade the Mercure Component to version 0.3');
+ }
+
+ @trigger_error('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead.', E_USER_DEPRECATED);
+ break;
+ }
+
+ if (!isset(self::ALLOWED_KEYS[$key])) {
+ throw new InvalidArgumentException(sprintf('The option "%s" set in the "mercure" attribute of the "%s" resource does not exist. Existing options: "%s"', $key, $resourceClass, implode('", "', self::ALLOWED_KEYS)));
+ }
}
if ('deletedObjects' === $property) {
$this->deletedObjects[(object) [
'id' => $this->iriConverter->getIriFromItem($object),
'iri' => $this->iriConverter->getIriFromItem($object, UrlGeneratorInterface::ABS_URL),
- ]] = $value;
+ ]] = $options;
return;
}
- $this->{$property}[$object] = $value;
+ $this->{$property}[$object] = $options;
}
/**
* @param object $object
*/
- private function publishUpdate($object, array $targets, string $type): void
+ private function publishUpdate($object, array $options, string $type): void
{
if ($object instanceof \stdClass) {
// By convention, if the object has been deleted, we send only its IRI.
// This may change in the feature, because it's not JSON Merge Patch compliant,
// and I'm not a fond of this approach.
- $iri = $object->iri;
+ $iri = $options['topics'] ?? $object->iri;
/** @var string $data */
- $data = json_encode(['@id' => $object->id]);
+ $data = $options['data'] ?? json_encode(['@id' => $object->id]);
} else {
$resourceClass = $this->getObjectClass($object);
$context = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('normalization_context', []);
- $iri = $this->iriConverter->getIriFromItem($object, UrlGeneratorInterface::ABS_URL);
- $data = $this->serializer->serialize($object, key($this->formats), $context);
+ $iri = $options['topics'] ?? $this->iriConverter->getIriFromItem($object, UrlGeneratorInterface::ABS_URL);
+ $data = $options['data'] ?? $this->serializer->serialize($object, key($this->formats), $context);
}
- $updates = array_merge([new Update($iri, $data, $targets)], $this->getGraphQlSubscriptionUpdates($object, $targets, $type));
+ $updates = array_merge([$this->buildUpdate($iri, $data, $options)], $this->getGraphQlSubscriptionUpdates($object, $options, $type));
foreach ($updates as $update) {
$this->messageBus ? $this->dispatch($update) : ($this->publisher)($update);
@@ -209,7 +233,7 @@ private function publishUpdate($object, array $targets, string $type): void
*
* @return Update[]
*/
- private function getGraphQlSubscriptionUpdates($object, array $targets, string $type): array
+ private function getGraphQlSubscriptionUpdates($object, array $options, string $type): array
{
if ('update' !== $type || !$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) {
return [];
@@ -219,13 +243,24 @@ private function getGraphQlSubscriptionUpdates($object, array $targets, string $
$updates = [];
foreach ($payloads as [$subscriptionId, $data]) {
- $updates[] = new Update(
+ $updates[] = $this->buildUpdate(
$this->graphQlMercureSubscriptionIriGenerator->generateTopicIri($subscriptionId),
(string) (new JsonResponse($data))->getContent(),
- $targets
+ $options
);
}
return $updates;
}
+
+ private function buildUpdate(string $iri, string $data, array $options): Update
+ {
+ if (method_exists(Update::class, 'isPrivate')) {
+ return new Update($iri, $data, $options['private'] ?? false, $options['id'] ?? null, $options['type'] ?? null, $options['retry'] ?? null);
+ }
+
+ // Mercure Component < 0.4.
+ /* @phpstan-ignore-next-line */
+ return new Update($iri, $data, $options);
+ }
}
diff --git a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php
index 6b539999645..da0b8f3b505 100644
--- a/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php
+++ b/src/Bridge/Doctrine/Orm/Filter/SearchFilter.php
@@ -92,11 +92,12 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
$caseSensitive = true;
$metadata = $this->getNestedMetadata($resourceClass, $associations);
- $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass);
- $values = array_map([$this, 'getIdFromValue'], $values);
-
if ($metadata->hasField($field)) {
- if (!$this->hasValidValues($values, $doctrineTypeField)) {
+ if ('id' === $field) {
+ $values = array_map([$this, 'getIdFromValue'], $values);
+ }
+
+ if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
$this->logger->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
]);
@@ -113,7 +114,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
}
if (1 === \count($values)) {
- $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $doctrineTypeField, $values[0], $caseSensitive);
+ $this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values[0], $caseSensitive);
return;
}
@@ -139,7 +140,9 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
return;
}
+ $values = array_map([$this, 'getIdFromValue'], $values);
$associationFieldIdentifier = 'id';
+ $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass);
if (null !== $this->identifiersExtractor) {
$associationResourceClass = $metadata->getAssociationTargetClass($field);
@@ -168,11 +171,11 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
if (1 === \count($values)) {
$queryBuilder
->andWhere(sprintf('%s.%s = :%s', $associationAlias, $associationField, $valueParameter))
- ->setParameter($valueParameter, $values[0], $doctrineTypeField);
+ ->setParameter($valueParameter, $values[0]);
} else {
$queryBuilder
->andWhere(sprintf('%s.%s IN (:%s)', $associationAlias, $associationField, $valueParameter))
- ->setParameter($valueParameter, $values, $doctrineTypeField);
+ ->setParameter($valueParameter, $values);
}
}
@@ -181,7 +184,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
*
* @throws InvalidArgumentException If strategy does not exist
*/
- protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $fieldType, $value, bool $caseSensitive)
+ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $value, bool $caseSensitive)
{
$wrapCase = $this->createWrapCase($caseSensitive);
$valueParameter = $queryNameGenerator->generateParameterName($field);
@@ -191,27 +194,27 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild
case self::STRATEGY_EXACT:
$queryBuilder
->andWhere(sprintf($wrapCase('%s.%s').' = '.$wrapCase(':%s'), $alias, $field, $valueParameter))
- ->setParameter($valueParameter, $value, $fieldType);
+ ->setParameter($valueParameter, $value);
break;
case self::STRATEGY_PARTIAL:
$queryBuilder
->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s, \'%%\')'), $alias, $field, $valueParameter))
- ->setParameter($valueParameter, $value, $fieldType);
+ ->setParameter($valueParameter, $value);
break;
case self::STRATEGY_START:
$queryBuilder
->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(:%s, \'%%\')'), $alias, $field, $valueParameter))
- ->setParameter($valueParameter, $value, $fieldType);
+ ->setParameter($valueParameter, $value);
break;
case self::STRATEGY_END:
$queryBuilder
->andWhere(sprintf($wrapCase('%s.%s').' LIKE '.$wrapCase('CONCAT(\'%%\', :%s)'), $alias, $field, $valueParameter))
- ->setParameter($valueParameter, $value, $fieldType);
+ ->setParameter($valueParameter, $value);
break;
case self::STRATEGY_WORD_START:
$queryBuilder
->andWhere(sprintf($wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(:%3$s, \'%%\')').' OR '.$wrapCase('%1$s.%2$s').' LIKE '.$wrapCase('CONCAT(\'%% \', :%3$s, \'%%\')'), $alias, $field, $valueParameter))
- ->setParameter($valueParameter, $value, $fieldType);
+ ->setParameter($valueParameter, $value);
break;
default:
throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy));
diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
index 0fb5c5260fc..c7d42878e36 100644
--- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
+++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
@@ -16,7 +16,7 @@
use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException;
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
use Elasticsearch\Client;
use Elasticsearch\Common\Exceptions\Missing404Exception;
diff --git a/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php b/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php
index 8bac4e27f52..8a3ce082486 100644
--- a/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php
+++ b/src/Bridge/RamseyUuid/Identifier/Normalizer/UuidNormalizer.php
@@ -43,6 +43,6 @@ public function denormalize($data, $class, $format = null, array $context = [])
*/
public function supportsDenormalization($data, $type, $format = null)
{
- return is_a($type, UuidInterface::class, true);
+ return \is_string($data) && is_a($type, UuidInterface::class, true);
}
}
diff --git a/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php b/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php
new file mode 100644
index 00000000000..87090190a85
--- /dev/null
+++ b/src/Bridge/RamseyUuid/Serializer/UuidDenormalizer.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Bridge\RamseyUuid\Serializer;
+
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+final class UuidDenormalizer implements DenormalizerInterface
+{
+ public function denormalize($data, $type, $format = null, array $context = [])
+ {
+ return Uuid::fromString($data);
+ }
+
+ public function supportsDenormalization($data, $type, $format = null)
+ {
+ return \is_string($data) && is_a($type, UuidInterface::class, true);
+ }
+}
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml b/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml
index a5d0626ab06..bbf81f232fa 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/ramsey_uuid.xml
@@ -8,5 +8,9 @@
+
+
+
+
diff --git a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php
index f988850c080..f603e08f3a1 100644
--- a/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php
+++ b/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactory.php
@@ -13,7 +13,6 @@
namespace ApiPlatform\Core\Bridge\Symfony\PropertyInfo\Metadata\Property;
-use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
@@ -36,16 +35,11 @@ public function __construct(PropertyInfoExtractorInterface $propertyInfo)
/**
* {@inheritdoc}
- *
- * @throws RuntimeException
*/
public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
$properties = $this->propertyInfo->getProperties($resourceClass, $options);
- if (null === $properties) {
- throw new RuntimeException(sprintf('There is no PropertyInfo extractor supporting the class "%s".', $resourceClass));
- }
- return new PropertyNameCollection($properties);
+ return new PropertyNameCollection($properties ?? []);
}
}
diff --git a/src/Bridge/Symfony/Routing/RouteNameGenerator.php b/src/Bridge/Symfony/Routing/RouteNameGenerator.php
index d765dc9ef0e..89b3ca3ac5f 100644
--- a/src/Bridge/Symfony/Routing/RouteNameGenerator.php
+++ b/src/Bridge/Symfony/Routing/RouteNameGenerator.php
@@ -16,7 +16,7 @@
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\OperationTypeDeprecationHelper;
use ApiPlatform\Core\Exception\InvalidArgumentException;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
/**
* Generates the Symfony route name associated with an operation name and a resource short name.
diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php
index c073edaeebe..826f193760e 100644
--- a/src/GraphQl/Type/FieldsBuilder.php
+++ b/src/GraphQl/Type/FieldsBuilder.php
@@ -21,7 +21,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\NullableType;
use GraphQL\Type\Definition\Type as GraphQLType;
diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php
index db7cec25557..9cc2da4d29c 100644
--- a/src/GraphQl/Type/TypeConverter.php
+++ b/src/GraphQl/Type/TypeConverter.php
@@ -105,9 +105,12 @@ private function getResourceType(Type $type, bool $input, ?string $queryName, ?s
try {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
- if ([] === ($resourceMetadata->getGraphql() ?? [])) {
+ if (null === $resourceMetadata->getGraphql()) {
return null;
}
+ if ('Node' === $resourceMetadata->getShortName()) {
+ throw new \UnexpectedValueException('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.');
+ }
} catch (ResourceClassNotFoundException $e) {
// Skip objects that are not resources for now
return null;
diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php
index bece5e16396..60d61499a67 100644
--- a/src/Hydra/Serializer/CollectionNormalizer.php
+++ b/src/Hydra/Serializer/CollectionNormalizer.php
@@ -93,13 +93,11 @@ public function normalize($object, $format = null, array $context = [])
$data['hydra:member'][] = $iriOnly ? ['@id' => $this->iriConverter->getIriFromItem($obj)] : $this->normalizer->normalize($obj, $format, $context);
}
- $paginated = null;
- if (
- \is_array($object) ||
- ($paginated = $object instanceof PaginatorInterface) ||
- $object instanceof \Countable && !$object instanceof PartialPaginatorInterface
- ) {
- $data['hydra:totalItems'] = $paginated ? $object->getTotalItems() : \count($object);
+ if ($object instanceof PaginatorInterface) {
+ $data['hydra:totalItems'] = $object->getTotalItems();
+ }
+ if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) {
+ $data['hydra:totalItems'] = \count($object);
}
return $data;
diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php
index 197050fdd55..793eba67639 100644
--- a/src/JsonLd/Serializer/ItemNormalizer.php
+++ b/src/JsonLd/Serializer/ItemNormalizer.php
@@ -66,7 +66,9 @@ public function supportsNormalization($data, $format = null): bool
*/
public function normalize($object, $format = null, array $context = [])
{
- if (null !== $this->getOutputClass($this->getObjectClass($object), $context)) {
+ $objectClass = $this->getObjectClass($object);
+ $outputClass = $this->getOutputClass($objectClass, $context);
+ if (null !== $outputClass && !isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS])) {
return parent::normalize($object, $format, $context);
}
diff --git a/src/Operation/DashPathSegmentNameGenerator.php b/src/Operation/DashPathSegmentNameGenerator.php
index 07acc591ecc..1a31f1fa7fc 100644
--- a/src/Operation/DashPathSegmentNameGenerator.php
+++ b/src/Operation/DashPathSegmentNameGenerator.php
@@ -13,7 +13,7 @@
namespace ApiPlatform\Core\Operation;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
/**
* Generate a path name with a dash separator according to a string and whether it's a collection or not.
diff --git a/src/Operation/UnderscorePathSegmentNameGenerator.php b/src/Operation/UnderscorePathSegmentNameGenerator.php
index a48f051fa75..c0e5694ac53 100644
--- a/src/Operation/UnderscorePathSegmentNameGenerator.php
+++ b/src/Operation/UnderscorePathSegmentNameGenerator.php
@@ -13,7 +13,7 @@
namespace ApiPlatform\Core\Operation;
-use Doctrine\Common\Inflector\Inflector;
+use ApiPlatform\Core\Util\Inflector;
/**
* Generate a path name with an underscore separator according to a string and whether it's a collection or not.
diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php
index 3b4d4446bbe..6a75419b13f 100644
--- a/src/Serializer/AbstractItemNormalizer.php
+++ b/src/Serializer/AbstractItemNormalizer.php
@@ -52,6 +52,8 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
use ContextTrait;
use InputOutputMetadataTrait;
+ public const IS_TRANSFORMED_TO_SAME_CLASS = 'is_transformed_to_same_class';
+
protected $propertyNameCollectionFactory;
protected $propertyMetadataFactory;
protected $iriConverter;
@@ -115,18 +117,24 @@ public function hasCacheableSupportsMethod(): bool
*/
public function normalize($object, $format = null, array $context = [])
{
- if ($object !== $transformed = $this->transformOutput($object, $context)) {
+ if (!($isTransformed = isset($context[self::IS_TRANSFORMED_TO_SAME_CLASS])) && $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
}
- $context['api_normalize'] = true;
- $context['api_resource'] = $object;
- unset($context['output']);
- unset($context['resource_class']);
+ if ($object !== $transformed = $this->transformOutput($object, $context, $outputClass)) {
+ $context['api_normalize'] = true;
+ $context['api_resource'] = $object;
+ unset($context['output'], $context['resource_class']);
+ } else {
+ $context[self::IS_TRANSFORMED_TO_SAME_CLASS] = true;
+ }
return $this->serializer->normalize($transformed, $format, $context);
}
+ if ($isTransformed) {
+ unset($context[self::IS_TRANSFORMED_TO_SAME_CLASS]);
+ }
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
$context = $this->initContext($resourceClass, $context);
@@ -669,9 +677,12 @@ protected function getDataTransformer($data, string $to, array $context = []): ?
* For a given resource, it returns an output representation if any
* If not, the resource is returned.
*/
- protected function transformOutput($object, array $context = [])
+ protected function transformOutput($object, array $context = [], string $outputClass = null)
{
- $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ if (null === $outputClass) {
+ $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
+ }
+
if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
return $dataTransformer->transform($object, $outputClass, $context);
}
diff --git a/src/Util/AnnotationFilterExtractorTrait.php b/src/Util/AnnotationFilterExtractorTrait.php
index 756dab0d376..33be708471a 100644
--- a/src/Util/AnnotationFilterExtractorTrait.php
+++ b/src/Util/AnnotationFilterExtractorTrait.php
@@ -15,7 +15,6 @@
use ApiPlatform\Core\Annotation\ApiFilter;
use Doctrine\Common\Annotations\Reader;
-use Doctrine\Common\Inflector\Inflector;
/**
* Generates a service id for a generic filter.
diff --git a/src/Util/Inflector.php b/src/Util/Inflector.php
new file mode 100644
index 00000000000..0c9cf904316
--- /dev/null
+++ b/src/Util/Inflector.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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\Util;
+
+use Doctrine\Common\Inflector\Inflector as LegacyInflector;
+use Doctrine\Inflector\Inflector as InflectorObject;
+use Doctrine\Inflector\InflectorFactory;
+
+/**
+ * Facade for Doctrine Inflector.
+ *
+ * This class allows us to maintain compatibility with Doctrine Inflector 1.3 and 2.0 at the same time.
+ *
+ * @internal
+ */
+final class Inflector
+{
+ /**
+ * @var InflectorObject
+ */
+ private static $instance;
+
+ private static function getInstance(): InflectorObject
+ {
+ return self::$instance
+ ?? self::$instance = InflectorFactory::create()->build();
+ }
+
+ /**
+ * @see LegacyInflector::tableize()
+ */
+ public static function tableize(string $word): string
+ {
+ return class_exists(InflectorFactory::class) ? self::getInstance()->tableize($word) : LegacyInflector::tableize($word);
+ }
+
+ /**
+ * @see LegacyInflector::pluralize()
+ */
+ public static function pluralize(string $word): string
+ {
+ return class_exists(InflectorFactory::class) ? self::getInstance()->pluralize($word) : LegacyInflector::pluralize($word);
+ }
+}
diff --git a/tests/Annotation/ApiResourceTest.php b/tests/Annotation/ApiResourceTest.php
index 1fdc1fefe2c..5f24c6aacca 100644
--- a/tests/Annotation/ApiResourceTest.php
+++ b/tests/Annotation/ApiResourceTest.php
@@ -43,7 +43,7 @@ public function testConstruct()
'input' => 'Foo',
'iri' => 'http://example.com/res',
'itemOperations' => ['foo' => ['bar']],
- 'mercure' => '[\'foo\', object.owner]',
+ 'mercure' => ['private' => true],
'messenger' => true,
'normalizationContext' => ['groups' => ['bar']],
'order' => ['foo', 'bar' => 'ASC'],
@@ -85,7 +85,7 @@ public function testConstruct()
'formats' => ['foo', 'bar' => ['application/bar']],
'filters' => ['foo', 'bar'],
'input' => 'Foo',
- 'mercure' => '[\'foo\', object.owner]',
+ 'mercure' => ['private' => true],
'messenger' => true,
'normalization_context' => ['groups' => ['bar']],
'order' => ['foo', 'bar' => 'ASC'],
diff --git a/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php b/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php
index 26d7954ce86..3780d3d1e65 100644
--- a/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php
+++ b/tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php
@@ -39,8 +39,12 @@
*/
class PublishMercureUpdatesListenerTest extends TestCase
{
- public function testPublishUpdate(): void
+ public function testLegacyPublishUpdate(): void
{
+ if (method_exists(Update::class, 'isPrivate')) {
+ $this->markTestSkipped();
+ }
+
$toInsert = new Dummy();
$toInsert->setId(1);
$toInsertNotResource = new NotAResource('foo', 'bar');
@@ -86,7 +90,7 @@ public function testPublishUpdate(): void
$targets = [];
$publisher = function (Update $update) use (&$topics, &$targets): string {
$topics = array_merge($topics, $update->getTopics());
- $targets[] = $update->getTargets();
+ $targets[] = $update->getTargets(); // @phpstan-ignore-line
return 'id';
};
@@ -117,8 +121,97 @@ public function testPublishUpdate(): void
$this->assertSame([[], [], [], ['foo', 'bar']], $targets);
}
+ public function testPublishUpdate(): void
+ {
+ if (!method_exists(Update::class, 'isPrivate')) {
+ $this->markTestSkipped();
+ }
+
+ $toInsert = new Dummy();
+ $toInsert->setId(1);
+ $toInsertNotResource = new NotAResource('foo', 'bar');
+
+ $toUpdate = new Dummy();
+ $toUpdate->setId(2);
+ $toUpdateNoMercureAttribute = new DummyCar();
+
+ $toDelete = new Dummy();
+ $toDelete->setId(3);
+ $toDeleteExpressionLanguage = new DummyFriend();
+ $toDeleteExpressionLanguage->setId(4);
+
+ $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
+ $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class);
+ $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyCar::class))->willReturn(DummyCar::class);
+ $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyFriend::class))->willReturn(DummyFriend::class);
+ $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
+ $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false);
+ $resourceClassResolverProphecy->isResourceClass(DummyCar::class)->willReturn(true);
+ $resourceClassResolverProphecy->isResourceClass(DummyFriend::class)->willReturn(true);
+
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($toInsert, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/1')->shouldBeCalled();
+ $iriConverterProphecy->getIriFromItem($toUpdate, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/2')->shouldBeCalled();
+ $iriConverterProphecy->getIriFromItem($toDelete, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/3')->shouldBeCalled();
+ $iriConverterProphecy->getIriFromItem($toDelete)->willReturn('/dummies/3')->shouldBeCalled();
+ $iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage)->willReturn('/dummy_friends/4')->shouldBeCalled();
+ $iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummy_friends/4')->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => true, 'normalization_context' => ['groups' => ['foo', 'bar']]]));
+ $resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata());
+ $resourceMetadataFactoryProphecy->create(DummyFriend::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => ['private' => true, 'retry' => 10]]));
+
+ $serializerProphecy = $this->prophesize(SerializerInterface::class);
+ $serializerProphecy->serialize($toInsert, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('1');
+ $serializerProphecy->serialize($toUpdate, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('2');
+
+ $formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']];
+
+ $topics = [];
+ $private = [];
+ $retry = [];
+ $publisher = function (Update $update) use (&$topics, &$private, &$retry): string {
+ $topics = array_merge($topics, $update->getTopics());
+ $private[] = $update->isPrivate();
+ $retry[] = $update->getRetry();
+
+ return 'id';
+ };
+
+ $listener = new PublishMercureUpdatesListener(
+ $resourceClassResolverProphecy->reveal(),
+ $iriConverterProphecy->reveal(),
+ $resourceMetadataFactoryProphecy->reveal(),
+ $serializerProphecy->reveal(),
+ $formats,
+ null,
+ $publisher
+ );
+
+ $uowProphecy = $this->prophesize(UnitOfWork::class);
+ $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert, $toInsertNotResource])->shouldBeCalled();
+ $uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate, $toUpdateNoMercureAttribute])->shouldBeCalled();
+ $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete, $toDeleteExpressionLanguage])->shouldBeCalled();
+
+ $emProphecy = $this->prophesize(EntityManagerInterface::class);
+ $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
+ $eventArgs = new OnFlushEventArgs($emProphecy->reveal());
+
+ $listener->onFlush($eventArgs);
+ $listener->postFlush();
+
+ $this->assertSame(['http://example.com/dummies/1', 'http://example.com/dummies/2', 'http://example.com/dummies/3', 'http://example.com/dummy_friends/4'], $topics);
+ $this->assertSame([false, false, false, true], $private);
+ $this->assertSame([null, null, null, 10], $retry);
+ }
+
public function testPublishGraphQlUpdates(): void
{
+ if (!method_exists(Update::class, 'isPrivate')) {
+ $this->markTestSkipped();
+ }
+
$toUpdate = new Dummy();
$toUpdate->setId(2);
@@ -138,11 +231,13 @@ public function testPublishGraphQlUpdates(): void
$formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']];
$topics = [];
- $targets = [];
+ $private = [];
+ $retry = [];
$data = [];
- $publisher = function (Update $update) use (&$topics, &$targets, &$data): string {
+ $publisher = function (Update $update) use (&$topics, &$private, &$retry, &$data): string {
$topics = array_merge($topics, $update->getTopics());
- $targets[] = $update->getTargets();
+ $private[] = $update->isPrivate();
+ $retry[] = $update->getRetry();
$data[] = $update->getData();
return 'id';
@@ -181,7 +276,8 @@ public function testPublishGraphQlUpdates(): void
$listener->postFlush();
$this->assertSame(['http://example.com/dummies/2', 'subscription-topic-iri'], $topics);
- $this->assertSame([[], []], $targets);
+ $this->assertSame([false, false], $private);
+ $this->assertSame([null, null], $retry);
$this->assertSame(['2', '["data"]'], $data);
}
@@ -204,7 +300,7 @@ public function testNoPublisher(): void
public function testInvalidMercureAttribute(): void
{
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of targets or a valid expression, "integer" given.');
+ $this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of options or an expression returning this array, "integer" given.');
$toInsert = new Dummy();
diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
index 2bf5c361644..36505c4dc9c 100644
--- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
+++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/DoctrineExtractorTest.php
@@ -60,7 +60,6 @@ public function testGetProperties(): void
'integer',
'string',
'key',
- 'file',
'hash',
'collection',
'objectId',
@@ -149,7 +148,6 @@ public function typesProvider(): array
['integer', [new Type(Type::BUILTIN_TYPE_INT)]],
['string', [new Type(Type::BUILTIN_TYPE_STRING)]],
['key', [new Type(Type::BUILTIN_TYPE_INT)]],
- ['file', null],
['hash', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(TYPE::BUILTIN_TYPE_INT))]],
['objectId', [new Type(Type::BUILTIN_TYPE_STRING)]],
@@ -208,8 +206,8 @@ private function createExtractor(): DoctrineExtractor
$config = DoctrineMongoDbOdmSetup::createAnnotationMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'], true);
$documentManager = DocumentManager::create(null, $config);
- if (!MongoDbType::hasType('foo')) {
- MongoDbType::addType('foo', DoctrineFooType::class);
+ if (!MongoDbType::hasType('custom_foo')) {
+ MongoDbType::addType('custom_foo', DoctrineFooType::class);
}
return new DoctrineExtractor($documentManager);
diff --git a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
index 4e9e2a86194..64a34ed4f65 100644
--- a/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
+++ b/tests/Bridge/Doctrine/MongoDbOdm/PropertyInfo/Fixtures/DoctrineDummy.php
@@ -131,11 +131,6 @@ class DoctrineDummy
*/
private $key;
- /**
- * @Field(type="file")
- */
- private $file;
-
/**
* @Field(type="hash")
*/
diff --git a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php
index 3a74e6cfb55..cbf158da679 100644
--- a/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php
+++ b/tests/Bridge/RamseyUuid/Normalizer/UuidNormalizerTest.php
@@ -44,4 +44,11 @@ public function testFailDenormalizeUuid()
$this->assertTrue($normalizer->supportsDenormalization($uuid, Uuid::class));
$normalizer->denormalize($uuid, Uuid::class);
}
+
+ public function testDoNotSupportNotString()
+ {
+ $uuid = Uuid::uuid4();
+ $normalizer = new UuidNormalizer();
+ $this->assertFalse($normalizer->supportsDenormalization($uuid, Uuid::class));
+ }
}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 823ca459ecd..9daf7bdd61d 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -967,6 +967,7 @@ private function getPartialContainerBuilderProphecy($configuration = null)
'api_platform.serializer.group_filter',
'api_platform.serializer.normalizer.item',
'api_platform.serializer.property_filter',
+ 'api_platform.serializer.uuid_denormalizer',
'api_platform.serializer_locator',
'api_platform.subresource_data_provider',
'api_platform.subresource_operation_factory',
diff --git a/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php
new file mode 100644
index 00000000000..27384cccb64
--- /dev/null
+++ b/tests/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyNameCollectionFactoryTest.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Bridge\Symfony\PropertyInfo\Metadata\Property;
+
+use ApiPlatform\Core\Bridge\Symfony\PropertyInfo\Metadata\Property\PropertyInfoPropertyNameCollectionFactory;
+use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithOnlyPrivateProperty;
+use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithOnlyPublicProperty;
+use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithoutProperty;
+use ApiPlatform\Core\Tests\Fixtures\DummyObjectWithPublicAndPrivateProperty;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
+use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
+
+/**
+ * @author Oskar Stark
+ */
+class PropertyInfoPropertyNameCollectionFactoryTest extends TestCase
+{
+ public function testCreateMethodReturnsEmptyPropertyNameCollectionForObjectWithOnlyPrivateProperty()
+ {
+ $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([
+ new ReflectionExtractor(),
+ ]));
+
+ $collection = $factory->create(DummyObjectWithOnlyPrivateProperty::class);
+
+ self::assertCount(0, $collection->getIterator());
+ }
+
+ public function testCreateMethodReturnsEmptyPropertyNameCollectionForObjectWithoutProperties()
+ {
+ $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([
+ new ReflectionExtractor(),
+ ]));
+
+ $collection = $factory->create(DummyObjectWithoutProperty::class);
+
+ self::assertCount(0, $collection->getIterator());
+ }
+
+ public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWithPublicAndPrivateProperty()
+ {
+ $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([
+ new ReflectionExtractor(),
+ ]));
+
+ $collection = $factory->create(DummyObjectWithPublicAndPrivateProperty::class);
+
+ self::assertCount(1, $collection->getIterator());
+ }
+
+ public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWithPublicProperty()
+ {
+ $factory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([
+ new ReflectionExtractor(),
+ ]));
+
+ $collection = $factory->create(DummyObjectWithOnlyPublicProperty::class);
+
+ self::assertCount(1, $collection->getIterator());
+ }
+}
diff --git a/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php b/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php
new file mode 100644
index 00000000000..1cc3c6dfaf9
--- /dev/null
+++ b/tests/Fixtures/DummyObjectWithOnlyPrivateProperty.php
@@ -0,0 +1,19 @@
+
+ *
+ * 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;
+
+class DummyObjectWithOnlyPrivateProperty
+{
+ private $foo;
+}
diff --git a/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php b/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php
new file mode 100644
index 00000000000..4a4ad2a22ed
--- /dev/null
+++ b/tests/Fixtures/DummyObjectWithOnlyPublicProperty.php
@@ -0,0 +1,19 @@
+
+ *
+ * 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;
+
+class DummyObjectWithOnlyPublicProperty
+{
+ public $foo;
+}
diff --git a/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php b/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php
new file mode 100644
index 00000000000..2c146a0048e
--- /dev/null
+++ b/tests/Fixtures/DummyObjectWithPublicAndPrivateProperty.php
@@ -0,0 +1,20 @@
+
+ *
+ * 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;
+
+class DummyObjectWithPublicAndPrivateProperty
+{
+ public $foo;
+ private $bar;
+}
diff --git a/tests/Fixtures/DummyObjectWithoutProperty.php b/tests/Fixtures/DummyObjectWithoutProperty.php
new file mode 100644
index 00000000000..3171e2fd992
--- /dev/null
+++ b/tests/Fixtures/DummyObjectWithoutProperty.php
@@ -0,0 +1,18 @@
+
+ *
+ * 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;
+
+class DummyObjectWithoutProperty
+{
+}
diff --git a/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php
new file mode 100644
index 00000000000..53a222e3cb0
--- /dev/null
+++ b/tests/Fixtures/TestBundle/DataTransformer/OutputDtoSameClassTransformer.php
@@ -0,0 +1,54 @@
+
+ *
+ * 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\DataTransformer;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputFallbackToSameClass as DummyDtoOutputFallbackToSameClassDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\DummyDtoOutputSameClass as DummyDtoOutputSameClassDocument;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputFallbackToSameClass;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoOutputSameClass;
+
+/**
+ * @author Daniel West
+ */
+final class OutputDtoSameClassTransformer implements DataTransformerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ if (
+ !$object instanceof DummyDtoOutputFallbackToSameClass &&
+ !$object instanceof DummyDtoOutputFallbackToSameClassDocument &&
+ !$object instanceof DummyDtoOutputSameClass &&
+ !$object instanceof DummyDtoOutputSameClassDocument
+ ) {
+ throw new \InvalidArgumentException();
+ }
+ $object->ipsum = 'modified';
+
+ return $object;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($data, string $to, array $context = []): bool
+ {
+ return (($data instanceof DummyDtoOutputFallbackToSameClass || $data instanceof DummyDtoOutputFallbackToSameClassDocument) && OutputDtoDummy::class === $to) ||
+ (($data instanceof DummyDtoOutputSameClass || $data instanceof DummyDtoOutputSameClassDocument) && (DummyDtoOutputSameClass::class === $to || DummyDtoOutputSameClassDocument::class === $to));
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php
new file mode 100644
index 00000000000..7a7d796d4a0
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputFallbackToSameClass.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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 ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+/**
+ * Dummy InputOutput.
+ *
+ * @author Daniel West
+ *
+ * @ApiResource(attributes={"output"=OutputDtoDummy::class})
+ * @ODM\Document
+ */
+class DummyDtoOutputFallbackToSameClass
+{
+ /**
+ * @var int The id
+ *
+ * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true)
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php
new file mode 100644
index 00000000000..fe121b03439
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/DummyDtoOutputSameClass.php
@@ -0,0 +1,54 @@
+
+ *
+ * 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 InputOutput.
+ *
+ * @author Daniel West
+ *
+ * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class})
+ * @ODM\Document
+ */
+class DummyDtoOutputSameClass
+{
+ /**
+ * @var int The id
+ *
+ * @ODM\Id(strategy="INCREMENT", type="integer", nullable=true)
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ODM\Field
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php
index 60fd02c77fb..6e0ad90c45a 100644
--- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php
+++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php
@@ -19,10 +19,13 @@
use Symfony\Component\Serializer\Annotation\Groups;
/**
- * @ApiResource(attributes={
- * "normalization_context"={"groups"={"inspection_read"}},
- * "denormalization_context"={"groups"={"inspection_write"}}
- * })
+ * @ApiResource(
+ * attributes={
+ * "normalization_context"={"groups"={"inspection_read"}},
+ * "denormalization_context"={"groups"={"inspection_write"}}
+ * },
+ * graphql={}
+ * )
* @ODM\Document
*/
class VoDummyInspection
diff --git a/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php
new file mode 100644
index 00000000000..3f319e4d987
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/OutputDtoDummy.php
@@ -0,0 +1,24 @@
+
+ *
+ * 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\Dto;
+
+/**
+ * OutputDtoDummy.
+ *
+ * @author Daniel West
+ */
+class OutputDtoDummy
+{
+ public $foo = 'foo';
+}
diff --git a/tests/Fixtures/TestBundle/Entity/Container.php b/tests/Fixtures/TestBundle/Entity/Container.php
deleted file mode 100644
index 09e0c75922d..00000000000
--- a/tests/Fixtures/TestBundle/Entity/Container.php
+++ /dev/null
@@ -1,68 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
-
-use ApiPlatform\Core\Annotation\ApiResource;
-use ApiPlatform\Core\Annotation\ApiSubresource;
-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\ORM\Mapping as ORM;
-
-/**
- * @ApiResource
- * @ORM\Entity
- */
-class Container
-{
- /**
- * @ORM\Id
- * @ORM\Column(type="guid")
- *
- * @var string UUID
- */
- private $id;
-
- /**
- * @ApiSubresource
- * @ORM\OneToMany(
- * targetEntity="Node",
- * mappedBy="container",
- * indexBy="serial",
- * fetch="LAZY",
- * cascade={},
- * orphanRemoval=false
- * )
- * @ORM\OrderBy({"serial"="ASC"})
- *
- * @var ArrayCollection|Node[]
- */
- private $nodes;
-
- public function getId(): string
- {
- return $this->id;
- }
-
- public function setId(string $id)
- {
- $this->id = $id;
- }
-
- /**
- * @return ArrayCollection|Node[]
- */
- public function getNodes()
- {
- return $this->nodes;
- }
-}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php
new file mode 100644
index 00000000000..a34152e2730
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputFallbackToSameClass.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\OutputDtoDummy;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Dummy InputOutput.
+ *
+ * @author Daniel West
+ *
+ * @ApiResource(attributes={"output"=OutputDtoDummy::class})
+ * @ORM\Entity
+ */
+class DummyDtoOutputFallbackToSameClass
+{
+ /**
+ * @var int The id
+ *
+ * @ORM\Column(type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php
new file mode 100644
index 00000000000..4bd43af9659
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/DummyDtoOutputSameClass.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Dummy InputOutput.
+ *
+ * @author Daniel West
+ *
+ * @ApiResource(attributes={"output"=DummyDtoOutputSameClass::class})
+ * @ORM\Entity
+ */
+class DummyDtoOutputSameClass
+{
+ /**
+ * @var int The id
+ *
+ * @ORM\Column(type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $lorem;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column
+ */
+ public $ipsum;
+
+ public function getId()
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/Node.php b/tests/Fixtures/TestBundle/Entity/Node.php
deleted file mode 100644
index e0985dc8853..00000000000
--- a/tests/Fixtures/TestBundle/Entity/Node.php
+++ /dev/null
@@ -1,62 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
-
-use ApiPlatform\Core\Annotation\ApiResource;
-use Doctrine\ORM\Mapping as ORM;
-
-/**
- * @see https://github.com/api-platform/core/pull/904#issuecomment-294132077
- * @ApiResource(graphql={})
- * @ORM\Entity
- */
-class Node
-{
- /**
- * @ORM\Id
- * @ORM\Column(name="serial", type="integer")
- *
- * @var int Node serial
- */
- private $serial;
-
- /**
- * @ORM\Id
- * @ORM\ManyToOne(targetEntity="Container", fetch="LAZY")
- * @ORM\JoinColumn(name="container_id", referencedColumnName="id", onDelete="RESTRICT")
- *
- * @var Container
- */
- private $container;
-
- public function setContainer(Container $container)
- {
- $this->container = $container;
- }
-
- public function getContainer(): Container
- {
- return $this->container;
- }
-
- public function setSerial(int $serial)
- {
- $this->serial = $serial;
- }
-
- public function getSerial(): int
- {
- return $this->serial;
- }
-}
diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php
index a0aca6022a6..dcb13c5d6ca 100644
--- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php
+++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php
@@ -19,10 +19,13 @@
use Symfony\Component\Serializer\Annotation\Groups;
/**
- * @ApiResource(attributes={
- * "normalization_context"={"groups"={"inspection_read"}},
- * "denormalization_context"={"groups"={"inspection_write"}}
- * })
+ * @ApiResource(
+ * attributes={
+ * "normalization_context"={"groups"={"inspection_read"}},
+ * "denormalization_context"={"groups"={"inspection_write"}}
+ * },
+ * graphql={}
+ * )
* @ORM\Entity
*/
class VoDummyInspection
diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php
index 6ab28fbb047..e5d2e9b52de 100644
--- a/tests/Fixtures/app/AppKernel.php
+++ b/tests/Fixtures/app/AppKernel.php
@@ -17,6 +17,8 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\TestBundle;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle;
+use Doctrine\Common\Inflector\Inflector;
+use Doctrine\Inflector\InflectorFactory;
use FOS\UserBundle\FOSUserBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
@@ -48,6 +50,12 @@ public function __construct(string $environment, bool $debug)
// patch for behat/symfony2-extension not supporting %env(APP_ENV)%
$this->environment = $_SERVER['APP_ENV'] ?? $environment;
+
+ // patch for old versions of Doctrine Inflector, to delete when we'll drop support for v1
+ // see https://github.com/doctrine/inflector/issues/147#issuecomment-628807276
+ if (!class_exists(InflectorFactory::class)) {
+ Inflector::rules('plural', ['/taxon/i' => 'taxa']);
+ }
}
public function registerBundles(): array
diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml
index caf23a48d8c..5736d354ac9 100644
--- a/tests/Fixtures/app/config/config_common.yml
+++ b/tests/Fixtures/app/config/config_common.yml
@@ -255,6 +255,12 @@ services:
tags:
- { name: 'api_platform.data_transformer' }
+ app.data_transformer.custom_output_dto_fallback_same_class:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\OutputDtoSameClassTransformer'
+ public: false
+ tags:
+ - { name: 'api_platform.data_transformer' }
+
app.data_transformer.input_dto:
class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataTransformer\InputDtoDataTransformer'
public: false
diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php
index af34e81465f..876db1df599 100644
--- a/tests/GraphQl/Type/TypeConverterTest.php
+++ b/tests/GraphQl/Type/TypeConverterTest.php
@@ -96,6 +96,19 @@ public function testConvertTypeNoGraphQlResourceMetadata(): void
$this->assertNull($graphqlType);
}
+ public function testConvertTypeNodeResource(): void
+ {
+ $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'node');
+
+ $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false);
+ $this->resourceMetadataFactoryProphecy->create('node')->shouldBeCalled()->willReturn((new ResourceMetadata('Node'))->withGraphql(['test']));
+
+ $this->expectException(\UnexpectedValueException::class);
+ $this->expectExceptionMessage('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.');
+
+ $this->typeConverter->convertType($type, false, null, null, null, 'resourceClass', 'rootClass', null, 0);
+ }
+
public function testConvertTypeResourceClassNotFound(): void
{
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy');
diff --git a/tests/Hal/Serializer/CollectionNormalizerTest.php b/tests/Hal/Serializer/CollectionNormalizerTest.php
index 96d2e1fe7ce..9c63cd8f7f1 100644
--- a/tests/Hal/Serializer/CollectionNormalizerTest.php
+++ b/tests/Hal/Serializer/CollectionNormalizerTest.php
@@ -112,7 +112,11 @@ public function testNormalizePartialPaginator()
private function normalizePaginator($partial = false)
{
- $paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class);
+ $paginatorProphecy = $this->prophesize(PaginatorInterface::class);
+ if ($partial) {
+ $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class);
+ }
+
$paginatorProphecy->getCurrentPage()->willReturn(3);
$paginatorProphecy->getItemsPerPage()->willReturn(12);
$paginatorProphecy->rewind()->will(function () {});
diff --git a/tests/Hydra/Serializer/CollectionNormalizerTest.php b/tests/Hydra/Serializer/CollectionNormalizerTest.php
index c98cc1a1cd1..2bc9d476b30 100644
--- a/tests/Hydra/Serializer/CollectionNormalizerTest.php
+++ b/tests/Hydra/Serializer/CollectionNormalizerTest.php
@@ -291,7 +291,10 @@ public function testNormalizePartialPaginator()
private function normalizePaginator($partial = false)
{
- $paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class);
+ $paginatorProphecy = $this->prophesize(PaginatorInterface::class);
+ if ($partial) {
+ $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class);
+ }
if (!$partial) {
$paginatorProphecy->getTotalItems()->willReturn(1312);