Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions features/hal.feature
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ Feature: HAL support
I need to be able to retrieve valid HAL responses.

@createSchema
Scenario: Retrieve the API entrypoint
When I add "Accept" header equal to "application/hal+json"
And I send a "GET" request to "/"
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/hal+json"
And the JSON node "_links.self.href" should be equal to "/"
And the JSON node "_links.dummy.href" should be equal to "/dummies"

Scenario: Create a third level
When I add "Content-Type" header equal to "application/json"
And I send a "POST" request to "/third_levels" with body:
Expand Down
13 changes: 7 additions & 6 deletions src/Action/EntrypointAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

namespace ApiPlatform\Core\Action;

use ApiPlatform\Core\JsonLd\EntrypointBuilderInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;

/**
* Generates the API entrypoint.
Expand All @@ -20,15 +21,15 @@
*/
final class EntrypointAction
{
private $entrypointBuilder;
private $resourceNameCollectionFactory;

public function __construct(EntrypointBuilderInterface $entrypointBuilder)
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollection)
{
$this->entrypointBuilder = $entrypointBuilder;
$this->resourceNameCollectionFactory = $resourceNameCollection;
}

public function __invoke() : array
public function __invoke() : ResourceNameCollection
{
return $this->entrypointBuilder->getEntrypoint();
return $this->resourceNameCollectionFactory->create();
}
}
6 changes: 6 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.naming.resource_path_naming_strategy" />
<argument type="service" id="service_container" />
<argument>%api_platform.formats%</argument>

<tag name="routing.loader" />
</service>

Expand Down Expand Up @@ -114,6 +116,10 @@
<service id="api_platform.action.get_item" alias="api_platform.action.placeholder" />
<service id="api_platform.action.put_item" alias="api_platform.action.placeholder" />
<service id="api_platform.action.delete_item" alias="api_platform.action.placeholder" />

<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
</service>
</services>

</container>
23 changes: 15 additions & 8 deletions src/Bridge/Symfony/Bundle/Resources/config/hal.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<!-- Serializer -->
<service id="api_platform.hal.encoder" class="ApiPlatform\Core\Serializer\JsonEncoder" public="false">
<argument>jsonhal</argument>

<tag name="serializer.encoder" />
</service>

<service id="api_platform.hal.normalizer.resource_name_collection" class="ApiPlatform\Core\Hal\Serializer\ResourceNameCollectionNormalizer" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.router" />

<tag name="serializer.normalizer" priority="32" />
</service>

<service id="api_platform.hal.normalizer.collection" class="ApiPlatform\Core\Hal\Serializer\CollectionNormalizer" public="false">
<argument type="service" id="api_platform.resource_class_resolver" />
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>

<tag name="serializer.normalizer" priority="16" />
</service>

<service id="api_platform.hal.normalizer.item" class="ApiPlatform\Core\Hal\Serializer\ItemNormalizer" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
Expand All @@ -23,13 +37,6 @@

<tag name="serializer.normalizer" priority="8" />
</service>

<service id="api_platform.hal.normalizer.collection" class="ApiPlatform\Core\Hal\Serializer\CollectionNormalizer" public="false">
<argument type="service" id="api_platform.resource_class_resolver" />
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>

<tag name="serializer.normalizer" priority="50" />
</service>
</services>

</container>
22 changes: 10 additions & 12 deletions src/Bridge/Symfony/Bundle/Resources/config/hydra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="api_platform.hydra.entrypoint_builder" class="ApiPlatform\Core\Hydra\EntrypointBuilder" public="false">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.router" />
</service>

<service id="api_platform.hydra.documentation_builder" class="ApiPlatform\Core\Hydra\ApiDocumentationBuilder" public="false">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
Expand Down Expand Up @@ -49,12 +42,20 @@

<!-- Serializer -->

<service id="api_platform.hydra.normalizer.resource_name_collection" class="ApiPlatform\Core\Hydra\Serializer\ResourceNameCollectionNormalizer" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.router" />

<tag name="serializer.normalizer" priority="32" />
</service>

<service id="api_platform.hydra.normalizer.collection" class="ApiPlatform\Core\Hydra\Serializer\CollectionNormalizer" public="false">
<argument type="service" id="api_platform.jsonld.context_builder" />
<argument type="service" id="api_platform.resource_class_resolver" />
<argument type="service" id="api_platform.iri_converter" />

<tag name="serializer.normalizer" priority="50" />
<tag name="serializer.normalizer" priority="16" />
</service>

<service id="api_platform.hydra.normalizer.constraint_violation_list" class="ApiPlatform\Core\Bridge\Symfony\Validator\Hydra\Serializer\ConstraintViolationListNormalizer" public="false">
Expand Down Expand Up @@ -82,11 +83,8 @@
<argument type="service" id="api_platform.resource_class_resolver" />
<argument type="service" id="api_platform.filters" />
</service>
<!-- Action -->

<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction">
<argument type="service" id="api_platform.hydra.entrypoint_builder" />
</service>
<!-- Action -->

<service id="api_platform.hydra.action.documentation" class="ApiPlatform\Core\Documentation\Action\DocumentationAction">
<argument type="service" id="api_platform.hydra.documentation_builder" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_hal_entrypoint" path="/{index}.jsonhal">
<route id="api_entrypoint" path="/{index}.{_format}">
<default key="_controller">api_platform.action.entrypoint</default>
<default key="_format" />
<default key="_api_respond">1</default>
<default key="index">index</default>
<requirement key="index">index</requirement>
Expand Down
8 changes: 0 additions & 8 deletions src/Bridge/Symfony/Bundle/Resources/config/routing/hydra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_hydra_entrypoint" path="/{index}.{_format}">
<default key="_controller">api_platform.action.entrypoint</default>
<default key="_api_respond">1</default>
<default key="_format">jsonld</default>
<default key="index">index</default>
<requirement key="index">index</requirement>
</route>

