Skip to content
40 changes: 39 additions & 1 deletion features/content_negotiation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,51 @@ Feature: Content Negotiation support
<name>XML!</name>
</root>
"""
Then the header "Content-Type" should be equal to "application/xml"
Then the response status code should be 201
And the header "Content-Type" should be equal to "application/xml"
And the response should be equal to
"""
<?xml version="1.0"?>
<response><id>1</id><name>XML!</name><alias/><description/><dummyDate/><dummyPrice/><jsonData/><relatedDummy/><dummyBoolean/><dummy/><relatedDummies/><nameConverted/></response>
"""

Scenario: Retrieve a collection in XML
When I add "Accept" header equal to "text/xml"
And I send a "GET" request to "/dummies"
Then the response status code should be 200
And the header "Content-Type" should be equal to "text/xml; charset=UTF-8"
And the response should be equal to
"""
<?xml version="1.0"?>
<response><item key="0"><id>1</id><name>XML!</name><alias/><description/><dummyDate/><dummyPrice/><jsonData/><relatedDummy/><dummyBoolean/><dummy/><relatedDummies/><nameConverted/></item></response>
"""

Scenario: Retrieve a collection in JSON
When I add "Accept" header equal to "application/json"
And I send a "GET" request to "/dummies"
Then the response status code should be 200
And the header "Content-Type" should be equal to "application/json"
And the response should be in JSON
And the JSON should be equal to:
"""
[
{
"id": 1,
"name": "XML!",
"alias": null,
"description": null,
"dummyDate": null,
"dummyPrice": null,
"jsonData": [],
"relatedDummy": null,
"dummyBoolean": null,
"dummy": null,
"relatedDummies": [],
"nameConverted": null
}
]
"""

@dropSchema
Scenario: Requesting an unknown format should return JSON-LD
When I add "Accept" header equal to "text/plain"
Expand Down
1 change: 1 addition & 0 deletions features/fos-user.feature → features/fos_user.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Feature: Create-Retrieve-Update-Delete
"""
{
"fullname": "Dummy User",
"username": "dummy.user",
"email": "dummy.user@example.com",
"plainPassword": "azerty"
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Feature: Create-Retrieve-Update-Delete with custom attribute
Feature: Create-Retrieve-Update-Delete with a Overridden Operation context
In order to use an hypermedia API
As a client software developer
I need to be able to retrieve, create, update and delete JSON-LD encoded resources.

@createSchema
Scenario: Create a resource
When I send a "POST" request to "/custom_attribute_dummies" with body:
When I send a "POST" request to "/overridden_operation_dummies" with body:
"""
{
"name": "My Custom Attribute Dummy",
"name": "My Overridden Operation Dummy",
"description" : "Gerard",
"alias": "notWritable"
}
Expand All @@ -19,52 +19,63 @@ Feature: Create-Retrieve-Update-Delete with custom attribute
And the JSON should be equal to:
"""
{
"@context": "/contexts/CustomAttributeDummy",
"@id": "/custom_attribute_dummies/1",
"@type": "CustomAttributeDummy",
"name": "My Custom Attribute Dummy",
"@context": "/contexts/OverriddenOperationDummy",
"@id": "/overridden_operation_dummies/1",
"@type": "OverriddenOperationDummy",
"name": "My Overridden Operation Dummy",
"alias": null,
"description": "Gerard"
}
"""

Scenario: Get a resource
When I send a "GET" request to "/custom_attribute_dummies/1"
When I send a "GET" request to "/overridden_operation_dummies/1"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json"
And the JSON should be equal to:
"""
{
"@context": "/contexts/CustomAttributeDummy",
"@id": "/custom_attribute_dummies/1",
"@type": "CustomAttributeDummy",
"name": "My Custom Attribute Dummy",
"@context": "/contexts/OverriddenOperationDummy",
"@id": "/overridden_operation_dummies/1",
"@type": "OverriddenOperationDummy",
"name": "My Overridden Operation Dummy",
"alias": null,
"description": "Gerard"
}
"""

Scenario: Get a resource in XML
When I add "Accept" header equal to "application/xml"
And I send a "GET" request to "/overridden_operation_dummies/1"
Then the response status code should be 200
And the header "Content-Type" should be equal to "application/xml"
And the response should be equal to
"""
<?xml version="1.0"?>
<response><name>My Overridden Operation Dummy</name><alias/><description>Gerard</description></response>
"""

Scenario: Get a not found exception
When I send a "GET" request to "/custom_attribute_dummies/42"
When I send a "GET" request to "/overridden_operation_dummies/42"
Then the response status code should be 404

Scenario: Get a collection
When I send a "GET" request to "/custom_attribute_dummies"
When I send a "GET" request to "/overridden_operation_dummies"
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"
And the JSON should be equal to:
"""
{
"@context": "/contexts/CustomAttributeDummy",
"@id": "\/custom_attribute_dummies",
"@context": "/contexts/OverriddenOperationDummy",
"@id": "\/overridden_operation_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "\/custom_attribute_dummies\/1",
"@type": "CustomAttributeDummy",
"name": "My Custom Attribute Dummy",
"@id": "\/overridden_operation_dummies\/1",
"@type": "OverriddenOperationDummy",
"name": "My Overridden Operation Dummy",
"alias": null,
"description": "Gerard"
}
Expand All @@ -74,10 +85,10 @@ Feature: Create-Retrieve-Update-Delete with custom attribute
"""

Scenario: Update a resource
When I send a "PUT" request to "/custom_attribute_dummies/1" with body:
When I send a "PUT" request to "/overridden_operation_dummies/1" with body:
"""
{
"@id": "/custom_attribute_dummies/1",
"@id": "/overridden_operation_dummies/1",
"name": "A nice dummy",
"alias": "Dummy"
}
Expand All @@ -88,32 +99,33 @@ Feature: Create-Retrieve-Update-Delete with custom attribute
And the JSON should be equal to:
"""
{
"@context": "/contexts/CustomAttributeDummy",
"@id": "/custom_attribute_dummies/1",
"@type": "CustomAttributeDummy",
"@context": "/contexts/OverriddenOperationDummy",
"@id": "/overridden_operation_dummies/1",
"@type": "OverriddenOperationDummy",
"alias": "Dummy",
"description": "Gerard"
}
"""

Scenario: Get the final resource
When I send a "GET" request to "/custom_attribute_dummies/1"
When I send a "GET" request to "/overridden_operation_dummies/1"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json"
And the JSON should be equal to:
"""
{
"@context": "/contexts/CustomAttributeDummy",
"@id": "/custom_attribute_dummies/1",
"@type": "CustomAttributeDummy",
"name": "My Custom Attribute Dummy",
"@context": "/contexts/OverriddenOperationDummy",
"@id": "/overridden_operation_dummies/1",
"@type": "OverriddenOperationDummy",
"name": "My Overridden Operation Dummy",
"alias": "Dummy",
"description": "Gerard"
}
"""

@dropSchema
Scenario: Delete a resource
When I send a "DELETE" request to "/custom_attribute_dummies/1"
When I send a "DELETE" request to "/overridden_operation_dummies/1"
Then the response status code should be 204
And the response should be empty
4 changes: 2 additions & 2 deletions src/Action/GetCollectionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
*/
public function __invoke(Request $request)
{
list($resourceClass, $operationName) = RequestAttributesExtractor::extractAttributes($request);
$attributes = RequestAttributesExtractor::extractAttributes($request);

return $this->collectionDataProvider->getCollection($resourceClass, $operationName);
return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name']);
}
}
4 changes: 2 additions & 2 deletions src/Action/GetItemAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public function __construct(ItemDataProviderInterface $itemDataProvider)
*/
public function __invoke(Request $request, $id)
{
list($resourceClass, , $operationName) = RequestAttributesExtractor::extractAttributes($request);
$attributes = RequestAttributesExtractor::extractAttributes($request);

$data = $this->itemDataProvider->getItem($resourceClass, $id, $operationName, true);
$data = $this->itemDataProvider->getItem($attributes['resource_class'], $id, $attributes['item_operation_name'], true);
if (!$data) {
throw new NotFoundHttpException('Not Found');
}
Expand Down
38 changes: 24 additions & 14 deletions src/Api/RequestAttributesExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
* Extracts data used by the library form a Request instance.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @internal
*/
final class RequestAttributesExtractor
{
const API_ATTRIBUTES = ['resource_class', 'format', 'mime_type'];

/**
* Extracts resource class, operation name and format request attributes. Throws an exception if the request does not contain required
* attributes.
* Extracts resource class, operation name and format request attributes. Throws an exception if the request does not
* contain required attributes.
*
* @param Request $request
*
Expand All @@ -33,24 +37,30 @@ final class RequestAttributesExtractor
*/
public static function extractAttributes(Request $request)
{
$resourceClass = $request->attributes->get('_resource_class');
$result = [];

if (!$resourceClass) {
throw new RuntimeException('The request attribute "_resource_class" must be defined.');
}
foreach (self::API_ATTRIBUTES as $key) {
$attributeKey = '_api_'.$key;
$attributeValue = $request->attributes->get($attributeKey);

$collectionOperation = $request->attributes->get('_collection_operation_name');
$itemOperation = $request->attributes->get('_item_operation_name');
if (null === $attributeValue) {
throw new RuntimeException(sprintf('The request attribute "%s" must be defined.', $attributeKey));
}

if (!$itemOperation && !$collectionOperation) {
throw new RuntimeException('One of the request attribute "_item_operation_name" or "_collection_operation_name" must be defined.');
$result[$key] = $attributeValue;
}

$format = $request->attributes->get('_api_format');
if (!$format) {
throw new RuntimeException('The request attribute "_api_format" must be defined.');
$collectionOperationName = $request->attributes->get('_api_collection_operation_name');
$itemOperationName = $request->attributes->get('_api_item_operation_name');

if ($collectionOperationName) {
$result['collection_operation_name'] = $collectionOperationName;
} elseif ($itemOperationName) {
$result['item_operation_name'] = $itemOperationName;
} else {
throw new RuntimeException('One of the request attribute "_api_collection_operation_name" or "_api_item_operation_name" must be defined.');
}

return [$resourceClass, $collectionOperation, $itemOperation, $format];
return $result;
}
}
7 changes: 3 additions & 4 deletions src/Bridge/Doctrine/EventListener/ManagerViewListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,16 @@ public function __construct(ManagerRegistry $managerRegistry)
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if (!in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_DELETE])) {
if (!in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_DELETE], true)) {
return;
}

