Skip to content

Commit 7cfc489

Browse files
mab05kantograssiot
authored andcommitted
add ability to return absolute urls instead of relative IRIs
1 parent 906aaa2 commit 7cfc489

File tree

19 files changed

+418
-12
lines changed

19 files changed

+418
-12
lines changed

.circleci/config.yml

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ jobs:
353353
name: Run Behat tests
354354
command: |
355355
mkdir -p build/logs/behat build/coverage
356-
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' -not -path 'features/mongodb/*' | circleci tests split --split-by=timings); do
356+
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' -not -path 'features/mongodb/*' -not -path 'features/absolute_url/*' | circleci tests split --split-by=timings); do
357357
_f=$(echo "$f" | tr / _)
358358
FEATURE="${_f}" vendor/bin/behat --format=progress --out=std --format=junit --out=build/logs/behat/"${_f}" --profile=default-coverage --no-interaction --colors "$f"
359359
done
@@ -508,6 +508,48 @@ jobs:
508508
file: build/logs/clover.xml
509509
- save-npm-cache
510510

511+
behat-absolute-url-coverage:
512+
executor: php
513+
environment:
514+
APP_ENV: absolute_url
515+
working_directory: ~/api-platform/core
516+
steps:
517+
- checkout
518+
- install-mongodb-php-extension
519+
- install-pcov-php-extension
520+
- disable-xdebug-php-extension
521+
- disable-php-memory-limit
522+
- restore-composer-cache
523+
- update-project-dependencies
524+
- save-composer-cache
525+
- clear-test-app-cache
526+
- run:
527+
name: Run Behat tests
528+
command: |
529+
mkdir -p build/logs/behat
530+
vendor/bin/behat --format=progress --out=std --format=junit --out=build/logs/behat --profile=absoluteUrl --no-interaction --colors
531+
- restore-npm-cache
532+
- merge-test-reports:
533+
dir: build/logs/behat
534+
out: build/logs/behat/junit.xml
535+
- store_test_results:
536+
path: build/logs
537+
- store_artifacts:
538+
path: build/logs/behat/junit.xml
539+
destination: build/logs/behat/junit.xml
540+
- merge-code-coverage-reports:
541+
dir: build/coverage
542+
out: build/logs/clover.xml
543+
- store_artifacts:
544+
path: build/logs/clover.xml
545+
destination: build/logs/clover.xml
546+
- codecov/upload:
547+
file: build/logs/clover.xml
548+
flags: behat_absolute_url
549+
- coveralls/upload:
550+
file: build/logs/clover.xml
551+
- save-npm-cache
552+
511553
workflows:
512554
version: 2
513555
lint:
@@ -521,3 +563,4 @@ workflows:
521563
- phpunit-mongodb-coverage
522564
- behat-mongodb-coverage
523565
- behat-elasticsearch-coverage
566+
- behat-absolute-url-coverage

.travis.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,24 @@ jobs:
244244
- *validate-openapi-v3-json
245245
- *validate-openapi-v3-yaml
246246

247+
- php: '7.3'
248+
env: APP_ENV=absolute_url
249+
before_install:
250+
- *enable-mongodb-php-extension
251+
- *disable-xdebug-php-extension
252+
- *disable-php-memory-limit
253+
- *add-composer-bin-dir-to-path
254+
install:
255+
- *update-project-dependencies
256+
before_script:
257+
- *clear-test-app-cache
258+
script:
259+
- *run-phpunit-tests
260+
- *clear-test-app-cache
261+
- vendor/bin/behat --format=progress --profile=absoluteUrl --no-interaction
262+
- *validate-openapi-v2-json
263+
- *validate-openapi-v2-yaml
264+
- *validate-openapi-v3-json
265+
- *validate-openapi-v3-yaml
266+
247267
fast_finish: true

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ Coverage will be available in `coverage/index.html`.
8484
The command to launch Behat tests is:
8585

8686
```shell
87-
./vendor/bin/behat --suite=default --stop-on-failure -vvv
87+
./vendor/bin/behat --profile=default --stop-on-failure -vvv
8888
```
8989

9090
If you want to launch Behat tests for MongoDB, the command is:
9191

9292
```shell
93-
APP_ENV=mongodb ./vendor/bin/behat --suite=mongodb --stop-on-failure -vvv
93+
APP_ENV=mongodb ./vendor/bin/behat --profile=mongodb --stop-on-failure -vvv
9494
```
9595

9696
## Squash your Commits

behat.yml.dist

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ default:
2020
- 'Behat\MinkExtension\Context\MinkContext'
2121
- 'Behatch\Context\RestContext'
2222
filters:
23-
tags: '~@postgres&&~@mongodb&&~@elasticsearch'
23+
tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@absoluteUrl'
2424
extensions:
2525
'Behat\Symfony2Extension':
2626
kernel:
@@ -65,7 +65,7 @@ postgres:
6565
- 'Behat\MinkExtension\Context\MinkContext'
6666
- 'Behatch\Context\RestContext'
6767
filters:
68-
tags: '~@sqlite&&~@mongodb&&~@elasticsearch'
68+
tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@absoluteUrl'
6969

7070
postgres-no-legacy:
7171
suites:
@@ -98,7 +98,7 @@ mongodb:
9898
- 'Behat\MinkExtension\Context\MinkContext'
9999
- 'Behatch\Context\RestContext'
100100
filters:
101-
tags: '~@sqlite&&~@elasticsearch&&~@!mongodb'
101+
tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@absoluteUrl'
102102

103103
mongodb-no-legacy:
104104
suites:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
@absoluteUrl
2+
Feature: IRI should contain Absolute URL
3+
In order to add detail to IRIs
4+
Include the absolute url
5+
6+
@createSchema
7+
Scenario: I should be able to GET a collection of Objects with Absolute Urls
8+
Given there are 1 absoluteUrlDummy objects with a related absoluteUrlRelationDummy
9+
And I add "Accept" header equal to "application/ld+json"
10+
And I add "Content-Type" header equal to "application/json"
11+
And I send a "GET" request to "/absolute_url_dummies"
12+
And the JSON should be equal to:
13+
"""
14+
{
15+
"@context": "\/contexts\/AbsoluteUrlDummy",
16+
"@id": "http:\/\/example.com\/absolute_url_dummies",
17+
"@type": "hydra:Collection",
18+
"hydra:member": [
19+
{
20+
"@id": "http:\/\/example.com\/absolute_url_dummies\/1",
21+
"@type": "AbsoluteUrlDummy",
22+
"absoluteUrlRelationDummy": "http:\/\/example.com\/absolute_url_relation_dummies\/1",
23+
"id": 1
24+
}
25+
],
26+
"hydra:totalItems": 1
27+
}
28+
29+
"""
30+
31+
Scenario: I should be able to POST an object using an Absolute Url
32+
Given I add "Accept" header equal to "application/ld+json"
33+
And I add "Content-Type" header equal to "application/json"
34+
And I send a "POST" request to "/absolute_url_relation_dummies" with body:
35+
"""
36+
{
37+
"absolute_url_dummies": "http://example.com/absolute_url_dummies/1"
38+
}
39+
"""
40+
Then the response status code should be 201
41+
And the JSON should be equal to:
42+
"""
43+
{
44+
"@context": "\/contexts\/AbsoluteUrlRelationDummy",
45+
"@id": "http:\/\/example.com\/absolute_url_relation_dummies\/2",
46+
"@type": "AbsoluteUrlRelationDummy",
47+
"absoluteUrlDummies": [],
48+
"id": 2
49+
}
50+
"""
51+
52+
Scenario: I should be able to GET an Item with Absolute Urls
53+
Given I add "Accept" header equal to "application/ld+json"
54+
And I add "Content-Type" header equal to "application/json"
55+
And I send a "GET" request to "/absolute_url_dummies/1"
56+
And print last JSON response
57+
And the JSON should be equal to:
58+
"""
59+
{
60+
"@context": "\/contexts\/AbsoluteUrlDummy",
61+
"@id": "http:\/\/example.com\/absolute_url_dummies\/1",
62+
"@type": "AbsoluteUrlDummy",
63+
"absoluteUrlRelationDummy": "http:\/\/example.com\/absolute_url_relation_dummies\/1",
64+
"id": 1
65+
}
66+
"""
67+
68+
Scenario: I should be able to GET subresources with Absolute Urls
69+
Given I add "Accept" header equal to "application/ld+json"
70+
And I add "Content-Type" header equal to "application/json"
71+
And I send a "GET" request to "/absolute_url_relation_dummies/1/absolute_url_dummies"
72+
And print last JSON response
73+
And the JSON should be equal to:
74+
"""
75+
{
76+
"@context": "\/contexts\/AbsoluteUrlDummy",
77+
"@id": "http:\/\/example.com\/absolute_url_relation_dummies\/1\/absolute_url_dummies",
78+
"@type": "hydra:Collection",
79+
"hydra:member": [
80+
{
81+
"@id": "http:\/\/example.com\/absolute_url_dummies\/1",
82+
"@type": "AbsoluteUrlDummy",
83+
"absoluteUrlRelationDummy": "http:\/\/example.com\/absolute_url_relation_dummies\/1",
84+
"id": 1
85+
}
86+
],
87+
"hydra:totalItems": 1
88+
}
89+
"""

features/bootstrap/DoctrineContext.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
declare(strict_types=1);
1313

14+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlDummy as AbsoluteUrlDummyDocument;
15+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument;
1416
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Address as AddressDocument;
1517
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Answer as AnswerDocument;
1618
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeItem as CompositeItemDocument;
@@ -65,6 +67,8 @@
6567
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Taxon as TaxonDocument;
6668
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument;
6769
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\User as UserDocument;
70+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlDummy;
71+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy;
6872
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address;
6973
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer;
7074
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeItem;
@@ -1502,6 +1506,23 @@ public function thereAreConvertedOwnerObjects(int $nb)
15021506
$this->manager->flush();
15031507
}
15041508

1509+
/**
1510+
* @Given there are :nb absoluteUrlDummy objects with a related absoluteUrlRelationDummy
1511+
*/
1512+
public function thereAreAbsoluteUrlDummies(int $nb)
1513+
{
1514+
for ($i = 1; $i <= $nb; ++$i) {
1515+
$absoluteUrlRelationDummy = $this->buildAbsoluteUrlRelationDummy();
1516+
$absoluteUrlDummy = $this->buildAbsoluteUrlDummy();
1517+
$absoluteUrlDummy->absoluteUrlRelationDummy = $absoluteUrlRelationDummy;
1518+
1519+
$this->manager->persist($absoluteUrlRelationDummy);
1520+
$this->manager->persist($absoluteUrlDummy);
1521+
}
1522+
1523+
$this->manager->flush();
1524+
}
1525+
15051526
private function isOrm(): bool
15061527
{
15071528
return null !== $this->schemaTool;
@@ -1879,4 +1900,20 @@ private function buildConvertedRelated()
18791900
{
18801901
return $this->isOrm() ? new ConvertedRelated() : new ConvertedRelatedDocument();
18811902
}
1903+
1904+
/**
1905+
* @return AbsoluteUrlDummyDocument|AbsoluteUrlDummy
1906+
*/
1907+
private function buildAbsoluteUrlDummy()
1908+
{
1909+
return $this->isOrm() ? new AbsoluteUrlDummy() : new AbsoluteUrlDummyDocument();
1910+
}
1911+
1912+
/**
1913+
* @return AbsoluteUrlRelationDummyDocument|AbsoluteUrlRelationDummy
1914+
*/
1915+
private function buildAbsoluteUrlRelationDummy()
1916+
{
1917+
return $this->isOrm() ? new AbsoluteUrlRelationDummy() : new AbsoluteUrlRelationDummyDocument();
1918+
}
18821919
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
154154
$container->setParameter('api_platform.title', $config['title']);
155155
$container->setParameter('api_platform.description', $config['description']);
156156
$container->setParameter('api_platform.version', $config['version']);
157+
$container->setParameter('api_platform.absolute_url', $config['absolute_url']);
157158
$container->setParameter('api_platform.show_webby', $config['show_webby']);
158159
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
159160
$container->setParameter('api_platform.formats', $formats);

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public function getConfigTreeBuilder()
8484
->cannotBeEmpty()
8585
->defaultValue('0.0.0')
8686
->end()
87+
->booleanNode('absolute_url')
88+
->defaultFalse()
89+
->info('If true, IRIs will include absolute url.')
90+
->end()
8791
->booleanNode('show_webby')->defaultTrue()->info('If true, show Webby on the documentation page')->end()
8892
->scalarNode('default_operation_path_resolver')
8993
->defaultValue('api_platform.operation_path_resolver.underscore')

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<argument type="service" id="api_platform.subresource_data_provider" on-invalid="ignore" />
6666
<argument type="service" id="api_platform.identifier.converter" on-invalid="ignore" />
6767
<argument type="service" id="api_platform.resource_class_resolver" />
68+
<argument>%api_platform.absolute_url%</argument>
6869
</service>
6970
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />
7071

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ final class IriConverter implements IriConverterInterface
4949
private $routeNameResolver;
5050
private $router;
5151
private $identifiersExtractor;
52+
private $absoluteUrl;
5253

53-
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)
54+
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, bool $absoluteUrl = false)
5455
{
5556
$this->itemDataProvider = $itemDataProvider;
5657
$this->routeNameResolver = $routeNameResolver;
@@ -59,6 +60,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
5960
$this->subresourceDataProvider = $subresourceDataProvider;
6061
$this->identifierConverter = $identifierConverter;
6162
$this->resourceClassResolver = $resourceClassResolver;
63+
$this->absoluteUrl = $absoluteUrl;
6264

6365
if (null === $identifiersExtractor) {
6466
@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);
@@ -137,7 +139,7 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface
137139
public function getIriFromResourceClass(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
138140
{
139141
try {
140-
return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::COLLECTION), [], $referenceType);
142+
return $this->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::COLLECTION), [], $referenceType);
141143
} catch (RoutingExceptionInterface $e) {
142144
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e);
143145
}
@@ -153,7 +155,7 @@ public function getItemIriFromResourceClass(string $resourceClass, array $identi
153155
try {
154156
$identifiers = $this->generateIdentifiersUrl($identifiers, $resourceClass);
155157

156-
return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
158+
return $this->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
157159
} catch (RoutingExceptionInterface $e) {
158160
throw new InvalidArgumentException(sprintf(
159161
'Unable to generate an IRI for "%s".',
@@ -168,12 +170,21 @@ public function getItemIriFromResourceClass(string $resourceClass, array $identi
168170
public function getSubresourceIriFromResourceClass(string $resourceClass, array $context, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
169171
{
170172
try {
171-
return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $referenceType);
173+
return $this->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $referenceType);
172174
} catch (RoutingExceptionInterface $e) {
173175
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e);
174176
}
175177
}
176178

179+
/**
180+
* @param array $parameters
181+
* @param int $referenceType
182+
*/
183+
private function generate(string $name, $parameters = [], $referenceType = UrlGeneratorInterface::ABS_PATH): string
184+
{
185+
return $this->router->generate($name, $parameters, true === $this->absoluteUrl ? UrlGeneratorInterface::ABS_URL : $referenceType);
186+
}
187+
177188
/**
178189
* Generate the identifier url.
179190
*

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class ApiPlatformExtensionTest extends TestCase
117117
'title' => 'title',
118118
'description' => 'description',
119119
'version' => 'version',
120+
'absolute_url' => false,
120121
'formats' => [
121122
'jsonld' => ['mime_types' => ['application/ld+json']],
122123
'jsonhal' => ['mime_types' => ['application/hal+json']],
@@ -836,6 +837,7 @@ private function getPartialContainerBuilderProphecy($configuration = null)
836837
],
837838
'api_platform.title' => 'title',
838839
'api_platform.version' => 'version',
840+
'api_platform.absolute_url' => false,
839841
'api_platform.show_webby' => true,
840842
'api_platform.allow_plain_identifiers' => false,
841843
'api_platform.eager_loading.enabled' => Argument::type('bool'),
@@ -1122,6 +1124,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
11221124
'api_platform.validator.serialize_payload_fields' => [],
11231125
'api_platform.elasticsearch.enabled' => false,
11241126
'api_platform.defaults' => ['attributes' => []],
1127+
'api_platform.absolute_url' => false,
11251128
];
11261129

11271130
if ($hasSwagger) {

0 commit comments

Comments
 (0)