From e157133830112dc023b9625f2290d538827a22aa Mon Sep 17 00:00:00 2001 From: "Michael A. Bos" Date: Sun, 22 Sep 2019 10:50:35 -0700 Subject: [PATCH 1/2] add ability to return absolute urls instead of relative IRIs apply absolute url to all generated URLs/IRIs Allow passing an url generation strategy instead of a bool Switch to a resource attribute to manage URL generation strategy --- features/bootstrap/DoctrineContext.php | 72 +++++++++++ features/doctrine/search_filter.feature | 1 - features/hal/absolute_url.feature | 119 +++++++++++++++++ features/hal/network_path.feature | 117 +++++++++++++++++ features/jsonapi/absolute_url.feature | 120 ++++++++++++++++++ features/jsonapi/network_path.feature | 120 ++++++++++++++++++ features/jsonld/absolute_url.feature | 83 ++++++++++++ features/jsonld/network_path.feature | 86 +++++++++++++ src/Annotation/ApiResource.php | 11 +- .../ApiPlatformExtension.php | 2 + .../DependencyInjection/Configuration.php | 2 +- .../Symfony/Bundle/Resources/config/api.xml | 2 + .../Symfony/Bundle/Resources/config/hal.xml | 1 + .../Bundle/Resources/config/jsonapi.xml | 1 + src/Bridge/Symfony/Routing/IriConverter.php | 31 +++-- src/Bridge/Symfony/Routing/Router.php | 8 +- src/Hal/Serializer/CollectionNormalizer.php | 22 +++- .../Serializer/CollectionNormalizer.php | 22 +++- src/JsonLd/ContextBuilder.php | 9 +- .../AbstractCollectionNormalizer.php | 5 +- src/Util/IriHelper.php | 20 +-- tests/Annotation/ApiResourceTest.php | 3 + .../ApiPlatformExtensionTest.php | 1 + .../Symfony/Routing/IriConverterTest.php | 50 ++++++-- tests/Bridge/Symfony/Routing/RouterTest.php | 19 +++ .../TestBundle/Document/AbsoluteUrlDummy.php | 40 ++++++ .../Document/AbsoluteUrlRelationDummy.php | 48 +++++++ .../TestBundle/Document/NetworkPathDummy.php | 40 ++++++ .../Document/NetworkPathRelationDummy.php | 48 +++++++ .../TestBundle/Entity/AbsoluteUrlDummy.php | 42 ++++++ .../Entity/AbsoluteUrlRelationDummy.php | 50 ++++++++ .../TestBundle/Entity/NetworkPathDummy.php | 42 ++++++ .../Entity/NetworkPathRelationDummy.php | 50 ++++++++ .../Serializer/CollectionNormalizerTest.php | 13 +- .../Serializer/CollectionNormalizerTest.php | 40 +++++- tests/Util/IriHelperTest.php | 58 ++++++++- 36 files changed, 1339 insertions(+), 59 deletions(-) create mode 100644 features/hal/absolute_url.feature create mode 100644 features/hal/network_path.feature create mode 100644 features/jsonapi/absolute_url.feature create mode 100644 features/jsonapi/network_path.feature create mode 100644 features/jsonld/absolute_url.feature create mode 100644 features/jsonld/network_path.feature create mode 100644 tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php create mode 100644 tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php create mode 100644 tests/Fixtures/TestBundle/Document/NetworkPathDummy.php create mode 100644 tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php create mode 100644 tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index bfb1ee4cefd..8bcbb41aa5c 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -11,6 +11,8 @@ declare(strict_types=1); +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlDummy as AbsoluteUrlDummyDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Address as AddressDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Answer as AnswerDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeItem as CompositeItemDocument; @@ -52,6 +54,8 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\FourthLevel as FourthLevelDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Greeting as GreetingDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\MaxDepthDummy as MaxDepthDummyDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\NetworkPathDummy as NetworkPathDummyDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\NetworkPathRelationDummy as NetworkPathRelationDummyDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Order as OrderDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Person as PersonDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\PersonToPet as PersonToPetDocument; @@ -69,6 +73,8 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\UrlEncodedId as UrlEncodedIdDocument; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeItem; @@ -113,6 +119,9 @@ 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\NetworkPathDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\NetworkPathRelationDummy; +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; @@ -1527,6 +1536,7 @@ public function thereAreConvertedOwnerObjects(int $nb) } /** +<<<<<<< HEAD * @Given there are :nb dummy mercure objects */ public function thereAreDummyMercureObjects(int $nb) @@ -1542,6 +1552,36 @@ public function thereAreDummyMercureObjects(int $nb) $this->manager->persist($relatedDummy); $this->manager->persist($dummyMercure); +======= + * @Given there are :nb absoluteUrlDummy objects with a related absoluteUrlRelationDummy + */ + public function thereAreAbsoluteUrlDummies(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $absoluteUrlRelationDummy = $this->buildAbsoluteUrlRelationDummy(); + $absoluteUrlDummy = $this->buildAbsoluteUrlDummy(); + $absoluteUrlDummy->absoluteUrlRelationDummy = $absoluteUrlRelationDummy; + + $this->manager->persist($absoluteUrlRelationDummy); + $this->manager->persist($absoluteUrlDummy); + } + + $this->manager->flush(); + } + + /** + * @Given there are :nb networkPathDummy objects with a related networkPathRelationDummy + */ + public function thereArenetworkPathDummies(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $networkPathRelationDummy = $this->buildNetworkPathRelationDummy(); + $networkPathDummy = $this->buildNetworkPathDummy(); + $networkPathDummy->networkPathRelationDummy = $networkPathRelationDummy; + + $this->manager->persist($networkPathRelationDummy); + $this->manager->persist($networkPathDummy); +>>>>>>> 880597f6... add ability to return absolute urls instead of relative IRIs } $this->manager->flush(); @@ -1926,10 +1966,42 @@ private function buildConvertedRelated() } /** +<<<<<<< HEAD * @return DummyMercure|DummyMercureDocument */ private function buildDummyMercure() { return $this->isOrm() ? new DummyMercure() : new DummyMercureDocument(); +======= + * @return AbsoluteUrlDummyDocument|AbsoluteUrlDummy + */ + private function buildAbsoluteUrlDummy() + { + return $this->isOrm() ? new AbsoluteUrlDummy() : new AbsoluteUrlDummyDocument(); + } + + /** + * @return AbsoluteUrlRelationDummyDocument|AbsoluteUrlRelationDummy + */ + private function buildAbsoluteUrlRelationDummy() + { + return $this->isOrm() ? new AbsoluteUrlRelationDummy() : new AbsoluteUrlRelationDummyDocument(); + } + + /** + * @return NetworkPathDummyDocument|NetworkPathDummy + */ + private function buildNetworkPathDummy() + { + return $this->isOrm() ? new NetworkPathDummy() : new NetworkPathDummyDocument(); + } + + /** + * @return NetworkPathRelationDummyDocument|NetworkPathRelationDummy + */ + private function buildNetworkPathRelationDummy() + { + return $this->isOrm() ? new NetworkPathRelationDummy() : new NetworkPathRelationDummyDocument(); +>>>>>>> 880597f6... add ability to return absolute urls instead of relative IRIs } } diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature index e33b7515c47..b895df1a12a 100644 --- a/features/doctrine/search_filter.feature +++ b/features/doctrine/search_filter.feature @@ -805,7 +805,6 @@ Feature: Search filter on collections When I send a "GET" request to "/converted_owners?name_converted.name_converted=Converted 3" Then the response status code should be 200 And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - Then print last JSON response And the JSON should be valid according to this schema: """ { diff --git a/features/hal/absolute_url.feature b/features/hal/absolute_url.feature new file mode 100644 index 00000000000..394bb425c10 --- /dev/null +++ b/features/hal/absolute_url.feature @@ -0,0 +1,119 @@ +Feature: IRI should contain Absolute URL + In order to add detail to IRIs + Include the absolute url + + @createSchema + Scenario: I should be able to GET a collection of Objects with Absolute Urls + Given there are 1 absoluteUrlDummy objects with a related absoluteUrlRelationDummy + And I add "Accept" header equal to "application/hal+json" + And I send a "GET" request to "/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_dummies" + }, + "item": [ + { + "href": "http://example.com/absolute_url_dummies/1" + } + ] + }, + "totalItems": 1, + "itemsPerPage": 3, + "_embedded": { + "item": [ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_dummies/1" + }, + "absoluteUrlRelationDummy": { + "href": "http://example.com/absolute_url_relation_dummies/1" + } + }, + "id": 1 + } + ] + } + } + """ + + Scenario: I should be able to POST an object using an Absolute Url + Given I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/absolute_url_relation_dummies" with body: + """ + { + "absolute_url_dummies": "http://example.com/absolute_url_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_relation_dummies/2" + } + }, + "id": 2 + } + """ + + Scenario: I should be able to GET an Item with Absolute Urls + Given I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" + And I send a "GET" request to "/absolute_url_dummies/1" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_dummies/1" + }, + "absoluteUrlRelationDummy": { + "href": "http://example.com/absolute_url_relation_dummies/1" + } + }, + "id": 1 + } + """ + + Scenario: I should be able to GET subresources with Absolute Urls + Given I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" + And I send a "GET" request to "/absolute_url_relation_dummies/1/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_relation_dummies/1/absolute_url_dummies" + }, + "item": [ + { + "href": "http://example.com/absolute_url_dummies/1" + } + ] + }, + "totalItems": 1, + "itemsPerPage": 3, + "_embedded": { + "item": [ + { + "_links": { + "self": { + "href": "http://example.com/absolute_url_dummies/1" + }, + "absoluteUrlRelationDummy": { + "href": "http://example.com/absolute_url_relation_dummies/1" + } + }, + "id": 1 + } + ] + } + } + """ diff --git a/features/hal/network_path.feature b/features/hal/network_path.feature new file mode 100644 index 00000000000..5fba0bcbb21 --- /dev/null +++ b/features/hal/network_path.feature @@ -0,0 +1,117 @@ +Feature: IRI should contain network path + In order to add detail to IRIs + Include the network path + + @createSchema + Scenario: I should be able to GET a collection of objects with network paths + Given there are 1 networkPathDummy objects with a related networkPathRelationDummy + And I add "Accept" header equal to "application/hal+json" + And I send a "GET" request to "/network_path_dummies" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "//example.com/network_path_dummies" + }, + "item": [ + { + "href": "//example.com/network_path_dummies/1" + } + ] + }, + "totalItems": 1, + "itemsPerPage": 3, + "_embedded": { + "item": [ + { + "_links": { + "self": { + "href": "//example.com/network_path_dummies/1" + }, + "networkPathRelationDummy": { + "href": "//example.com/network_path_relation_dummies/1" + } + }, + "id": 1 + } + ] + } + } + """ + + Scenario: I should be able to POST an object using a network path + Given I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/network_path_relation_dummies" with body: + """ + { + "network_path_dummies": "//example.com/network_path_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "//example.com/network_path_relation_dummies/2" + } + }, + "id": 2 + } + """ + + Scenario: I should be able to GET an Item with network paths + Given I add "Accept" header equal to "application/hal+json" + And I send a "GET" request to "/network_path_dummies/1" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "//example.com/network_path_dummies/1" + }, + "networkPathRelationDummy": { + "href": "//example.com/network_path_relation_dummies/1" + } + }, + "id": 1 + } + """ + + Scenario: I should be able to GET subresources with network paths + Given I add "Accept" header equal to "application/hal+json" + And I send a "GET" request to "/network_path_relation_dummies/1/network_path_dummies" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "//example.com/network_path_relation_dummies/1/network_path_dummies" + }, + "item": [ + { + "href": "//example.com/network_path_dummies/1" + } + ] + }, + "totalItems": 1, + "itemsPerPage": 3, + "_embedded": { + "item": [ + { + "_links": { + "self": { + "href": "//example.com/network_path_dummies/1" + }, + "networkPathRelationDummy": { + "href": "//example.com/network_path_relation_dummies/1" + } + }, + "id": 1 + } + ] + } + } + """ diff --git a/features/jsonapi/absolute_url.feature b/features/jsonapi/absolute_url.feature new file mode 100644 index 00000000000..2bf4d2f0367 --- /dev/null +++ b/features/jsonapi/absolute_url.feature @@ -0,0 +1,120 @@ +Feature: IRI should contain Absolute URL + In order to add detail to IRIs + Include the absolute url + + @createSchema + Scenario: I should be able to GET a collection of Objects with Absolute Urls + Given there are 1 absoluteUrlDummy objects with a related absoluteUrlRelationDummy + And I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "links": { + "self": "http://example.com/absolute_url_dummies" + }, + "meta": { + "totalItems": 1, + "itemsPerPage": 3, + "currentPage": 1 + }, + "data": [ + { + "id": "http://example.com/absolute_url_dummies/1", + "type": "AbsoluteUrlDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "absoluteUrlRelationDummy": { + "data": { + "type": "AbsoluteUrlRelationDummy", + "id": "http://example.com/absolute_url_relation_dummies/1" + } + } + } + } + ] + } + """ + + Scenario: I should be able to POST an object using an Absolute Url + Given I add "Accept" header equal to "application/vnd.api+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/absolute_url_relation_dummies" with body: + """ + { + "absolute_url_dummies": "http://example.com/absolute_url_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "data": { + "id": "http://example.com/absolute_url_relation_dummies/2", + "type": "AbsoluteUrlRelationDummy", + "attributes": { + "_id": 2 + } + } + } + """ + + Scenario: I should be able to GET an Item with Absolute Urls + Given I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/absolute_url_dummies/1" + And the JSON should be equal to: + """ + { + "data": { + "id": "http://example.com/absolute_url_dummies/1", + "type": "AbsoluteUrlDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "absoluteUrlRelationDummy": { + "data": { + "type": "AbsoluteUrlRelationDummy", + "id": "http://example.com/absolute_url_relation_dummies/1" + } + } + } + } + } + """ + + Scenario: I should be able to GET subresources with Absolute Urls + Given I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/absolute_url_relation_dummies/1/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "links": { + "self": "http://example.com/absolute_url_relation_dummies/1/absolute_url_dummies" + }, + "meta": { + "totalItems": 1, + "itemsPerPage": 3, + "currentPage": 1 + }, + "data": [ + { + "id": "http://example.com/absolute_url_dummies/1", + "type": "AbsoluteUrlDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "absoluteUrlRelationDummy": { + "data": { + "type": "AbsoluteUrlRelationDummy", + "id": "http://example.com/absolute_url_relation_dummies/1" + } + } + } + } + ] + } + """ diff --git a/features/jsonapi/network_path.feature b/features/jsonapi/network_path.feature new file mode 100644 index 00000000000..9837fb065e4 --- /dev/null +++ b/features/jsonapi/network_path.feature @@ -0,0 +1,120 @@ +Feature: IRI should contain network path + In order to add detail to IRIs + Include the network path + + @createSchema + Scenario: I should be able to GET a collection of objects with network paths + Given there are 1 networkPathDummy objects with a related networkPathRelationDummy + And I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/network_path_dummies" + And the JSON should be equal to: + """ + { + "links": { + "self": "//example.com/network_path_dummies" + }, + "meta": { + "totalItems": 1, + "itemsPerPage": 3, + "currentPage": 1 + }, + "data": [ + { + "id": "//example.com/network_path_dummies/1", + "type": "NetworkPathDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "networkPathRelationDummy": { + "data": { + "type": "NetworkPathRelationDummy", + "id": "//example.com/network_path_relation_dummies/1" + } + } + } + } + ] + } + """ + + Scenario: I should be able to POST an object using a network path + Given I add "Accept" header equal to "application/vnd.api+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/network_path_relation_dummies" with body: + """ + { + "network_path_dummies": "//example.com/network_path_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "data": { + "id": "//example.com/network_path_relation_dummies/2", + "type": "NetworkPathRelationDummy", + "attributes": { + "_id": 2 + } + } + } + """ + + Scenario: I should be able to GET an Item with network paths + Given I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/network_path_dummies/1" + And the JSON should be equal to: + """ + { + "data": { + "id": "//example.com/network_path_dummies/1", + "type": "NetworkPathDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "networkPathRelationDummy": { + "data": { + "type": "NetworkPathRelationDummy", + "id": "//example.com/network_path_relation_dummies/1" + } + } + } + } + } + """ + + Scenario: I should be able to GET subresources with network paths + Given I add "Accept" header equal to "application/vnd.api+json" + And I send a "GET" request to "/network_path_relation_dummies/1/network_path_dummies" + And the JSON should be equal to: + """ + { + "links": { + "self": "//example.com/network_path_relation_dummies/1/network_path_dummies" + }, + "meta": { + "totalItems": 1, + "itemsPerPage": 3, + "currentPage": 1 + }, + "data": [ + { + "id": "//example.com/network_path_dummies/1", + "type": "NetworkPathDummy", + "attributes": { + "_id": 1 + }, + "relationships": { + "networkPathRelationDummy": { + "data": { + "type": "NetworkPathRelationDummy", + "id": "//example.com/network_path_relation_dummies/1" + } + } + } + } + ] + } + """ diff --git a/features/jsonld/absolute_url.feature b/features/jsonld/absolute_url.feature new file mode 100644 index 00000000000..4421c7702da --- /dev/null +++ b/features/jsonld/absolute_url.feature @@ -0,0 +1,83 @@ +Feature: IRI should contain Absolute URL + In order to add detail to IRIs + Include the absolute url + + @createSchema + Scenario: I should be able to GET a collection of Objects with Absolute Urls + Given there are 1 absoluteUrlDummy objects with a related absoluteUrlRelationDummy + And I add "Accept" header equal to "application/ld+json" + And I send a "GET" request to "/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "@context": "http://example.com/contexts/AbsoluteUrlDummy", + "@id": "http://example.com/absolute_url_dummies", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "http://example.com/absolute_url_dummies/1", + "@type": "AbsoluteUrlDummy", + "absoluteUrlRelationDummy": "http://example.com/absolute_url_relation_dummies/1", + "id": 1 + } + ], + "hydra:totalItems": 1 + } + + """ + + Scenario: I should be able to POST an object using an Absolute Url + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/absolute_url_relation_dummies" with body: + """ + { + "absolute_url_dummies": "http://example.com/absolute_url_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "@context": "http://example.com/contexts/AbsoluteUrlRelationDummy", + "@id": "http://example.com/absolute_url_relation_dummies/2", + "@type": "AbsoluteUrlRelationDummy", + "absoluteUrlDummies": [], + "id": 2 + } + """ + + Scenario: I should be able to GET an Item with Absolute Urls + Given I add "Accept" header equal to "application/ld+json" + And I send a "GET" request to "/absolute_url_dummies/1" + And the JSON should be equal to: + """ + { + "@context": "http://example.com/contexts/AbsoluteUrlDummy", + "@id": "http://example.com/absolute_url_dummies/1", + "@type": "AbsoluteUrlDummy", + "absoluteUrlRelationDummy": "http://example.com/absolute_url_relation_dummies/1", + "id": 1 + } + """ + + Scenario: I should be able to GET subresources with Absolute Urls + Given I add "Accept" header equal to "application/ld+json" + And I send a "GET" request to "/absolute_url_relation_dummies/1/absolute_url_dummies" + And the JSON should be equal to: + """ + { + "@context": "http://example.com/contexts/AbsoluteUrlDummy", + "@id": "http://example.com/absolute_url_relation_dummies/1/absolute_url_dummies", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "http://example.com/absolute_url_dummies/1", + "@type": "AbsoluteUrlDummy", + "absoluteUrlRelationDummy": "http://example.com/absolute_url_relation_dummies/1", + "id": 1 + } + ], + "hydra:totalItems": 1 + } + """ diff --git a/features/jsonld/network_path.feature b/features/jsonld/network_path.feature new file mode 100644 index 00000000000..2f77e39aa91 --- /dev/null +++ b/features/jsonld/network_path.feature @@ -0,0 +1,86 @@ +Feature: IRI should contain network path + In order to add detail to IRIs + Include the network path + + @createSchema + Scenario: I should be able to GET a collection of objects with network paths + Given there are 1 networkPathDummy objects with a related networkPathRelationDummy + And I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/json" + And I send a "GET" request to "/network_path_dummies" + And the JSON should be equal to: + """ + { + "@context": "//example.com/contexts/NetworkPathDummy", + "@id": "//example.com/network_path_dummies", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "//example.com/network_path_dummies/1", + "@type": "NetworkPathDummy", + "networkPathRelationDummy": "//example.com/network_path_relation_dummies/1", + "id": 1 + } + ], + "hydra:totalItems": 1 + } + + """ + + Scenario: I should be able to POST an object using a network path + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/network_path_relation_dummies" with body: + """ + { + "network_path_dummies": "//example.com/network_path_dummies/1" + } + """ + Then the response status code should be 201 + And the JSON should be equal to: + """ + { + "@context": "//example.com/contexts/NetworkPathRelationDummy", + "@id": "//example.com/network_path_relation_dummies/2", + "@type": "NetworkPathRelationDummy", + "networkPathDummies": [], + "id": 2 + } + """ + + Scenario: I should be able to GET an Item with network paths + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/json" + And I send a "GET" request to "/network_path_dummies/1" + And the JSON should be equal to: + """ + { + "@context": "//example.com/contexts/NetworkPathDummy", + "@id": "//example.com/network_path_dummies/1", + "@type": "NetworkPathDummy", + "networkPathRelationDummy": "//example.com/network_path_relation_dummies/1", + "id": 1 + } + """ + + Scenario: I should be able to GET subresources with network paths + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/json" + And I send a "GET" request to "/network_path_relation_dummies/1/network_path_dummies" + And the JSON should be equal to: + """ + { + "@context": "//example.com/contexts/NetworkPathDummy", + "@id": "//example.com/network_path_relation_dummies/1/network_path_dummies", + "@type": "hydra:Collection", + "hydra:member": [ + { + "@id": "//example.com/network_path_dummies/1", + "@type": "NetworkPathDummy", + "networkPathRelationDummy": "//example.com/network_path_relation_dummies/1", + "id": 1 + } + ], + "hydra:totalItems": 1 + } + """ diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php index 1d78c6a26fd..1a89a559ec7 100644 --- a/src/Annotation/ApiResource.php +++ b/src/Annotation/ApiResource.php @@ -66,7 +66,8 @@ * @Attribute("subresourceOperations", type="array"), * @Attribute("sunset", type="string"), * @Attribute("swaggerContext", type="array"), - * @Attribute("validationGroups", type="mixed") + * @Attribute("urlGenerationStrategy", type="int"), + * @Attribute("validationGroups", type="mixed"), * ) */ final class ApiResource @@ -119,6 +120,7 @@ final class ApiResource 'routePrefix', 'sunset', 'swaggerContext', + 'urlGenerationStrategy', 'validationGroups', ]; @@ -434,6 +436,13 @@ final class ApiResource */ private $validationGroups; + /** + * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 + * + * @var int + */ + private $urlGenerationStrategy; + /** * @throws InvalidArgumentException */ diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index bf48df38a3b..0b1db78d2c8 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection; use ApiPlatform\Core\Api\FilterInterface; +use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\AbstractFilter as DoctrineMongoDbOdmAbstractFilter; @@ -171,6 +172,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.description', $config['description']); $container->setParameter('api_platform.version', $config['version']); $container->setParameter('api_platform.show_webby', $config['show_webby']); + $container->setParameter('api_platform.url_generation_strategy', $config['defaults']['url_generation_strategy'] ?? UrlGeneratorInterface::ABS_PATH); $container->setParameter('api_platform.exception_to_status', $config['exception_to_status']); $container->setParameter('api_platform.formats', $formats); $container->setParameter('api_platform.patch_formats', $patchFormats); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index d740813c86c..536d15ad886 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -553,7 +553,7 @@ private function addDefaultsSection(ArrayNodeDefinition $rootNode): void $defaultsNode ->ignoreExtraKeys() ->beforeNormalization() - ->always(function (array $defaults) use ($nameConverter) { + ->always(static function (array $defaults) use ($nameConverter) { $normalizedDefaults = []; foreach ($defaults as $option => $value) { $option = $nameConverter->normalize($option); diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 71a070c8505..ffa00199f6d 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -51,6 +51,7 @@ + %api_platform.url_generation_strategy% @@ -64,6 +65,7 @@ + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index a42c976c90b..9ba48f25be3 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -22,6 +22,7 @@ %api_platform.collection.pagination.page_parameter_name% + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml index 7d232706bc1..858b30e30d9 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml @@ -26,6 +26,7 @@ %api_platform.collection.pagination.page_parameter_name% + diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index 82ec9bc38f9..1e031609f8a 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\OperationDataProviderTrait; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; @@ -29,6 +28,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 ApiPlatform\Core\Util\AttributesExtractor; use ApiPlatform\Core\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -50,7 +50,7 @@ final class IriConverter implements IriConverterInterface private $router; private $identifiersExtractor; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierConverterInterface $identifierConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierConverterInterface $identifierConverter = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->itemDataProvider = $itemDataProvider; $this->routeNameResolver = $routeNameResolver; @@ -64,6 +64,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName @trigger_error(sprintf('Not injecting "%s" is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', IdentifiersExtractorInterface::class), E_USER_DEPRECATED); $this->identifiersExtractor = new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor()); } + $this->resourceMetadataFactory = $resourceMetadataFactory; } /** @@ -115,7 +116,7 @@ public function getItemFromIri(string $iri, array $context = []) /** * {@inheritdoc} */ - public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string + public function getIriFromItem($item, int $referenceType = null): string { $resourceClass = $this->getResourceClass($item, true); @@ -125,16 +126,16 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass), $e->getCode(), $e); } - return $this->getItemIriFromResourceClass($resourceClass, $identifiers, $referenceType); + return $this->getItemIriFromResourceClass($resourceClass, $identifiers, $this->getReferenceType($resourceClass, $referenceType)); } /** * {@inheritdoc} */ - public function getIriFromResourceClass(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string + public function getIriFromResourceClass(string $resourceClass, int $referenceType = null): string { try { - return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::COLLECTION), [], $referenceType); + return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::COLLECTION), [], $this->getReferenceType($resourceClass, $referenceType)); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); } @@ -143,14 +144,14 @@ public function getIriFromResourceClass(string $resourceClass, int $referenceTyp /** * {@inheritdoc} */ - public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = UrlGeneratorInterface::ABS_PATH): string + public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = null): string { $routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM); try { $identifiers = $this->generateIdentifiersUrl($identifiers, $resourceClass); - return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType); + return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $this->getReferenceType($resourceClass, $referenceType)); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); } @@ -159,10 +160,10 @@ public function getItemIriFromResourceClass(string $resourceClass, array $identi /** * {@inheritdoc} */ - public function getSubresourceIriFromResourceClass(string $resourceClass, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): string + public function getSubresourceIriFromResourceClass(string $resourceClass, array $context, int $referenceType = null): string { try { - return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $referenceType); + return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $this->getReferenceType($resourceClass, $referenceType)); } catch (RoutingExceptionInterface $e) { throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); } @@ -191,4 +192,14 @@ private function generateIdentifiersUrl(array $identifiers, string $resourceClas return array_values($identifiers); } + + private function getReferenceType(string $resourceClass, ?int $referenceType): ?int + { + if (null === $referenceType && null !== $this->resourceMetadataFactory) { + $metadata = $this->resourceMetadataFactory->create($resourceClass); + $referenceType = $metadata->getAttribute('url_generation_strategy'); + } + + return $referenceType; + } } diff --git a/src/Bridge/Symfony/Routing/Router.php b/src/Bridge/Symfony/Routing/Router.php index 3959a6c39ed..52aecd59c96 100644 --- a/src/Bridge/Symfony/Routing/Router.php +++ b/src/Bridge/Symfony/Routing/Router.php @@ -35,10 +35,12 @@ final class Router implements RouterInterface, UrlGeneratorInterface ]; private $router; + private $urlGenerationStrategy; - public function __construct(RouterInterface $router) + public function __construct(RouterInterface $router, int $urlGenerationStrategy = self::ABS_PATH) { $this->router = $router; + $this->urlGenerationStrategy = $urlGenerationStrategy; } /** @@ -96,8 +98,8 @@ public function match($pathInfo) /** * {@inheritdoc} */ - public function generate($name, $parameters = [], $referenceType = self::ABS_PATH) + public function generate($name, $parameters = [], $referenceType = null) { - return $this->router->generate($name, $parameters, self::CONST_MAP[$referenceType]); + return $this->router->generate($name, $parameters, self::CONST_MAP[$referenceType ?? $this->urlGenerationStrategy]); } } diff --git a/src/Hal/Serializer/CollectionNormalizer.php b/src/Hal/Serializer/CollectionNormalizer.php index d8266e9f406..8f00554de2f 100644 --- a/src/Hal/Serializer/CollectionNormalizer.php +++ b/src/Hal/Serializer/CollectionNormalizer.php @@ -13,6 +13,8 @@ namespace ApiPlatform\Core\Hal\Serializer; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Serializer\AbstractCollectionNormalizer; use ApiPlatform\Core\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; @@ -27,32 +29,40 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonhal'; + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataFactoryInterface $resourceMetadataFactory) + { + parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); + } + /** * {@inheritdoc} */ protected function getPaginationData($object, array $context = []): array { [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); - $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); + $parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName); + + $metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? ''); + $urlGenerationStrategy = $metadata->getAttribute('url_generation_strategy'); $data = [ '_links' => [ - 'self' => ['href' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null)], + 'self' => ['href' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy)], ], ]; if ($paginated) { if (null !== $lastPage) { - $data['_links']['first']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.); - $data['_links']['last']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage); + $data['_links']['first']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy); + $data['_links']['last']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy); } if (1. !== $currentPage) { - $data['_links']['prev']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.); + $data['_links']['prev']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy); } if ((null !== $lastPage && $currentPage !== $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) { - $data['_links']['next']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.); + $data['_links']['next']['href'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy); } } diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php index c92b2548f13..c0e920f9f29 100644 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ b/src/JsonApi/Serializer/CollectionNormalizer.php @@ -13,6 +13,8 @@ namespace ApiPlatform\Core\JsonApi\Serializer; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Serializer\AbstractCollectionNormalizer; use ApiPlatform\Core\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; @@ -28,32 +30,40 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonapi'; + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataFactoryInterface $resourceMetadataFactory) + { + parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); + } + /** * {@inheritdoc} */ protected function getPaginationData($object, array $context = []): array { [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); - $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); + $parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName); + + $metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? ''); + $urlGenerationStrategy = $metadata->getAttribute('url_generation_strategy'); $data = [ 'links' => [ - 'self' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null), + 'self' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy), ], ]; if ($paginated) { if (null !== $lastPage) { - $data['links']['first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.); - $data['links']['last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage); + $data['links']['first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy); + $data['links']['last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy); } if (1. !== $currentPage) { - $data['links']['prev'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.); + $data['links']['prev'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy); } if (null !== $lastPage && $currentPage !== $lastPage || null === $lastPage && $pageTotalItems >= $itemsPerPage) { - $data['links']['next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.); + $data['links']['next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy); } } diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index 88fdf1c08e4..625e3e7287a 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -90,8 +90,8 @@ public function getEntrypointContext(int $referenceType = UrlGeneratorInterface: */ public function getResourceContext(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array { - $metadata = $this->resourceMetadataFactory->create($resourceClass); - if (null === $shortName = $metadata->getShortName()) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + if (null === $shortName = $resourceMetadata->getShortName()) { return []; } @@ -101,9 +101,12 @@ public function getResourceContext(string $resourceClass, int $referenceType = U /** * {@inheritdoc} */ - public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string + public function getResourceContextUri(string $resourceClass, int $referenceType = null): string { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + if (null === $referenceType) { + $referenceType = $resourceMetadata->getAttribute('url_generation_strategy'); + } return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $resourceMetadata->getShortName()], $referenceType); } diff --git a/src/Serializer/AbstractCollectionNormalizer.php b/src/Serializer/AbstractCollectionNormalizer.php index 59268475747..4af7d4c456e 100644 --- a/src/Serializer/AbstractCollectionNormalizer.php +++ b/src/Serializer/AbstractCollectionNormalizer.php @@ -16,6 +16,7 @@ use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\DataProvider\PaginatorInterface; use ApiPlatform\Core\DataProvider\PartialPaginatorInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; @@ -40,11 +41,13 @@ abstract class AbstractCollectionNormalizer implements NormalizerInterface, Norm protected $resourceClassResolver; protected $pageParameterName; + protected $resourceMetadataFactory; - public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $this->resourceClassResolver = $resourceClassResolver; $this->pageParameterName = $pageParameterName; + $this->resourceMetadataFactory = $resourceMetadataFactory; } /** diff --git a/src/Util/IriHelper.php b/src/Util/IriHelper.php index d88a61f0592..4762a1e20d1 100644 --- a/src/Util/IriHelper.php +++ b/src/Util/IriHelper.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Util; +use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; /** @@ -56,27 +57,30 @@ public static function parseIri(string $iri, string $pageParameterName): array * * @param float $page */ - public static function createIri(array $parts, array $parameters, string $pageParameterName = null, float $page = null, bool $absoluteUrl = false): string + public static function createIri(array $parts, array $parameters, string $pageParameterName = null, float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string { if (null !== $page && null !== $pageParameterName) { $parameters[$pageParameterName] = $page; } + if (\is_bool($urlGenerationStrategy)) { + @trigger_error(sprintf('Passing a bool as 5th parameter to "%s::createIri()" is deprecated since API Platform 2.6. Pass an "%s" constant (int) instead.', __CLASS__, UrlGeneratorInterface::class), E_USER_DEPRECATED); + $urlGenerationStrategy = $urlGenerationStrategy ? UrlGeneratorInterface::ABS_URL : UrlGeneratorInterface::ABS_PATH; + } + $query = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); $parts['query'] = preg_replace('/%5B\d+%5D/', '%5B%5D', $query); $url = ''; - - if ($absoluteUrl && isset($parts['host'])) { + if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) { if (isset($parts['scheme'])) { - $url .= $parts['scheme']; + $scheme = $parts['scheme']; } elseif (isset($parts['port']) && 443 === $parts['port']) { - $url .= 'https'; + $scheme = 'https'; } else { - $url .= 'http'; + $scheme = 'http'; } - - $url .= '://'; + $url .= UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy ? '//' : "$scheme://"; if (isset($parts['user'])) { $url .= $parts['user']; diff --git a/tests/Annotation/ApiResourceTest.php b/tests/Annotation/ApiResourceTest.php index 5f24c6aacca..48e040c0ac9 100644 --- a/tests/Annotation/ApiResourceTest.php +++ b/tests/Annotation/ApiResourceTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Core\Tests\Annotation; use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Tests\Fixtures\AnnotatedClass; use Doctrine\Common\Annotations\AnnotationReader; @@ -64,6 +65,7 @@ public function testConstruct() 'swaggerContext' => ['description' => 'bar'], 'validationGroups' => ['foo', 'bar'], 'sunset' => 'Thu, 11 Oct 2018 00:00:00 +0200', + 'urlGenerationStrategy' => UrlGeneratorInterface::ABS_PATH, ]); $this->assertSame('shortName', $resource->shortName); @@ -105,6 +107,7 @@ public function testConstruct() 'validation_groups' => ['baz', 'qux'], 'cache_headers' => ['max_age' => 0, 'shared_max_age' => 0, 'vary' => ['Custom-Vary-1', 'Custom-Vary-2']], 'sunset' => 'Thu, 11 Oct 2018 00:00:00 +0200', + 'url_generation_strategy' => 1, ], $resource->attributes); } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index dec70e86d7b..9ada175bd00 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -862,6 +862,7 @@ private function getPartialContainerBuilderProphecy($configuration = null) 'api_platform.defaults' => ['attributes' => []], 'api_platform.enable_entrypoint' => true, 'api_platform.enable_docs' => true, + 'api_platform.url_generation_strategy' => 1, ]; $pagination = [ diff --git a/tests/Bridge/Symfony/Routing/IriConverterTest.php b/tests/Bridge/Symfony/Routing/IriConverterTest.php index 81f5fcc911b..365c6b1cd71 100644 --- a/tests/Bridge/Symfony/Routing/IriConverterTest.php +++ b/tests/Bridge/Symfony/Routing/IriConverterTest.php @@ -27,6 +27,8 @@ 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 ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use PHPUnit\Framework\TestCase; @@ -139,12 +141,27 @@ public function testGetIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::COLLECTION)->willReturn('dummies'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('dummies', [], UrlGeneratorInterface::ABS_PATH)->willReturn('/dummies'); + $routerProphecy->generate('dummies', [], null)->willReturn('/dummies'); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $this->assertEquals($converter->getIriFromResourceClass(Dummy::class), '/dummies'); } + public function testGetIriFromResourceClassAbsoluteUrl() + { + $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); + $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::COLLECTION)->willReturn('dummies'); + + $routerProphecy = $this->prophesize(RouterInterface::class); + $routerProphecy->generate('dummies', [], UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('', '', '', [], [], ['url_generation_strategy' => UrlGeneratorInterface::ABS_URL])); + + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, null, $resourceMetadataFactoryProphecy->reveal()); + $this->assertEquals($converter->getIriFromResourceClass(Dummy::class), 'http://example.com/dummies'); + } + public function testNotAbleToGenerateGetIriFromResourceClass() { $this->expectException(InvalidArgumentException::class); @@ -154,7 +171,7 @@ public function testNotAbleToGenerateGetIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::COLLECTION)->willReturn('dummies'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('dummies', [], UrlGeneratorInterface::ABS_PATH)->willThrow(new RouteNotFoundException()); + $routerProphecy->generate('dummies', [], null)->willThrow(new RouteNotFoundException()); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $converter->getIriFromResourceClass(Dummy::class); @@ -166,7 +183,7 @@ public function testGetSubresourceIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::SUBRESOURCE, Argument::type('array'))->willReturn('api_dummies_related_dummies_get_subresource'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('api_dummies_related_dummies_get_subresource', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willReturn('/dummies/1/related_dummies'); + $routerProphecy->generate('api_dummies_related_dummies_get_subresource', ['id' => 1], null)->willReturn('/dummies/1/related_dummies'); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $this->assertEquals($converter->getSubresourceIriFromResourceClass(Dummy::class, ['subresource_identifiers' => ['id' => 1], 'subresource_resources' => [RelatedDummy::class => 1]]), '/dummies/1/related_dummies'); @@ -181,7 +198,7 @@ public function testNotAbleToGenerateGetSubresourceIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::SUBRESOURCE, Argument::type('array'))->willReturn('dummies'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('dummies', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willThrow(new RouteNotFoundException()); + $routerProphecy->generate('dummies', ['id' => 1], null)->willThrow(new RouteNotFoundException()); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $converter->getSubresourceIriFromResourceClass(Dummy::class, ['subresource_identifiers' => ['id' => 1], 'subresource_resources' => [RelatedDummy::class => 1]]); @@ -193,12 +210,27 @@ public function testGetItemIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::ITEM)->willReturn('api_dummies_get_item'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('api_dummies_get_item', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willReturn('/dummies/1'); + $routerProphecy->generate('api_dummies_get_item', ['id' => 1], null)->willReturn('/dummies/1'); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $this->assertEquals($converter->getItemIriFromResourceClass(Dummy::class, ['id' => 1]), '/dummies/1'); } + public function testGetItemIriFromResourceClassAbsoluteUrl() + { + $routeNameResolverProphecy = $this->prophesize(RouteNameResolverInterface::class); + $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::ITEM)->willReturn('api_dummies_get_item'); + + $routerProphecy = $this->prophesize(RouterInterface::class); + $routerProphecy->generate('api_dummies_get_item', ['id' => 1], UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/1'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('', '', '', [], [], ['url_generation_strategy' => UrlGeneratorInterface::ABS_URL])); + + $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy, null, null, null, $resourceMetadataFactoryProphecy->reveal()); + $this->assertEquals($converter->getItemIriFromResourceClass(Dummy::class, ['id' => 1]), 'http://example.com/dummies/1'); + } + public function testNotAbleToGenerateGetItemIriFromResourceClass() { $this->expectException(InvalidArgumentException::class); @@ -208,7 +240,7 @@ public function testNotAbleToGenerateGetItemIriFromResourceClass() $routeNameResolverProphecy->getRouteName(Dummy::class, OperationType::ITEM)->willReturn('dummies'); $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('dummies', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willThrow(new RouteNotFoundException()); + $routerProphecy->generate('dummies', ['id' => 1], null)->willThrow(new RouteNotFoundException()); $converter = $this->getIriConverter($routerProphecy, $routeNameResolverProphecy); $converter->getItemIriFromResourceClass(Dummy::class, ['id' => 1]); @@ -339,7 +371,7 @@ private function getResourceClassResolver() return $resourceClassResolver->reveal(); } - private function getIriConverter($routerProphecy = null, $routeNameResolverProphecy = null, $itemDataProviderProphecy = null, $subresourceDataProviderProphecy = null, $identifierConverterProphecy = null) + private function getIriConverter($routerProphecy = null, $routeNameResolverProphecy = null, $itemDataProviderProphecy = null, $subresourceDataProviderProphecy = null, $identifierConverterProphecy = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -366,7 +398,9 @@ private function getIriConverter($routerProphecy = null, $routeNameResolverProph null, new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, null, $this->getResourceClassResolver()), $subresourceDataProviderProphecy ? $subresourceDataProviderProphecy->reveal() : null, - $identifierConverterProphecy ? $identifierConverterProphecy->reveal() : null + $identifierConverterProphecy ? $identifierConverterProphecy->reveal() : null, + null, + $resourceMetadataFactory ); } } diff --git a/tests/Bridge/Symfony/Routing/RouterTest.php b/tests/Bridge/Symfony/Routing/RouterTest.php index 9f498eab771..3c6721c9e3f 100644 --- a/tests/Bridge/Symfony/Routing/RouterTest.php +++ b/tests/Bridge/Symfony/Routing/RouterTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\Routing; +use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\Router; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -59,6 +60,24 @@ public function testGenerate() $this->assertSame('/bar', $router->generate('foo')); } + public function testGenerateWithDefaultStrategy() + { + $mockedRouter = $this->prophesize(RouterInterface::class); + $mockedRouter->generate('foo', [], UrlGeneratorInterface::ABS_URL)->willReturn('/bar')->shouldBeCalled(); + + $router = new Router($mockedRouter->reveal(), UrlGeneratorInterface::ABS_URL); + $this->assertSame('/bar', $router->generate('foo')); + } + + public function testGenerateWithStrategy() + { + $mockedRouter = $this->prophesize(RouterInterface::class); + $mockedRouter->generate('foo', [], UrlGeneratorInterface::ABS_URL)->willReturn('/bar')->shouldBeCalled(); + + $router = new Router($mockedRouter->reveal()); + $this->assertSame('/bar', $router->generate('foo', [], UrlGeneratorInterface::ABS_URL)); + } + public function testMatch() { $context = new RequestContext('/app_dev.php', 'GET', 'localhost', 'https'); diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php new file mode 100644 index 00000000000..6997f3ab80a --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -0,0 +1,40 @@ + + * + * 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\Api\UrlGeneratorInterface; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::ABS_URL) + * @ODM\Document + */ +class AbsoluteUrlDummy +{ + /** + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @ODM\ReferenceOne(targetDocument=AbsoluteUrlRelationDummy::class, inversedBy="absoluteUrlDummies", storeAs="id") + */ + public $absoluteUrlRelationDummy; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php new file mode 100644 index 00000000000..968c628c227 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php @@ -0,0 +1,48 @@ + + * + * 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\Annotation\ApiSubresource; +use ApiPlatform\Core\Api\UrlGeneratorInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::ABS_URL) + * @ODM\Document + */ +class AbsoluteUrlRelationDummy +{ + /** + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @ODM\ReferenceMany(targetDocument=AbsoluteUrlDummy::class, mappedBy="absoluteUrlRelationDummy") + * @ApiSubresource + */ + public $absoluteUrlDummies; + + public function __construct() + { + $this->absoluteUrlDummies = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php new file mode 100644 index 00000000000..1026bd41402 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -0,0 +1,40 @@ + + * + * 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\Api\UrlGeneratorInterface; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::NET_PATH) + * @ODM\Document + */ +class NetworkPathDummy +{ + /** + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @ODM\ReferenceOne(targetDocument=NetworkPathRelationDummy::class, inversedBy="networkPathDummies", storeAs="id") + */ + public $networkPathRelationDummy; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php new file mode 100644 index 00000000000..05331574484 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php @@ -0,0 +1,48 @@ + + * + * 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\Annotation\ApiSubresource; +use ApiPlatform\Core\Api\UrlGeneratorInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::NET_PATH) + * @ODM\Document + */ +class NetworkPathRelationDummy +{ + /** + * @ODM\Id(strategy="INCREMENT", type="integer") + */ + private $id; + + /** + * @ODM\ReferenceMany(targetDocument=NetworkPathDummy::class, mappedBy="networkPathRelationDummy") + * @ApiSubresource + */ + public $networkPathDummies; + + public function __construct() + { + $this->networkPathDummies = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php new file mode 100644 index 00000000000..cff27c3ac83 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -0,0 +1,42 @@ + + * + * 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\Api\UrlGeneratorInterface; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::ABS_URL) + * @ORM\Entity + */ +class AbsoluteUrlDummy +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\ManyToOne(targetEntity="AbsoluteUrlRelationDummy", inversedBy="absoluteUrlDummies") + */ + public $absoluteUrlRelationDummy; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php new file mode 100644 index 00000000000..34090ee2bc9 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php @@ -0,0 +1,50 @@ + + * + * 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 ApiPlatform\Core\Api\UrlGeneratorInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::ABS_URL) + * @ORM\Entity + */ +class AbsoluteUrlRelationDummy +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\OneToMany(targetEntity="AbsoluteUrlDummy", mappedBy="absoluteUrlRelationDummy") + * @ApiSubresource + */ + public $absoluteUrlDummies; + + public function __construct() + { + $this->absoluteUrlDummies = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php new file mode 100644 index 00000000000..29f7e8ea043 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -0,0 +1,42 @@ + + * + * 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\Api\UrlGeneratorInterface; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::NET_PATH) + * @ORM\Entity + */ +class NetworkPathDummy +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\ManyToOne(targetEntity="NetworkPathRelationDummy", inversedBy="networkPathDummies") + */ + public $networkPathRelationDummy; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php new file mode 100644 index 00000000000..cfd35391c6d --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php @@ -0,0 +1,50 @@ + + * + * 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 ApiPlatform\Core\Api\UrlGeneratorInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource(urlGenerationStrategy=UrlGeneratorInterface::NET_PATH) + * @ORM\Entity + */ +class NetworkPathRelationDummy +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\OneToMany(targetEntity="NetworkPathDummy", mappedBy="networkPathRelationDummy") + * @ApiSubresource + */ + public $networkPathDummies; + + public function __construct() + { + $this->networkPathDummies = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Hal/Serializer/CollectionNormalizerTest.php b/tests/Hal/Serializer/CollectionNormalizerTest.php index 9c63cd8f7f1..c00ede98312 100644 --- a/tests/Hal/Serializer/CollectionNormalizerTest.php +++ b/tests/Hal/Serializer/CollectionNormalizerTest.php @@ -17,6 +17,8 @@ use ApiPlatform\Core\DataProvider\PaginatorInterface; use ApiPlatform\Core\DataProvider\PartialPaginatorInterface; use ApiPlatform\Core\Hal\Serializer\CollectionNormalizer; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -28,7 +30,8 @@ class CollectionNormalizerTest extends TestCase public function testSupportsNormalize() { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT)); $this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT)); @@ -41,11 +44,12 @@ public function testNormalizeApiSubLevel() { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass()->shouldNotBeCalled(); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('bar', null, ['api_sub_level' => true])->willReturn(22); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $this->assertEquals(['foo' => 22], $normalizer->normalize(['foo' => 'bar'], null, ['api_sub_level' => true])); @@ -134,13 +138,16 @@ private function normalizePaginator($partial = false) $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($paginatorProphecy, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn(['_links' => ['self' => '/me'], 'name' => 'Kévin']); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); return $normalizer->normalize($paginatorProphecy->reveal(), CollectionNormalizer::FORMAT, [ diff --git a/tests/JsonApi/Serializer/CollectionNormalizerTest.php b/tests/JsonApi/Serializer/CollectionNormalizerTest.php index 7096379e25d..da7248ec468 100644 --- a/tests/JsonApi/Serializer/CollectionNormalizerTest.php +++ b/tests/JsonApi/Serializer/CollectionNormalizerTest.php @@ -17,6 +17,8 @@ use ApiPlatform\Core\DataProvider\PaginatorInterface; use ApiPlatform\Core\DataProvider\PartialPaginatorInterface; use ApiPlatform\Core\JsonApi\Serializer\CollectionNormalizer; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -29,8 +31,9 @@ class CollectionNormalizerTest extends TestCase public function testSupportsNormalize() { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT)); $this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT)); @@ -56,9 +59,13 @@ public function testNormalizePaginator() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($paginator, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn([ @@ -72,7 +79,7 @@ public function testNormalizePaginator() ], ]); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $expected = [ @@ -102,6 +109,7 @@ public function testNormalizePaginator() $this->assertEquals($expected, $normalizer->normalize($paginator, CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', 'resource_class' => 'Foo', ])); } @@ -122,9 +130,13 @@ public function testNormalizePartialPaginator() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($paginator, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn([ @@ -138,7 +150,7 @@ public function testNormalizePartialPaginator() ], ]); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $expected = [ @@ -165,6 +177,7 @@ public function testNormalizePartialPaginator() $this->assertEquals($expected, $normalizer->normalize($paginator, CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', 'resource_class' => 'Foo', ])); } @@ -175,10 +188,12 @@ public function testNormalizeArray() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); - + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn([ @@ -192,7 +207,7 @@ public function testNormalizeArray() ], ]); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $expected = [ @@ -212,6 +227,7 @@ public function testNormalizeArray() $this->assertEquals($expected, $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'resource_class' => 'Foo', ])); } @@ -223,9 +239,13 @@ public function testNormalizeIncludedData() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn([ @@ -249,7 +269,7 @@ public function testNormalizeIncludedData() ], ]); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $expected = [ @@ -279,6 +299,7 @@ public function testNormalizeIncludedData() $this->assertEquals($expected, $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'resource_class' => 'Foo', ])); } @@ -293,18 +314,23 @@ public function testNormalizeWithoutDataKey() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata()); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'api_sub_level' => true, 'resource_class' => 'Foo', ])->willReturn([]); - $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page'); + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); $normalizer->setNormalizer($itemNormalizer->reveal()); $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', 'resource_class' => 'Foo', ]); } diff --git a/tests/Util/IriHelperTest.php b/tests/Util/IriHelperTest.php index 8e6814f2404..83e61df47c1 100644 --- a/tests/Util/IriHelperTest.php +++ b/tests/Util/IriHelperTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Util; +use ApiPlatform\Core\Api\UrlGeneratorInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Util\IriHelper; use PHPUnit\Framework\TestCase; @@ -39,7 +40,32 @@ public function testHelpers() $this->assertEquals('/hello.json?foo=bar&bar=3&page=2', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2.)); } - public function testHelpersWithAbsoluteUrl() + /** + * @group legacy + * @expectedDeprecation Passing a bool as 5th parameter to "ApiPlatform\Core\Util\IriHelper::createIri()" is deprecated since API Platform 2.6. Pass an "ApiPlatform\Core\Api\UrlGeneratorInterface" constant (int) instead. + */ + public function testLegacyHelpers() + { + $parsed = [ + 'parts' => [ + 'path' => '/hello.json', + 'query' => 'foo=bar&page=2&bar=3', + ], + 'parameters' => [ + 'foo' => 'bar', + 'bar' => '3', + ], + ]; + + $this->assertEquals($parsed, IriHelper::parseIri('/hello.json?foo=bar&page=2&bar=3', 'page')); + $this->assertEquals('/hello.json?foo=bar&bar=3&page=2', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., false)); + } + + /** + * @group legacy + * @expectedDeprecation Passing a bool as 5th parameter to "ApiPlatform\Core\Util\IriHelper::createIri()" is deprecated since API Platform 2.6. Pass an "ApiPlatform\Core\Api\UrlGeneratorInterface" constant (int) instead. + */ + public function testLegacyHelpersWithAbsoluteUrl() { $parsed = [ 'parts' => [ @@ -70,6 +96,36 @@ public function testHelpersWithAbsoluteUrl() $this->assertEquals('https://foo:bar@localhost:443/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., true)); } + public function testHelpersWithNetworkPath() + { + $parsed = [ + 'parts' => [ + 'path' => '/hello.json', + 'query' => 'foo=bar&page=2&bar=3', + 'scheme' => 'http', + 'user' => 'foo', + 'pass' => 'bar', + 'host' => 'localhost', + 'port' => 8080, + 'fragment' => 'foo', + ], + 'parameters' => [ + 'foo' => 'bar', + 'bar' => '3', + ], + ]; + + $this->assertEquals('//foo:bar@localhost:8080/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH)); + + unset($parsed['parts']['scheme']); + + $this->assertEquals('//foo:bar@localhost:8080/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH)); + + $parsed['parts']['port'] = 443; + + $this->assertEquals('//foo:bar@localhost:443/hello.json?foo=bar&bar=3&page=2#foo', IriHelper::createIri($parsed['parts'], $parsed['parameters'], 'page', 2., UrlGeneratorInterface::NET_PATH)); + } + public function testParseIriWithInvalidUrl() { $this->expectException(InvalidArgumentException::class); From 10001072ee75e9c70189bd9bfdc0f2233bc52569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 24 Jun 2020 16:31:00 +0200 Subject: [PATCH 2/2] fix: rename function --- features/bootstrap/DoctrineContext.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 8bcbb41aa5c..a8610a3a500 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -1536,14 +1536,13 @@ public function thereAreConvertedOwnerObjects(int $nb) } /** -<<<<<<< HEAD * @Given there are :nb dummy mercure objects */ public function thereAreDummyMercureObjects(int $nb) { for ($i = 1; $i <= $nb; ++$i) { $relatedDummy = $this->buildRelatedDummy(); - $relatedDummy->setName('RelatedDummy #'.$i); + $relatedDummy->setName('RelatedDummy #' . $i); $dummyMercure = $this->buildDummyMercure(); $dummyMercure->name = "Dummy Mercure #$i"; @@ -1552,7 +1551,12 @@ public function thereAreDummyMercureObjects(int $nb) $this->manager->persist($relatedDummy); $this->manager->persist($dummyMercure); -======= + } + + $this->manager->flush(); + } + + /** * @Given there are :nb absoluteUrlDummy objects with a related absoluteUrlRelationDummy */ public function thereAreAbsoluteUrlDummies(int $nb) @@ -1572,7 +1576,7 @@ public function thereAreAbsoluteUrlDummies(int $nb) /** * @Given there are :nb networkPathDummy objects with a related networkPathRelationDummy */ - public function thereArenetworkPathDummies(int $nb) + public function thereAreNetworkPathDummies(int $nb) { for ($i = 1; $i <= $nb; ++$i) { $networkPathRelationDummy = $this->buildNetworkPathRelationDummy(); @@ -1581,7 +1585,6 @@ public function thereArenetworkPathDummies(int $nb) $this->manager->persist($networkPathRelationDummy); $this->manager->persist($networkPathDummy); ->>>>>>> 880597f6... add ability to return absolute urls instead of relative IRIs } $this->manager->flush(); @@ -1966,13 +1969,15 @@ private function buildConvertedRelated() } /** -<<<<<<< HEAD * @return DummyMercure|DummyMercureDocument */ private function buildDummyMercure() { return $this->isOrm() ? new DummyMercure() : new DummyMercureDocument(); -======= + + } + + /** * @return AbsoluteUrlDummyDocument|AbsoluteUrlDummy */ private function buildAbsoluteUrlDummy() @@ -2002,6 +2007,5 @@ private function buildNetworkPathDummy() private function buildNetworkPathRelationDummy() { return $this->isOrm() ? new NetworkPathRelationDummy() : new NetworkPathRelationDummyDocument(); ->>>>>>> 880597f6... add ability to return absolute urls instead of relative IRIs } }