Skip to content

Commit 0e4e0a3

Browse files
feat(symfony): add getOperation Expression Language function on Mercure topics
1 parent 2e48c7e commit 0e4e0a3

File tree

6 files changed

+201
-3
lines changed

6 files changed

+201
-3
lines changed

features/mercure/publish.feature

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Feature: Mercure publish support
2+
In order to publish an Update to the Mercure hub
3+
As a developer
4+
I need to specify which topics I want to send the Update on
5+
6+
@createSchema
7+
Scenario: Checks that Mercure Updates are dispatched following topics configured with expression language
8+
Given I add "Accept" header equal to "application/ld+json"
9+
And I add "Content-Type" header equal to "application/ld+json"
10+
When I send a "POST" request to "/mercure_with_topics" with body:
11+
"""
12+
{
13+
"name": "Hello World!"
14+
}
15+
"""
16+
Then the response status code should be 201
17+
And the response should be in JSON
18+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
Then 1 Mercure update should have been sent
20+
And the Mercure update should have topics:
21+
| http://example.com/mercure_with_topics/1 |
22+
| http://example.com/custom_resource/mercure_with_topics/1 |
23+
And the Mercure update should have data:
24+
"""
25+
{
26+
"@context": "/contexts/MercureWithTopics",
27+
"@id": "/mercure_with_topics/1",
28+
"@type": "MercureWithTopics",
29+
"id": 1,
30+
"name": "Hello World!"
31+
}
32+
"""

src/Doctrine/EventListener/PublishMercureUpdatesListener.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface as GraphQlMercureSubscriptionIriGeneratorInterface;
2323
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface as GraphQlSubscriptionManagerInterface;
2424
use ApiPlatform\Metadata\HttpOperation;
25+
use ApiPlatform\Metadata\Operation;
2526
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2627
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
2728
use ApiPlatform\Symfony\Messenger\DispatchTrait;
@@ -82,7 +83,10 @@ public function __construct(ResourceClassResolverInterface $resourceClassResolve
8283
$this->expressionLanguage->addFunction($rawurlencode);
8384

8485
$this->expressionLanguage->addFunction(
85-
new ExpressionFunction('iri', static fn (string $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL): string => sprintf('iri(%s, %d)', $apiResource, $referenceType), static fn (array $arguments, $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL): string => $iriConverter->getIriFromResource($apiResource, $referenceType))
86+
new ExpressionFunction('getOperation', static fn (string $apiResource, string $name): string => sprintf('getOperation(%s, %s)', $apiResource, $name), static fn (array $arguments, $apiResource, string $name): Operation => $resourceMetadataFactory->create($resourceClassResolver->getResourceClass($apiResource))->getOperation($name))
87+
);
88+
$this->expressionLanguage->addFunction(
89+
new ExpressionFunction('iri', static fn (string $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL, string $operation = null): string => sprintf('iri(%s, %d, %s)', $apiResource, $referenceType, $operation), static fn (array $arguments, $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL, $operation = null): string => $iriConverter->getIriFromResource($apiResource, $referenceType, $operation))
8690
);
8791
}
8892

tests/Behat/MercureContext.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
use Behat\Behat\Context\Context;
1717
use Behat\Gherkin\Node\PyStringNode;
18+
use Behat\Gherkin\Node\TableNode;
19+
use PHPUnit\Framework\Assert;
1820
use Psr\Container\ContainerInterface;
21+
use Symfony\Component\Mercure\Update;
1922

