Skip to content

Commit e77ac0b

Browse files
committed
links
1 parent db50493 commit e77ac0b

File tree

13 files changed

+159
-27
lines changed

13 files changed

+159
-27
lines changed

features/mercure/discover.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Feature: Mercure discovery support
66
@createSchema
77
Scenario: Checks that the Mercure Link is added
88
Given I send a "GET" request to "/dummy_mercures"
9-
Then the header "Link" should be equal to '<http://example.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation",<https://demo.mercure.rocks/hub>; rel="mercure"'
9+
Then the header "Link" should contain '<https://demo.mercure.rocks/hub>; rel="mercure"'
1010

1111
Scenario: Checks that the Mercure Link is not added on endpoints where updates are not dispatched
1212
Given I send a "GET" request to "/"
13-
Then the header "Link" should be equal to '<http://example.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'
13+
Then the header "Link" should not contain '<https://demo.mercure.rocks/hub>; rel="mercure"'

src/Doctrine/Common/State/PersistProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
5656
\assert(method_exists($manager, 'getReference'));
5757
// TODO: the call to getReference is most likely to fail with complex identifiers
5858
$newData = $data;
59-
if ($previousData = $request?->attributes->get('previous_data')) {
59+
if ($previousData = $context['previous_data'] ?? $request?->attributes->get('previous_data')) {
6060
$newData = 1 === \count($uriVariables) ? $manager->getReference($class, current($uriVariables)) : clone $previousData;
6161
}
6262

src/Hydra/EventListener/AddLinkHeaderListener.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ public function onKernelResponse(ResponseEvent $event): void
4646

4747
$apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL);
4848
$link = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl);
49+
$linkProvider = $request->attributes->get('_links') ?? new GenericLinkProvider();
4950

50-
if (null === $linkProvider = $request->attributes->get('_links')) {
51-
$request->attributes->set('_links', new GenericLinkProvider([$link]));
52-
53-
return;
51+
foreach ($linkProvider->getLinks() as $link) {
52+
if ($link->getHref() === $apiDocUrl) {
53+
return;
54+
}
5455
}
56+
5557
$request->attributes->set('_links', $linkProvider->withLink($link));
5658
}
5759
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Hydra\State;
15+
16+
use ApiPlatform\Api\UrlGeneratorInterface;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\State\ProcessorInterface;
20+
use Symfony\Component\WebLink\GenericLinkProvider;
21+
use Symfony\Component\WebLink\Link;
22+
23+
final class HydraLinkProcessor implements ProcessorInterface
24+
{
25+
/**
26+
* @param ProcessorInterface<mixed> $inner
27+
*/
28+
public function __construct(private readonly ProcessorInterface $inner, private readonly UrlGeneratorInterface $urlGenerator)
29+
{
30+
}
31+
32+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
33+
{
34+
if (!($request = $context['request'] ?? null)) {
35+
return $this->inner->process($data, $operation, $uriVariables, $context);
36+
}
37+
38+
$apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL);
39+
$link = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl);
40+
$linkProvider = $request->attributes->get('_links') ?? new GenericLinkProvider();
41+
$request->attributes->set('_links', $linkProvider->withLink($link));
42+
43+
return $this->inner->process($data, $operation, $uriVariables, $context);
44+
}
45+
}

