diff --git a/features/main/crud.feature b/features/main/crud.feature
index d38d1870f25..c9ae98e444e 100644
--- a/features/main/crud.feature
+++ b/features/main/crud.feature
@@ -22,6 +22,8 @@ Feature: Create-Retrieve-Update-Delete
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/dummies/1"
+ And the header "Location" should be equal to "/dummies/1"
And the JSON should be equal to:
"""
{
@@ -420,6 +422,7 @@ Feature: Create-Retrieve-Update-Delete
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/dummies/1"
And the JSON should be equal to:
"""
{
@@ -457,6 +460,7 @@ Feature: Create-Retrieve-Update-Delete
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/dummies/1"
And the JSON should be equal to:
"""
{
diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature
index f0c3d66b3d9..c7c15a31821 100644
--- a/features/main/crud_abstract.feature
+++ b/features/main/crud_abstract.feature
@@ -16,6 +16,8 @@ Feature: Create-Retrieve-Update-Delete on abstract resource
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/concrete_dummies/1"
+ And the header "Location" should be equal to "/concrete_dummies/1"
And the JSON should be equal to:
"""
{
@@ -90,6 +92,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/concrete_dummies/1"
And the JSON should be equal to:
"""
{
diff --git a/features/main/custom_normalized.feature b/features/main/custom_normalized.feature
index 852fe7d2ccc..f9d20c12176 100644
--- a/features/main/custom_normalized.feature
+++ b/features/main/custom_normalized.feature
@@ -16,6 +16,8 @@ Feature: Using custom normalized entity
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/custom_normalized_dummies/1"
+ And the header "Location" should be equal to "/custom_normalized_dummies/1"
And the JSON should be equal to:
"""
{
@@ -40,6 +42,8 @@ Feature: Using custom normalized entity
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/related_normalized_dummies/1"
+ And the header "Location" should be equal to "/related_normalized_dummies/1"
And the JSON should be equal to:
"""
{
@@ -68,6 +72,7 @@ Feature: Using custom normalized entity
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/related_normalized_dummies/1"
And the JSON should be equal to:
"""
{
@@ -134,6 +139,7 @@ Feature: Using custom normalized entity
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/custom_normalized_dummies/1"
And the JSON should be equal to:
"""
{
diff --git a/features/main/custom_writable_identifier.feature b/features/main/custom_writable_identifier.feature
index 98e005c95ed..9e141a563db 100644
--- a/features/main/custom_writable_identifier.feature
+++ b/features/main/custom_writable_identifier.feature
@@ -16,6 +16,8 @@ Feature: Using custom writable identifier on resource
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug"
+ And the header "Location" should be equal to "/custom_writable_identifier_dummies/my_slug"
And the JSON should be equal to:
"""
{
@@ -78,6 +80,7 @@ Feature: Using custom writable identifier on resource
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified"
And the JSON should be equal to:
"""
{
diff --git a/features/main/uuid.feature b/features/main/uuid.feature
index d444dad8900..2a599e49cd8 100644
--- a/features/main/uuid.feature
+++ b/features/main/uuid.feature
@@ -16,6 +16,8 @@ Feature: Using uuid identifier on resource
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
+ And the header "Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
Scenario: Get a resource
When I send a "GET" request to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
@@ -67,6 +69,7 @@ Feature: Using uuid identifier on resource
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
And the JSON should be equal to:
"""
{
@@ -87,6 +90,8 @@ Feature: Using uuid identifier on resource
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo"
+ And the header "Location" should be equal to "/custom_generated_identifiers/foo"
And the JSON should be equal to:
"""
{
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index 69fab2d6f40..1a2ec454d35 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -152,6 +152,7 @@
+
diff --git a/src/EventListener/RespondListener.php b/src/EventListener/RespondListener.php
index 99c9e350d6f..280a330cffe 100644
--- a/src/EventListener/RespondListener.php
+++ b/src/EventListener/RespondListener.php
@@ -42,15 +42,29 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
return;
}
+ $headers = [
+ 'Content-Type' => sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())),
+ 'Vary' => 'Accept',
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Frame-Options' => 'deny',
+ ];
+
+ if (\in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'])) {
+ if ($request->attributes->has('_api_write_item_iri')) {
+ $headers['Content-Location'] = $request->attributes->get('_api_write_item_iri');
+
+ if ($request->isMethod('POST')) {
+ $headers['Location'] = $request->attributes->get('_api_write_item_iri');
+ }
+ } else {
+ @trigger_error(sprintf('No request attribute from `_api_write_item_iri` key is deprecated since API Platform 2.3 and will not be supported in API Platform 3, an string should always be returned. see deprecated into %s constructor for more details.', WriteListener::class), E_USER_DEPRECATED);
+ }
+ }
+
$event->setResponse(new Response(
$controllerResult,
self::METHOD_TO_CODE[$request->getMethod()] ?? Response::HTTP_OK,
- [
- 'Content-Type' => sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())),
- 'Vary' => 'Accept',
- 'X-Content-Type-Options' => 'nosniff',
- 'X-Frame-Options' => 'deny',
- ]
+ $headers
));
}
}
diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php
index c2aec1c922e..5ab4fab1bd7 100644
--- a/src/EventListener/WriteListener.php
+++ b/src/EventListener/WriteListener.php
@@ -13,22 +13,30 @@
namespace ApiPlatform\Core\EventListener;
+use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
/**
* Bridges persistense and the API system.
*
+ * @final
* @author Kévin Dunglas
* @author Baptiste Meyer
*/
class WriteListener
{
private $dataPersister;
+ private $iriConverter;
- public function __construct(DataPersisterInterface $dataPersister)
+ public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null)
{
$this->dataPersister = $dataPersister;
+ $this->iriConverter = $iriConverter;
+
+ if (null === $iriConverter) {
+ @trigger_error(sprintf('Class %s will have a second `IriConverterInterface $iriConverter` argument in version 3.0. Not defining it is deprecated since 2.3.', __CLASS__), E_USER_DEPRECATED);
+ }
}
/**
@@ -57,6 +65,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
}
$event->setControllerResult($persistResult ?? $controllerResult);
+
+ if (null !== $this->iriConverter) {
+ $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
+ }
break;
case 'DELETE':
$this->dataPersister->remove($controllerResult);
diff --git a/tests/EventListener/RespondListenerTest.php b/tests/EventListener/RespondListenerTest.php
index 464fd856215..feae684cfb1 100644
--- a/tests/EventListener/RespondListenerTest.php
+++ b/tests/EventListener/RespondListenerTest.php
@@ -67,7 +67,7 @@ public function testCreate201Response()
{
$kernelProphecy = $this->prophesize(HttpKernelInterface::class);
- $request = new Request([], [], ['_api_respond' => true]);
+ $request = new Request([], [], ['_api_respond' => true, '_api_write_item_iri' => '/dummy_entities/1']);
$request->setMethod('POST');
$request->setRequestFormat('xml');
@@ -88,6 +88,8 @@ public function testCreate201Response()
$this->assertEquals('Accept', $response->headers->get('Vary'));
$this->assertEquals('nosniff', $response->headers->get('X-Content-Type-Options'));
$this->assertEquals('deny', $response->headers->get('X-Frame-Options'));
+ $this->assertEquals('/dummy_entities/1', $response->headers->get('Location'));
+ $this->assertEquals('/dummy_entities/1', $response->headers->get('Content-Location'));
}
public function testCreate204Response()
diff --git a/tests/EventListener/WriteListenerTest.php b/tests/EventListener/WriteListenerTest.php
index ce705dcd3a5..dcb16cc80df 100644
--- a/tests/EventListener/WriteListenerTest.php
+++ b/tests/EventListener/WriteListenerTest.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\Tests\EventListener;
+use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use ApiPlatform\Core\EventListener\WriteListener;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
@@ -35,6 +36,9 @@ public function testOnKernelViewWithControllerResultAndPersist()
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
$dataPersisterProphecy->persist($dummy)->willReturn($dummy)->shouldBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummy/1')->shouldBeCalled();
+
$request = new Request();
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -48,8 +52,9 @@ public function testOnKernelViewWithControllerResultAndPersist()
foreach (['PATCH', 'PUT', 'POST'] as $httpMethod) {
$request->setMethod($httpMethod);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
$this->assertSame($dummy, $event->getControllerResult());
+ $this->assertEquals('/dummy/1', $request->attributes->get('_api_write_item_iri'));
}
}
@@ -98,6 +103,9 @@ public function testOnKernelViewWithControllerResultAndPersistWithImmutableResou
$dataPersisterProphecy = $this->prophesize(DataPersisterInterface::class);
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummy/1')->shouldBeCalled();
+
$dataPersisterProphecy
->persist($dummy)
->willReturn($dummy2) // Persist is not mutating $dummy, but return a brand new technically unrelated object instead
@@ -117,9 +125,10 @@ public function testOnKernelViewWithControllerResultAndPersistWithImmutableResou
$request->setMethod($httpMethod);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
$this->assertSame($dummy2, $event->getControllerResult());
+ $this->assertEquals('/dummy/1', $request->attributes->get('_api_write_item_iri'));
}
}
@@ -132,6 +141,9 @@ public function testOnKernelViewWithControllerResultAndRemove()
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
$dataPersisterProphecy->remove($dummy)->shouldBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
+
$request = new Request();
$request->setMethod('DELETE');
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -143,7 +155,7 @@ public function testOnKernelViewWithControllerResultAndRemove()
$dummy
);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
}
public function testOnKernelViewWithSafeMethod()
@@ -156,6 +168,9 @@ public function testOnKernelViewWithSafeMethod()
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
+
$request = new Request();
$request->setMethod('HEAD');
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -167,7 +182,7 @@ public function testOnKernelViewWithSafeMethod()
$dummy
);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
}
public function testOnKernelViewWithNoResourceClass()
@@ -180,6 +195,9 @@ public function testOnKernelViewWithNoResourceClass()
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
+
$request = new Request();
$request->setMethod('POST');
@@ -190,7 +208,7 @@ public function testOnKernelViewWithNoResourceClass()
$dummy
);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
}
public function testOnKernelViewWithNoDataPersisterSupport()
@@ -203,6 +221,9 @@ public function testOnKernelViewWithNoDataPersisterSupport()
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
+ $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
+ $iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
+
$request = new Request();
$request->setMethod('POST');
$request->attributes->set('_api_resource_class', 'Dummy');
@@ -214,6 +235,6 @@ public function testOnKernelViewWithNoDataPersisterSupport()
$dummy
);
- (new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
+ (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
}
}