Skip to content

Commit 0535c17

Browse files
committed
Support for Mercure 0.10
1 parent 2502e50 commit 0535c17

File tree

3 files changed

+137
-22
lines changed

3 files changed

+137
-22
lines changed

src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,19 @@
3636
*/
3737
final class PublishMercureUpdatesListener
3838
{
39+
private const ALLOWED_KEYS = [
40+
'topics' => true,
41+
'data' => true,
42+
'private' => true,
43+
'id' => true,
44+
'type' => true,
45+
'retry' => true,
46+
];
47+
3948
use DispatchTrait;
4049
use ResourceClassInfoTrait;
4150

4251
private $iriConverter;
43-
private $resourceMetadataFactory;
4452
private $serializer;
4553
private $publisher;
4654
private $expressionLanguage;
@@ -127,60 +135,82 @@ private function storeEntityToPublish($entity, string $property): void
127135
return;
128136
}
129137

130-
$value = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
131-
if (false === $value) {
138+
$options = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
139+
if (false === $options) {
132140
return;
133141
}
134142

135-
if (\is_string($value)) {
143+
if (\is_string($options)) {
136144
if (null === $this->expressionLanguage) {
137145
throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".');
138146
}
139147

140-
$value = $this->expressionLanguage->evaluate($value, ['object' => $entity]);
148+
$options = $this->expressionLanguage->evaluate($options, ['object' => $entity]);
141149
}
142150

143-
if (true === $value) {
144-
$value = [];
151+
if (true === $options) {
152+
$options = [];
145153
}
146154

147-
if (!\is_array($value)) {
148-
throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of targets or a valid expression, "%s" given.', $resourceClass, \gettype($value)));
155+
if (!\is_array($options)) {
156+
throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of options or an expression returning this array, "%s" given.', $resourceClass, \gettype($options)));
157+
}
158+
159+
foreach ($options as $key => $value) {
160+
if (0 === $key) {
161+
if (method_exists(Update::class, 'isPrivate')) {
162+
throw new \InvalidArgumentException('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead or downgrade the Mercure Component to version 0.3');
163+
}
164+
165+
@trigger_error('Targets do not exist anymore since Mercure 0.10. Mark the update as private instead.', E_USER_DEPRECATED);
166+
break;
167+
}
168+
169+
if (!isset(self::ALLOWED_KEYS[$key])) {
170+
throw new InvalidArgumentException(sprintf('The option "%s" set in the "mercure" attribute of the "%s" resource does not exist. Existing options: "%s"', $key, $resourceClass, implode('", "', self::ALLOWED_KEYS)));
171+
}
149172
}
150173

151174
if ('deletedEntities' === $property) {
152175
$this->deletedEntities[(object) [
153176
'id' => $this->iriConverter->getIriFromItem($entity),
154177
'iri' => $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL),
155-
]] = $value;
178+
]] = $options;
156179

157180
return;
158181
}
159182

160-
$this->{$property}[$entity] = $value;
183+
$this->{$property}[$entity] = $options;
161184
}
162185

163186
/**
164187
* @param object $entity
188+
* @param array
165189
*/
166-
private function publishUpdate($entity, array $targets): void
190+
private function publishUpdate($entity, array $options): void
167191
{
168192
if ($entity instanceof \stdClass) {
169193
// By convention, if the entity has been deleted, we send only its IRI
170194
// This may change in the feature, because it's not JSON Merge Patch compliant,
171195
// and I'm not a fond of this approach
172-
$iri = $entity->iri;
196+
$iri = $options['topics'] ?? $entity->iri;
173197
/** @var string $data */
174-
$data = json_encode(['@id' => $entity->id]);
198+
$data = $options['data'] ?? json_encode(['@id' => $entity->id]);
175199
} else {
176200
$resourceClass = $this->getObjectClass($entity);
177201
$context = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('normalization_context', []);
178202

179-
$iri = $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL);
180-
$data = $this->serializer->serialize($entity, key($this->formats), $context);
203+
$iri = $options['topics'] ?? $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL);
204+
$data = $options['data'] ?? $this->serializer->serialize($entity, key($this->formats), $context);
181205
}
182206

183-
$update = new Update($iri, $data, $targets);
207+
if (method_exists(Update::class, 'isPrivate')) {
208+
$update = new Update($iri, $data, $options['private'] ?? false, $options['id'] ?? null, $options['type'] ?? null, $options['retry'] ?? null);
209+
} else {
210+
// Mercure Component < 0.4
211+
// @phpstan-ignore-next-line
212+
$update = new Update($iri, $data, $options);
213+
}
184214
$this->messageBus ? $this->dispatch($update) : ($this->publisher)($update);
185215
}
186216
}

tests/Annotation/ApiResourceTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function testConstruct()
4343
'input' => 'Foo',
4444
'iri' => 'http://example.com/res',
4545
'itemOperations' => ['foo' => ['bar']],
46-
'mercure' => '[\'foo\', object.owner]',
46+
'mercure' => ['private' => true],
4747
'messenger' => true,
4848
'normalizationContext' => ['groups' => ['bar']],
4949
'order' => ['foo', 'bar' => 'ASC'],
@@ -85,7 +85,7 @@ public function testConstruct()
8585
'formats' => ['foo', 'bar' => ['application/bar']],
8686
'filters' => ['foo', 'bar'],
8787
'input' => 'Foo',
88-
'mercure' => '[\'foo\', object.owner]',
88+
'mercure' => ['private' => true],
8989
'messenger' => true,
9090
'normalization_context' => ['groups' => ['bar']],
9191
'order' => ['foo', 'bar' => 'ASC'],

