From 7f0e00cd2d838037f716e0b8588a6529ef9f158c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Rz=CC=87any?= Date: Thu, 14 Sep 2023 10:33:20 +0200 Subject: [PATCH 1/3] fix: add evaluateTopics() method --- .../PublishMercureUpdatesListener.php | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php index e255a6ec42d..427caf8c737 100644 --- a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -190,35 +190,15 @@ private function storeObjectToPublish(object $object, string $property): void $options['enable_async_update'] ??= true; - if ($options['topics'] ?? false) { - $topics = []; - foreach ((array) $options['topics'] as $topic) { - if (!\is_string($topic)) { - $topics[] = $topic; - continue; - } - - if (!str_starts_with($topic, '@=')) { - $topics[] = $topic; - continue; - } - - if (null === $this->expressionLanguage) { - throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); - } - - $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); - } - - $options['topics'] = $topics; - } - if ('deletedObjects' === $property) { $types = $operation instanceof HttpOperation ? $operation->getTypes() : null; if (null === $types) { $types = [$operation->getShortName()]; } + // We need to evaluate it here, because in publishUpdate() the resource would be already deleted + $this->evaluateTopics($options, $object); + $this->deletedObjects[(object) [ 'id' => $this->iriConverter->getIriFromResource($object), 'iri' => $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL), @@ -244,6 +224,9 @@ private function publishUpdate(object $object, array $options, string $type): vo $resourceClass = $this->getObjectClass($object); $context = $options['normalization_context'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation()->getNormalizationContext() ?? []; + // We need to evaluate it here, because in storeObjectToPublish() the resource would not have been persisted yet + $this->evaluateTopics($options, $object); + $iri = $options['topics'] ?? $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL); $data = $options['data'] ?? $this->serializer->serialize($object, key($this->formats), $context); } @@ -260,6 +243,32 @@ private function publishUpdate(object $object, array $options, string $type): vo } } + private function evaluateTopics(array &$options, object $object): void + { + if ($options['topics'] ?? false) { + $topics = []; + foreach ((array) $options['topics'] as $topic) { + if (!\is_string($topic)) { + $topics[] = $topic; + continue; + } + + if (!str_starts_with($topic, '@=')) { + $topics[] = $topic; + continue; + } + + if (null === $this->expressionLanguage) { + throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); + } + + $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); + } + + $options['topics'] = $topics; + } + } + /** * @return Update[] */ From dbadc6aa0cdebd9cab0a78fdfc9f8ffdfa362a35 Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:27:06 +0200 Subject: [PATCH 2/3] tests: add TNR --- features/mercure/publish.feature | 33 ++++++++ tests/Behat/MercureContext.php | 77 +++++++++++++++++++ .../Document/Issue5074/MercureWithTopics.php | 36 +++++++++ .../Entity/Issue5074/MercureWithTopics.php | 38 +++++++++ 4 files changed, 184 insertions(+) create mode 100644 features/mercure/publish.feature create mode 100644 tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php diff --git a/features/mercure/publish.feature b/features/mercure/publish.feature new file mode 100644 index 00000000000..ee7e3b7793f --- /dev/null +++ b/features/mercure/publish.feature @@ -0,0 +1,33 @@ +Feature: Mercure publish support + In order to publish an Update to the Mercure hub + As a developer + I need to specify which topics I want to send the Update on + + @createSchema + # see https://github.com/api-platform/core/issues/5074 + Scenario: Checks that Mercure Updates are dispatched properly + Given I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/ld+json" + When I send a "POST" request to "/issue5074/mercure_with_topics" with body: + """ + { + "name": "Hello World!", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + } + """ + 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" + Then 1 Mercure update should have been sent + And the Mercure update should have topics: + | http://example.com/issue5074/mercure_with_topics/1 | + And the Mercure update should have data: + """ + { + "@context": "/contexts/MercureWithTopics", + "@id": "/issue5074/mercure_with_topics/1", + "@type": "MercureWithTopics", + "id": 1, + "name": "Hello World!" + } + """ diff --git a/tests/Behat/MercureContext.php b/tests/Behat/MercureContext.php index c88a62fe4c3..ebcc90a2796 100644 --- a/tests/Behat/MercureContext.php +++ b/tests/Behat/MercureContext.php @@ -15,7 +15,10 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; use Psr\Container\ContainerInterface; +use Symfony\Component\Mercure\Update; /** * Context for Mercure. @@ -28,6 +31,80 @@ public function __construct(private readonly ContainerInterface $driverContainer { } + /** + * @Then :number Mercure updates should have been sent + * @Then :number Mercure update should have been sent + */ + public function mercureUpdatesShouldHaveBeenSent(int $number): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $total = \count($updateHandler->getUpdates()); + + if (0 === $total) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + Assert::assertEquals($number, $total, sprintf('Expected %d Mercure updates to be sent, got %d.', $number, $total)); + } + + /** + * @Then the first Mercure update should have topics: + * @Then the Mercure update should have topics: + */ + public function firstMercureUpdateShouldHaveTopics(TableNode $table): void + { + $this->mercureUpdateShouldHaveTopics(1, $table); + } + + /** + * @Then the first Mercure update should have data: + * @Then the Mercure update should have data: + */ + public function firstMercureUpdateShouldHaveData(PyStringNode $data): void + { + $this->mercureUpdateShouldHaveData(1, $data); + } + + /** + * @Then the Mercure update number :index should have topics: + */ + public function mercureUpdateShouldHaveTopics(int $index, TableNode $table): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $updates = $updateHandler->getUpdates(); + + if (0 === \count($updates)) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + if (!isset($updates[$index - 1])) { + throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index)); + } + /** @var Update $update */ + $update = $updates[$index - 1]; + Assert::assertEquals(array_keys($table->getRowsHash()), array_values($update->getTopics())); + } + + /** + * @Then the Mercure update number :index should have data: + */ + public function mercureUpdateShouldHaveData(int $index, PyStringNode $data): void + { + $updateHandler = $this->driverContainer->get('mercure.hub.default.message_handler'); + $updates = $updateHandler->getUpdates(); + + if (0 === \count($updates)) { + throw new \RuntimeException('No Mercure update has been sent.'); + } + + if (!isset($updates[$index - 1])) { + throw new \RuntimeException(sprintf('Mercure update #%d does not exist.', $index)); + } + /** @var Update $update */ + $update = $updates[$index - 1]; + Assert::assertJsonStringEqualsJsonString($data->getRaw(), $update->getData()); + } + /** * @Then the following Mercure update with topics :topics should have been sent: */ diff --git a/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php b/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php new file mode 100644 index 00000000000..6688909b286 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Issue5074/MercureWithTopics.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Document\Issue5074; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue5074/mercure_with_topics/{id}{._format}'), + new Post(uriTemplate: '/issue5074/mercure_with_topics{._format}'), + ], + mercure: ['topics' => '@=iri(object)'], + extraProperties: ['standard_put' => false] +)] +#[ODM\Document] +class MercureWithTopics +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + public $id; + #[ODM\Field(type: 'string')] + public $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php b/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php new file mode 100644 index 00000000000..acc22f05e95 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5074/MercureWithTopics.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5074; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue5074/mercure_with_topics/{id}{._format}'), + new Post(uriTemplate: '/issue5074/mercure_with_topics{._format}'), + ], + mercure: ['topics' => '@=iri(object)'], + extraProperties: ['standard_put' => false] +)] +#[ORM\Entity] +class MercureWithTopics +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public $id; + #[ORM\Column] + public $name; +} From 26e2bbe96b6aea1a31dfccbab23bd1f1b736592e Mon Sep 17 00:00:00 2001 From: Vincent <407859+vincentchalamon@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:23:37 +0200 Subject: [PATCH 3/3] Update src/Doctrine/EventListener/PublishMercureUpdatesListener.php Co-authored-by: Antoine Bluchet --- .../PublishMercureUpdatesListener.php | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php index 427caf8c737..4bc59e18774 100644 --- a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -245,28 +245,30 @@ private function publishUpdate(object $object, array $options, string $type): vo private function evaluateTopics(array &$options, object $object): void { - if ($options['topics'] ?? false) { - $topics = []; - foreach ((array) $options['topics'] as $topic) { - if (!\is_string($topic)) { - $topics[] = $topic; - continue; - } - - if (!str_starts_with($topic, '@=')) { - $topics[] = $topic; - continue; - } - - if (null === $this->expressionLanguage) { - throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); - } - - $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); + if (!($options['topics'] ?? false)) { + return; + } + + $topics = []; + foreach ((array) $options['topics'] as $topic) { + if (!\is_string($topic)) { + $topics[] = $topic; + continue; } - $options['topics'] = $topics; + if (!str_starts_with($topic, '@=')) { + $topics[] = $topic; + continue; + } + + if (null === $this->expressionLanguage) { + throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); + } + + $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); } + + $options['topics'] = $topics; } /**