$resourceClass = $request->attributes->get('_resource_class');
if (!$resourceClass) {
$resourceClass = $request->attributes->get('_api_resource_class');
if (null === $resourceClass) {
return;
}

$controllerResult = $event->getControllerResult();

if (null === $objectManager = $this->getManager($resourceClass, $controllerResult)) {
return $controllerResult;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ public function load(array $configs, ContainerBuilder $container)
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$supportedFormats = [];
foreach ($config['supported_formats'] as $format => $value) {
$formats = [];
foreach ($config['formats'] as $format => $value) {
foreach ($value['mime_types'] as $mimeType) {
$supportedFormats[$mimeType] = $format;
$formats[$mimeType] = $format;
}
}

Expand All @@ -67,7 +67,7 @@ public function load(array $configs, ContainerBuilder $container)

$container->setParameter('api_platform.title', $config['title']);
$container->setParameter('api_platform.description', $config['description']);
$container->setParameter('api_platform.supported_formats', $supportedFormats);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
$container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getConfigTreeBuilder()
->children()
->scalarNode('title')->defaultValue('')->info('The title of the API.')->end()
->scalarNode('description')->defaultValue('')->info('The description of the API.')->end()
->arrayNode('supported_formats')
->arrayNode('formats')
->defaultValue(['jsonld' => ['mime_types' => ['application/ld+json']]])
->info('The list of enabled formats. The first one will be the default.')
->normalizeKeys(false)
Expand Down
Loading