tests/Bridge/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@
3737
*/
3838
class PublishMercureUpdatesListenerTest extends TestCase
3939
{
40-
public function testPublishUpdate()
40+
public function testLegacyPublishUpdate(): void
4141
{
42+
if (method_exists(Update::class, 'isPrivate')) {
43+
$this->markTestSkipped();
44+
}
45+
4246
$toInsert = new Dummy();
4347
$toInsert->setId(1);
4448
$toInsertNotResource = new NotAResource('foo', 'bar');
@@ -115,7 +119,88 @@ public function testPublishUpdate()
115119
$this->assertSame([[], [], [], ['foo', 'bar']], $targets);
116120
}
117121

118-
public function testNoPublisher()
122+
public function testPublishUpdate(): void
123+
{
124+
$toInsert = new Dummy();
125+
$toInsert->setId(1);
126+
$toInsertNotResource = new NotAResource('foo', 'bar');
127+
128+
$toUpdate = new Dummy();
129+
$toUpdate->setId(2);
130+
$toUpdateNoMercureAttribute = new DummyCar();
131+
132+
$toDelete = new Dummy();
133+
$toDelete->setId(3);
134+
$toDeleteExpressionLanguage = new DummyFriend();
135+
$toDeleteExpressionLanguage->setId(4);
136+
137+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
138+
$resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class);
139+
$resourceClassResolverProphecy->getResourceClass(Argument::type(DummyCar::class))->willReturn(DummyCar::class);
140+
$resourceClassResolverProphecy->getResourceClass(Argument::type(DummyFriend::class))->willReturn(DummyFriend::class);
141+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
142+
$resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false);
143+
$resourceClassResolverProphecy->isResourceClass(DummyCar::class)->willReturn(true);
144+
$resourceClassResolverProphecy->isResourceClass(DummyFriend::class)->willReturn(true);
145+
146+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
147+
$iriConverterProphecy->getIriFromItem($toInsert, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/1')->shouldBeCalled();
148+
$iriConverterProphecy->getIriFromItem($toUpdate, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/2')->shouldBeCalled();
149+
$iriConverterProphecy->getIriFromItem($toDelete, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/3')->shouldBeCalled();
150+
$iriConverterProphecy->getIriFromItem($toDelete)->willReturn('/dummies/3')->shouldBeCalled();
151+
$iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage)->willReturn('/dummy_friends/4')->shouldBeCalled();
152+
$iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummy_friends/4')->shouldBeCalled();
153+
154+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
155+
$resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => true, 'normalization_context' => ['groups' => ['foo', 'bar']]]));
156+
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata());
157+
$resourceMetadataFactoryProphecy->create(DummyFriend::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => ['private' => true, 'retry' => 10]]));
158+
159+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
160+
$serializerProphecy->serialize($toInsert, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('1');
161+
$serializerProphecy->serialize($toUpdate, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('2');
162+
163+
$formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']];
164+
165+
$topics = [];
166+
$private = [];
167+
$retry = [];
168+
$publisher = function (Update $update) use (&$topics, &$private, &$retry): string {
169+
$topics = array_merge($topics, $update->getTopics());
170+
$private[] = $update->isPrivate();
171+
$retry[] = $update->getRetry();
172+
173+
return 'id';
174+
};
175+
176+
$listener = new PublishMercureUpdatesListener(
177+
$resourceClassResolverProphecy->reveal(),
178+
$iriConverterProphecy->reveal(),
179+
$resourceMetadataFactoryProphecy->reveal(),
180+
$serializerProphecy->reveal(),
181+
$formats,
182+
null,
183+
$publisher
184+
);
185+
186+
$uowProphecy = $this->prophesize(UnitOfWork::class);
187+
$uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert, $toInsertNotResource])->shouldBeCalled();
188+
$uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate, $toUpdateNoMercureAttribute])->shouldBeCalled();
189+
$uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete, $toDeleteExpressionLanguage])->shouldBeCalled();
190+
191+
$emProphecy = $this->prophesize(EntityManagerInterface::class);
192+
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
193+
$eventArgs = new OnFlushEventArgs($emProphecy->reveal());
194+
195+
$listener->onFlush($eventArgs);
196+
$listener->postFlush();
197+
198+
$this->assertSame(['http://example.com/dummies/1', 'http://example.com/dummies/2', 'http://example.com/dummies/3', 'http://example.com/dummy_friends/4'], $topics);
199+
$this->assertSame([false, false, false, true], $private);
200+
$this->assertSame([null, null, null, 10], $retry);
201+
}
202+
203+
public function testNoPublisher(): void
119204
{
120205
$this->expectException(InvalidArgumentException::class);
121206
$this->expectExceptionMessage('A message bus or a publisher must be provided.');
@@ -134,7 +219,7 @@ public function testNoPublisher()
134219
public function testInvalidMercureAttribute()
135220
{
136221
$this->expectException(InvalidArgumentException::class);
137-
$this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of targets or a valid expression, "integer" given.');
222+
$this->expectExceptionMessage('The value of the "mercure" attribute of the "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" resource class must be a boolean, an array of options or an expression returning this array, "integer" given.');
138223

139224
$toInsert = new Dummy();
140225

0 commit comments

Comments
 (0)