src/Serializer/SerializerContextBuilder.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ public function createFromRequest(Request $request, bool $normalization, array $
5656
$context['output'] = $operation->getOutput();
5757
$context['skip_deprecated_exception_normalizers'] = true;
5858

59+
if ($types = $operation->getTypes()) {
60+
$context['types'] = $types;
61+
}
62+
63+
// TODO: remove this as uri variables are available in the SerializerProcessor but correctly parsed
64+
if ($operation->getUriVariables()) {
65+
$context['uri_variables'] = [];
66+
67+
foreach (array_keys($operation->getUriVariables()) as $parameterName) {
68+
$context['uri_variables'][$parameterName] = $request->attributes->get($parameterName);
69+
}
70+
}
71+
5972
if (($options = $operation?->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
6073
$context['force_resource_class'] = $operation->getClass();
6174
}
@@ -68,13 +81,9 @@ public function createFromRequest(Request $request, bool $normalization, array $
6881
$context['groups'][] = 'trace';
6982
}
7083

71-
if ($operation->getTypes()) {
72-
$context['types'] = $operation->getTypes();
73-
}
74-
7584
if (!$normalization) {
7685
if (!isset($context['api_allow_update'])) {
77-
$context['api_allow_update'] = \in_array($method = $operation->getMethod(), ['PUT', 'PATCH'], true);
86+
$context['api_allow_update'] = \in_array($method = $request->getMethod(), ['PUT', 'PATCH'], true);
7887

7988
if ($context['api_allow_update'] && 'PATCH' === $method) {
8089
$context['deep_object_to_populate'] ??= true;

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,11 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a
487487
}
488488

489489
$loader->load('jsonld.xml');
490+
491+
if ($config['event_listeners_backward_compatibility_layer'] ?? true) {
492+
$loader->load('legacy/hydra.xml');
493+
}
494+
490495
$loader->load('hydra.xml');
491496

492497
if (!$container->has('api_platform.json_schema.schema_factory')) {
@@ -755,6 +760,10 @@ private function registerMercureConfiguration(ContainerBuilder $container, array
755760

756761
$container->setParameter('api_platform.mercure.include_type', $config['mercure']['include_type']);
757762

763+
if ($config['event_listeners_backward_compatibility_layer'] ?? true) {
764+
$loader->load('legacy/mercure.xml');
765+
}
766+
758767
$loader->load('mercure.xml');
759768

760769
if ($this->isConfigEnabled($container, $config['doctrine'])) {

src/Symfony/Bundle/Resources/config/hydra.xml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
</service>
2121

2222

23-
<!-- Event listener -->
23+
<!-- State -->
2424

25-
<service id="api_platform.hydra.listener.response.add_link_header" class="ApiPlatform\Hydra\EventListener\AddLinkHeaderListener">
25+
<service id="api_platform.hydra.processor.link" class="ApiPlatform\Hydra\State\HydraLinkProcessor" decorates="api_platform.state_processor" decoration-priority="410">
26+
<argument type="service" id="api_platform.hydra.processor.link.inner" />
2627
<argument type="service" id="api_platform.router" />
27-
28-
<tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
2928
</service>
3029

3130
<!-- Serializer -->
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
<services>
6+
<service id="api_platform.hydra.listener.response.add_link_header" class="ApiPlatform\Hydra\EventListener\AddLinkHeaderListener">
7+
<argument type="service" id="api_platform.router" />
8+
9+
<tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
10+
</service>
11+
</services>
12+
</container>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<!-- Event listener -->
9+
10+
<service id="api_platform.mercure.listener.response.add_link_header" class="ApiPlatform\Symfony\EventListener\AddLinkHeaderListener">
11+
<argument type="service" id="Symfony\Component\Mercure\Discovery" on-invalid="ignore"/>
12+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
13+
14+
<tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
15+
</service>
16+
</services>
17+
</container>

src/Symfony/Bundle/Resources/config/mercure.xml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
<services>
88
<!-- Event listener -->
99

10-
<service id="api_platform.mercure.listener.response.add_link_header" class="ApiPlatform\Symfony\EventListener\AddLinkHeaderListener">
10+
<service id="api_platform.mercure.processor.add_link_header" class="ApiPlatform\Symfony\State\MercureLinkProcessor" decorates="api_platform.state_processor" decoration-priority="400">
11+
<argument type="service" id="api_platform.mercure.processor.add_link_header.inner" />
1112
<argument type="service" id="Symfony\Component\Mercure\Discovery" on-invalid="ignore"/>
12-
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
13-
14-
<tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
1513
</service>
1614
</services>
1715
</container>

src/Symfony/Bundle/Resources/config/state.xml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<service id="api_platform.state_processor.locator" class="ApiPlatform\State\CallableProcessor">
1313
<argument type="tagged_locator" tag="api_platform.state_processor" index-by="key" />
1414
</service>
15-
<service id="api_platform.state_processor" alias="api_platform.state_processor.locator" />
1615

1716
<service id="api_platform.state_provider.content_negotiation" class="ApiPlatform\State\Provider\ContentNegotiationProvider" decorates="api_platform.state_provider" decoration-priority="100">
1817
<argument type="service" id="api_platform.state_provider.content_negotiation.inner" />
@@ -37,20 +36,19 @@
3736
<argument type="service" id="api_platform.resource_class_resolver" />
3837
<argument type="service" id="api_platform.iri_converter" />
3938
</service>
39+
<service id="api_platform.state_processor" alias="api_platform.state_processor.respond" />
4040

41-
<service id="api_platform.state_processor.serialize" class="ApiPlatform\State\Processor\SerializeProcessor" decorates="api_platform.state_processor.respond" decoration-priority="200">
41+
<service id="api_platform.state_processor.serialize" class="ApiPlatform\State\Processor\SerializeProcessor" decorates="api_platform.state_processor" decoration-priority="200">
4242
<argument type="service" id="api_platform.state_processor.serialize.inner" />
4343
<argument type="service" id="api_platform.serializer" />
4444
<argument type="service" id="api_platform.serializer.context_builder" />
4545
</service>
4646

47-
<service id="api_platform.state_processor.write" class="ApiPlatform\State\Processor\WriteProcessor" decorates="api_platform.state_processor.respond" decoration-priority="100">
47+
<service id="api_platform.state_processor.write" class="ApiPlatform\State\Processor\WriteProcessor" decorates="api_platform.state_processor" decoration-priority="100">
4848
<argument type="service" id="api_platform.state_processor.write.inner" />
4949
<argument type="service" id="api_platform.state_processor.locator" />
5050
</service>
5151

52-
<service id="api_platform.state_processor" alias="api_platform.state_processor.respond" />
53-
5452
<service id="api_platform.pagination" class="ApiPlatform\State\Pagination\Pagination">
5553
<argument>%api_platform.collection.pagination%</argument>
5654
<argument>%api_platform.graphql.collection.pagination%</argument>

src/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,14 @@ public function __invoke(Request $request): Response
8282
'extraConfiguration' => $this->swaggerUiContext->getExtraConfiguration(),
8383
];
8484

85-
if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) {
85+
$originalRouteParams = $request->attributes->get('_api_original_route_params') ?? [];
86+
$resourceClass = $originalRouteParams['_api_resource_class'] ?? $request->attributes->get('_api_resource_class');
87+
88+
if ($request->isMethodSafe() && $resourceClass) {
8689
$swaggerData['id'] = $request->attributes->get('id');
8790
$swaggerData['queryParameters'] = $request->query->all();
8891

89-
$metadata = $this->resourceMetadataFactory->create($resourceClass)->getOperation($request->attributes->get('_api_operation_name'));
92+
$metadata = $this->resourceMetadataFactory->create($resourceClass)->getOperation($originalRouteParams['_api_operation_name'] ?? $request->attributes->get('_api_operation_name'));
9093

9194
$swaggerData['shortName'] = $metadata->getShortName();
9295
$swaggerData['operationId'] = $this->normalizeOperationName($metadata->getName());
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\State;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\State\ProcessorInterface;
18+
use Symfony\Component\Mercure\Discovery;
19+
20+
final class MercureLinkProcessor implements ProcessorInterface
21+
{
22+
/**
23+
* @param ProcessorInterface<mixed> $inner
24+
*/
25+
public function __construct(private readonly ProcessorInterface $inner, private readonly Discovery $discovery)
26+
{
27+
}
28+
29+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
30+
{
31+
if (!($request = $context['request'] ?? null) || !$mercure = $operation->getMercure()) {
32+
return $this->inner->process($data, $operation, $uriVariables, $context);
33+
}
34+
35+
$hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null;
36+
$this->discovery->addLink($request, $hub);
37+
38+
return $this->inner->process($data, $operation, $uriVariables, $context);
39+
}
40+
}

0 commit comments

Comments
 (0)