2023
/**
2124
* Context for Mercure.
@@ -28,6 +31,80 @@ public function __construct(private readonly ContainerInterface $driverContainer
2831
{
2932
}
3033

34+
/**
35+
* @Then :number Mercure updates should have been sent
36+
* @Then :number Mercure update should have been sent
37+
*/
38+
public function mercureUpdatesShouldHaveBeenSent(int $number): void
39+
{
40+
$updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler');
41+
$total = \count($updateHandler->getUpdates());
42+
43+
if (0 === $total) {
44+
throw new \RuntimeException('No Mercure update has been sent.');
45+
}
46+
47+
Assert::assertEquals($number, $total, sprintf('Expected %d Mercure updates to be sent, got %d.', $number, $total));
48+
}
49+
50+
/**
51+
* @Then the first Mercure update should have topics:
52+
* @Then the Mercure update should have topics:
53+
*/
54+
public function firstMercureUpdateShouldHaveTopics(TableNode $table): void
55+
{
56+
$this->mercureUpdateShouldHaveTopics(1, $table);
57+
}
58+
59+
/**
60+
* @Then the first Mercure update should have data:
61+
* @Then the Mercure update should have data:
62+
*/
63+
public function firstMercureUpdateShouldHaveData(PyStringNode $data): void
64+
{
65+
$this->mercureUpdateShouldHaveData(1, $data);
66+
}
67+
68+
/**
69+
* @Then the Mercure update number :index should have topics:
70+
*/
71+
public function mercureUpdateShouldHaveTopics(int $index, TableNode $table): void
72+
{
73+
$updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler');
74+
$updates = $updateHandler->getUpdates();
75+
76+
if (0 === \count($updates)) {
77+
throw new \RuntimeException('No Mercure update has been sent.');
78+
}
79+
80+
if (!isset($updates[$index - 1])) {
81+
throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index));
82+
}
83+
/** @var Update $update */
84+
$update = $updates[$index - 1];
85+
Assert::assertEquals(array_keys($table->getRowsHash()), array_values($update->getTopics()));
86+
}
87+
88+
/**
89+
* @Then the Mercure update number :index should have data:
90+
*/
91+
public function mercureUpdateShouldHaveData(int $index, PyStringNode $data): void
92+
{
93+
$updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler');
94+
$updates = $updateHandler->getUpdates();
95+
96+
if (0 === \count($updates)) {
97+
throw new \RuntimeException('No Mercure update has been sent.');
98+
}
99+
100+
if (!isset($updates[$index - 1])) {
101+
throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index));
102+
}
103+
/** @var Update $update */
104+
$update = $updates[$index - 1];
105+
Assert::assertJsonStringEqualsJsonString($data->getRaw(), $update->getData());
106+
}
107+
31108
/**
32109
* @Then the following Mercure update with topics :topics should have been sent:
33110
*/
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Post;
19+
use ApiPlatform\Metadata\UrlGeneratorInterface;
20+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
21+
22+
#[ApiResource(
23+
operations: [
24+
new Get(uriTemplate: '/mercure_with_topics/{id}{._format}'),
25+
new Post(uriTemplate: '/mercure_with_topics{._format}'),
26+
new Get(uriTemplate: '/custom_resource/mercure_with_topics/{id}{._format}'),
27+
],
28+
mercure: [
29+
'topics' => [
30+
'@=iri(object)',
31+
'@=iri(object, '.UrlGeneratorInterface::ABS_URL.', getOperation(object, "/custom_resource/mercure_with_topics/{id}{._format}"))',
32+
],
33+
]
34+
)]
35+
#[ODM\Document]
36+
class MercureWithTopics
37+
{
38+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
39+
public $id;
40+
#[ODM\Field(type: 'string')]
41+
public $name;
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Post;
19+
use ApiPlatform\Metadata\UrlGeneratorInterface;
20+
use Doctrine\ORM\Mapping as ORM;
21+
22+
#[ApiResource(
23+
operations: [
24+
new Get(uriTemplate: '/mercure_with_topics/{id}{._format}'),
25+
new Post(uriTemplate: '/mercure_with_topics{._format}'),
26+
new Get(uriTemplate: '/custom_resource/mercure_with_topics/{id}{._format}'),
27+
],
28+
mercure: [
29+
'topics' => [
30+
'@=iri(object)',
31+
'@=iri(object, '.UrlGeneratorInterface::ABS_URL.', getOperation(object, "/custom_resource/mercure_with_topics/{id}{._format}"))',
32+
],
33+
]
34+
)]
35+
#[ORM\Entity]
36+
class MercureWithTopics
37+
{
38+
#[ORM\Id]
39+
#[ORM\Column(type: 'integer')]
40+
#[ORM\GeneratedValue(strategy: 'AUTO')]
41+
public $id;
42+
#[ORM\Column]
43+
public $name;
44+
}

tests/Symfony/Bundle/Test/ApiTestCaseTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,7 @@ public function testFindIriBy(): void
214214
*/
215215
public function testGetMercureMessages(): void
216216
{
217-
// debug mode is required to get Mercure TraceableHub
218-
$this->recreateSchema(['debug' => true, 'environment' => 'mercure']);
217+
$this->recreateSchema(['environment' => 'mercure']);
219218

220219
self::createClient()->request('POST', '/direct_mercures', [
221220
'headers' => [

0 commit comments

Comments
 (0)