<route id="api_hydra_doc" path="/apidoc.{_format}">
<default key="_controller">api_platform.hydra.action.documentation</default>
<default key="_api_respond">1</default>
Expand Down
8 changes: 0 additions & 8 deletions src/Bridge/Symfony/Bundle/Resources/config/routing/jsonld.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_jsonld_entrypoint" path="/{index}.{_format}">
<default key="_controller">api_platform.action.entrypoint</default>
<default key="_api_respond">1</default>
<default key="_format">jsonld</default>
<default key="index">index</default>
<requirement key="index">index</requirement>
</route>

<route id="api_jsonld_context" path="/contexts/{shortName}.{_format}">
<default key="_controller">api_platform.jsonld.action.context</default>
<default key="_api_respond">1</default>
Expand Down
23 changes: 19 additions & 4 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ final class ApiLoader extends Loader
private $resourceMetadataFactory;
private $resourcePathGenerator;
private $container;
private $formats;

public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourcePathNamingStrategyInterface $resourcePathGenerator, ContainerInterface $container)
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourcePathNamingStrategyInterface $resourcePathGenerator, ContainerInterface $container, array $formats)
{
$this->fileLoader = new XmlFileLoader(new FileLocator($kernel->locateResource('@ApiPlatformBundle/Resources/config/routing')));
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->resourcePathGenerator = $resourcePathGenerator;
$this->container = $container;
$this->formats = $formats;
}

/**
Expand All @@ -57,9 +59,7 @@ public function load($data, $type = null)
{
$routeCollection = new RouteCollection();

$routeCollection->addCollection($this->fileLoader->load('hal.xml'));
$routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
$routeCollection->addCollection($this->fileLoader->load('hydra.xml'));
$this->loadExternalFiles($routeCollection);

if ($this->container->getParameter('api_platform.enable_swagger')) {
$routeCollection->addCollection($this->fileLoader->load('swagger.xml'));
Expand Down Expand Up @@ -92,6 +92,21 @@ public function supports($resource, $type = null)
return 'api_platform' === $type;
}

/**
* Load external files.
*
* @param RouteCollection $routeCollection
*/
private function loadExternalFiles(RouteCollection $routeCollection)
{
$routeCollection->addCollection($this->fileLoader->load('api.xml'));

if (isset($this->formats['jsonld'])) {
$routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
$routeCollection->addCollection($this->fileLoader->load('hydra.xml'));
}
}

/**
* Creates and adds a route for the given operation to the route collection.
*
Expand Down
37 changes: 29 additions & 8 deletions src/EventListener/SerializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
Expand Down Expand Up @@ -49,17 +50,11 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
return;
}

if ($request->attributes->get('_api_respond') && !is_object($controllerResult)) {
if (!$this->serializer instanceof EncoderInterface) {
throw new RuntimeException('The serializer instance must implements the "%s" interface.', EncoderInterface::class);
}

$event->setControllerResult($this->serializer->encode($controllerResult, $request->getRequestFormat()));
}

try {
$attributes = RequestAttributesExtractor::extractAttributes($request);
} catch (RuntimeException $e) {
$this->serializeRawData($event, $request, $controllerResult);

return;
}

Expand All @@ -68,4 +63,30 @@ public function onKernelView(GetResponseForControllerResultEvent $event)

$event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context));
}

/**
* Tries to serialize data that are not API resources (e.g. the entrypoint or data returned by a custom controller).
*
* @param GetResponseForControllerResultEvent $event
* @param Request $request
* @param object $controllerResult
*/
private function serializeRawData(GetResponseForControllerResultEvent $event, Request $request, $controllerResult)
{
if (!$request->attributes->get('_api_respond')) {
return;
}

if (is_object($controllerResult)) {
$event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat()));

return;
}

if (!$this->serializer instanceof EncoderInterface) {
throw new RuntimeException('The serializer instance must implements the "%s" interface.', EncoderInterface::class);
}

$event->setControllerResult($this->serializer->encode($controllerResult, $request->getRequestFormat()));
}
}
71 changes: 71 additions & 0 deletions src/Hal/Serializer/ResourceNameCollectionNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ApiPlatform\Core\Hal\Serializer;

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\UrlGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* Normalizes the API entrypoint.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class ResourceNameCollectionNormalizer implements NormalizerInterface
{
const FORMAT = 'jsonhal';

private $resourceMetadataFactory;
private $iriConverter;
private $urlGenerator;

public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IriConverterInterface $iriConverter, UrlGeneratorInterface $urlGenerator)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->iriConverter = $iriConverter;
$this->urlGenerator = $urlGenerator;
}

/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = [])
{
$entrypoint = ['_links' => ['self' => ['href' => $this->urlGenerator->generate('api_entrypoint')]]];

foreach ($object as $resourceClass) {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

if (empty($resourceMetadata->getCollectionOperations())) {
continue;
}
try {
$entrypoint['_links'][lcfirst($resourceMetadata->getShortName())]['href'] = $this->iriConverter->getIriFromResourceClass($resourceClass);
} catch (InvalidArgumentException $ex) {
// Ignore resources without GET operations
}
}

return $entrypoint;
}

/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return self::FORMAT === $format && $data instanceof ResourceNameCollection;
}
}
2 changes: 1 addition & 1 deletion src/Hydra/ApiDocumentationBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public function getApiDocumentation() : array
$doc['hydra:description'] = $this->description;
}

$doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_hydra_entrypoint');
$doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
$doc['hydra:supportedClass'] = $classes;

return $doc;
Expand Down
Loading