Skip to content

Commit

Permalink
feat(symfony): add getOperation Expression Language function on Mercu…
Browse files Browse the repository at this point in the history
…re topics
  • Loading branch information
vincentchalamon committed Feb 5, 2024
1 parent 8a35ee2 commit 74b576d
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 3 deletions.
27 changes: 27 additions & 0 deletions features/mercure/publish.feature
Expand Up @@ -31,3 +31,30 @@ Feature: Mercure publish support
"name": "Hello World!"
}
"""

Scenario: Checks that Mercure Updates are dispatched following topics configured with expression language
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 "/mercure_with_topics_and_get_operations" with body:
"""
{
"name": "Hello World!"
}
"""
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/mercure_with_topics_and_get_operations/1 |
| http://example.com/custom_resource/mercure_with_topics_and_get_operations/1 |
And the Mercure update should have data:
"""
{
"@context": "/contexts/MercureWithTopicsAndGetOperation",
"@id": "/mercure_with_topics_and_get_operations/1",
"@type": "MercureWithTopicsAndGetOperation",
"id": 1,
"name": "Hello World!"
}
"""
6 changes: 5 additions & 1 deletion src/Doctrine/EventListener/PublishMercureUpdatesListener.php
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface as GraphQlSubscriptionManagerInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
Expand Down Expand Up @@ -84,7 +85,10 @@ public function __construct(LegacyResourceClassResolverInterface|ResourceClassRe
$this->expressionLanguage->addFunction($rawurlencode);

$this->expressionLanguage->addFunction(
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))
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))
);
$this->expressionLanguage->addFunction(
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))
);
}

Expand Down
119 changes: 119 additions & 0 deletions tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
Expand All @@ -30,6 +31,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMercure;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyOffer;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MercureWithTopicsAndGetOperation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\UnitOfWork;
Expand Down Expand Up @@ -169,6 +171,123 @@ public function testPublishUpdate(): void
$this->assertEquals([null, null, null, null, null, 10, null], $retry);
}

public function testPublishUpdateMultipleTopicsUsingExpressionLanguage(): void
{
$mercure = [
'topics' => [
'@=iri(object)',
'@=iri(object, '.UrlGeneratorInterface::ABS_PATH.')',
'@=iri(object, '.UrlGeneratorInterface::ABS_URL.', getOperation(object, "/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}"))',
],
];

$toInsert = new MercureWithTopicsAndGetOperation();
$toInsert->id = 1;
$toInsert->name = 'Hello World!';

$toUpdate = new MercureWithTopicsAndGetOperation();
$toUpdate->id = 2;
$toUpdate->name = 'Hello World!';

$toDelete = new MercureWithTopicsAndGetOperation();
$toDelete->id = 3;
$toDelete->name = 'Hello World!';

// Even if it's the Post operation which sends Updates to Mercure,
// the `mercure` configuration is retrieved from the first operation
// of the resource because the Doctrine Listener doesn't have a
// reference to the operation.
$getOperation = (new Get())->withMercure($mercure)->withShortName('MercureWithTopicsAndGetOperation');
$customGetOperation = (new Get(uriTemplate: '/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}'));
$postOperation = (new Post());

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass(Argument::type(MercureWithTopicsAndGetOperation::class))->willReturn(MercureWithTopicsAndGetOperation::class);
$resourceClassResolverProphecy->isResourceClass(MercureWithTopicsAndGetOperation::class)->willReturn(true);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);

$iriConverterProphecy->getIriFromResource($toInsert, UrlGeneratorInterface::ABS_URL, null)->willReturn('http://example.com/mercure_with_topics_and_get_operations/1')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toInsert, UrlGeneratorInterface::ABS_PATH, null)->willReturn('/mercure_with_topics_and_get_operations/1')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toInsert, UrlGeneratorInterface::ABS_URL, Argument::exact($customGetOperation))->willReturn('http://example.com/custom_resource/mercure_with_topics_and_get_operations/1')->shouldBeCalled();

$iriConverterProphecy->getIriFromResource($toUpdate, UrlGeneratorInterface::ABS_URL, null)->willReturn('http://example.com/mercure_with_topics_and_get_operations/2')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toUpdate, UrlGeneratorInterface::ABS_PATH, null)->willReturn('/mercure_with_topics_and_get_operations/2')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toUpdate, UrlGeneratorInterface::ABS_URL, Argument::exact($customGetOperation))->willReturn('http://example.com/custom_resource/mercure_with_topics_and_get_operations/2')->shouldBeCalled();

$iriConverterProphecy->getIriFromResource($toDelete)->willReturn('/mercure_with_topics_and_get_operations/3')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toDelete, UrlGeneratorInterface::ABS_URL, null)->willReturn('http://example.com/mercure_with_topics_and_get_operations/3')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toDelete, UrlGeneratorInterface::ABS_PATH, null)->willReturn('/mercure_with_topics_and_get_operations/3')->shouldBeCalled();
$iriConverterProphecy->getIriFromResource($toDelete, UrlGeneratorInterface::ABS_URL, Argument::exact($customGetOperation))->willReturn('http://example.com/custom_resource/mercure_with_topics_and_get_operations/3')->shouldBeCalled();

$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);

$resourceMetadataFactoryProphecy->create(MercureWithTopicsAndGetOperation::class)->willReturn(new ResourceMetadataCollection(MercureWithTopicsAndGetOperation::class, [
(new ApiResource())->withOperations(new Operations([
'get' => $getOperation,
'custom_get' => $customGetOperation,
'post' => $postOperation,
])),
]))->shouldBeCalled();

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->serialize($toInsert, 'jsonld', [])->willReturn('{"@type":"MercureWithTopicsAndGetOperation","@id":"/mercure_with_topics_and_get_operations/1","id":1,"name":"Hello World!"}')->shouldBeCalled();
$serializerProphecy->serialize($toUpdate, 'jsonld', [])->willReturn('{"@type":"MercureWithTopicsAndGetOperation","@id":"/mercure_with_topics_and_get_operations/2","id":2,"name":"Hello World!"}')->shouldBeCalled();

$formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']];

$topics = [];
$private = [];
$retry = [];
$data = [];

$defaultHub = $this->createMockHub(function (Update $update) use (&$topics, &$private, &$retry, &$data): string {
$topics = array_merge($topics, $update->getTopics());
$private[] = $update->isPrivate();
$retry[] = $update->getRetry();
$data[] = $update->getData();

return 'id';
});

$listener = new PublishMercureUpdatesListener(
$resourceClassResolverProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceMetadataFactoryProphecy->reveal(),
$serializerProphecy->reveal(),
$formats,
null,
new HubRegistry($defaultHub, ['default' => $defaultHub]),
null,
null,
null,
true,
);

$uowProphecy = $this->prophesize(UnitOfWork::class);
$uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert])->shouldBeCalled();
$uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate])->shouldBeCalled();
$uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete])->shouldBeCalled();

$emProphecy = $this->prophesize(EntityManagerInterface::class);
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
$eventArgs = new OnFlushEventArgs($emProphecy->reveal());

$listener->onFlush($eventArgs);
$listener->postFlush();

$this->assertEquals([
'{"@type":"MercureWithTopicsAndGetOperation","@id":"/mercure_with_topics_and_get_operations/1","id":1,"name":"Hello World!"}',
'{"@type":"MercureWithTopicsAndGetOperation","@id":"/mercure_with_topics_and_get_operations/2","id":2,"name":"Hello World!"}',
'{"@id":"\/mercure_with_topics_and_get_operations\/3","@type":"MercureWithTopicsAndGetOperation"}',
], $data);
$this->assertEquals([
'http://example.com/mercure_with_topics_and_get_operations/1', '/mercure_with_topics_and_get_operations/1', 'http://example.com/custom_resource/mercure_with_topics_and_get_operations/1',
'http://example.com/mercure_with_topics_and_get_operations/2', '/mercure_with_topics_and_get_operations/2', 'http://example.com/custom_resource/mercure_with_topics_and_get_operations/2',
'http://example.com/mercure_with_topics_and_get_operations/3', '/mercure_with_topics_and_get_operations/3', 'http://example.com/custom_resource/mercure_with_topics_and_get_operations/3',
], $topics);
}

public function testPublishGraphQlUpdates(): void
{
$toUpdate = new Dummy();
Expand Down
@@ -0,0 +1,42 @@
<?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.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Document;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

#[ApiResource(
operations: [
new Get(uriTemplate: '/mercure_with_topics_and_get_operations/{id}{._format}'),
new Post(uriTemplate: '/mercure_with_topics_and_get_operations{._format}'),
new Get(uriTemplate: '/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}'),
],
mercure: [
'topics' => [
'@=iri(object)',
'@=iri(object, '.UrlGeneratorInterface::ABS_URL.', getOperation(object, "/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}"))',
],
]
)]
#[ODM\Document]
class MercureWithTopicsAndGetOperation
{
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
public $id;
#[ODM\Field(type: 'string')]
public $name;
}
@@ -0,0 +1,44 @@
<?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.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use Doctrine\ORM\Mapping as ORM;

#[ApiResource(
operations: [
new Get(uriTemplate: '/mercure_with_topics_and_get_operations/{id}{._format}'),
new Post(uriTemplate: '/mercure_with_topics_and_get_operations{._format}'),
new Get(uriTemplate: '/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}'),
],
mercure: [
'topics' => [
'@=iri(object)',
'@=iri(object, '.UrlGeneratorInterface::ABS_URL.', getOperation(object, "/custom_resource/mercure_with_topics_and_get_operations/{id}{._format}"))',
],
]
)]
#[ORM\Entity]
class MercureWithTopicsAndGetOperation
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public $id;
#[ORM\Column]
public $name;
}
3 changes: 1 addition & 2 deletions tests/Symfony/Bundle/Test/ApiTestCaseTest.php
Expand Up @@ -245,8 +245,7 @@ public function testFindIriBy(): void
*/
public function testGetMercureMessages(): void
{
// debug mode is required to get Mercure TraceableHub
$this->recreateSchema(['debug' => true, 'environment' => 'mercure']);
$this->recreateSchema(['environment' => 'mercure']);

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

0 comments on commit 74b576d

Please sign in to comment.