Skip to content

Commit ad0d2b1

Browse files
authored
Support for Mercure 0.10 (#3584)
* Support for Mercure 0.10 * Fix linters * Fix testPublishUpdate
1 parent 2d0d948 commit ad0d2b1

File tree

3 files changed

+144
-23
lines changed

3 files changed

+144
-23
lines changed

src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php

Lines changed: 49 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,84 @@ 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
165188
*/
166-
private function publishUpdate($entity, array $targets): void
189+
private function publishUpdate($entity, array $options): void
167190
{
168191
if ($entity instanceof \stdClass) {
169192
// By convention, if the entity has been deleted, we send only its IRI
170193
// This may change in the feature, because it's not JSON Merge Patch compliant,
171194
// and I'm not a fond of this approach
172-
$iri = $entity->iri;
195+
$iri = $options['topics'] ?? $entity->iri;
173196
/** @var string $data */
174-
$data = json_encode(['@id' => $entity->id]);
197+
$data = $options['data'] ?? json_encode(['@id' => $entity->id]);
175198
} else {
176199
$resourceClass = $this->getObjectClass($entity);
177200
$context = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('normalization_context', []);
178201

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

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

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: 93 additions & 4 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');
@@ -84,7 +88,7 @@ public function testPublishUpdate()
8488
$targets = [];
8589
$publisher = function (Update $update) use (&$topics, &$targets): string {
8690
$topics = array_merge($topics, $update->getTopics());
87-
$targets[] = $update->getTargets();
91+
$targets[] = $update->getTargets(); // @phpstan-ignore-line
8892

8993
return 'id';
9094
};
@@ -115,7 +119,92 @@ public function testPublishUpdate()
115119
$this->assertSame([[], [], [], ['foo', 'bar']], $targets);
116120
}
117121

118-
public function testNoPublisher()
122+
public function testPublishUpdate(): void
123+
{
124+
if (!method_exists(Update::class, 'isPrivate')) {
125+
$this->markTestSkipped();
126+
}
127+
128+
$toInsert = new Dummy();
129+
$toInsert->setId(1);
130+
$toInsertNotResource = new NotAResource('foo', 'bar');
131+
132+
$toUpdate = new Dummy();
133+
$toUpdate->setId(2);
134+
$toUpdateNoMercureAttribute = new DummyCar();
135+
136+
$toDelete = new Dummy();
137+
$toDelete->setId(3);
138+
$toDeleteExpressionLanguage = new DummyFriend();
139+
$toDeleteExpressionLanguage->setId(4);
140+
141+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
142+
$resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class);
143+
$resourceClassResolverProphecy->getResourceClass(Argument::type(DummyCar::class))->willReturn(DummyCar::class);
144+
$resourceClassResolverProphecy->getResourceClass(Argument::type(DummyFriend::class))->willReturn(DummyFriend::class);
145+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
146+
$resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false);
147+
$resourceClassResolverProphecy->isResourceClass(DummyCar::class)->willReturn(true);
148+
$resourceClassResolverProphecy->isResourceClass(DummyFriend::class)->willReturn(true);
149+
150+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
151+
$iriConverterProphecy->getIriFromItem($toInsert, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/1')->shouldBeCalled();
152+
$iriConverterProphecy->getIriFromItem($toUpdate, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/2')->shouldBeCalled();
153+
$iriConverterProphecy->getIriFromItem($toDelete, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummies/3')->shouldBeCalled();
154+
$iriConverterProphecy->getIriFromItem($toDelete)->willReturn('/dummies/3')->shouldBeCalled();
155+
$iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage)->willReturn('/dummy_friends/4')->shouldBeCalled();
156+
$iriConverterProphecy->getIriFromItem($toDeleteExpressionLanguage, UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/dummy_friends/4')->shouldBeCalled();
157+
158+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
159+
$resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => true, 'normalization_context' => ['groups' => ['foo', 'bar']]]));
160+
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata());
161+
$resourceMetadataFactoryProphecy->create(DummyFriend::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['mercure' => ['private' => true, 'retry' => 10]]));
162+
163+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
164+
$serializerProphecy->serialize($toInsert, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('1');
165+
$serializerProphecy->serialize($toUpdate, 'jsonld', ['groups' => ['foo', 'bar']])->willReturn('2');
166+
167+
$formats = ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']];
168+
169+
$topics = [];
170+
$private = [];
171+
$retry = [];
172+
$publisher = function (Update $update) use (&$topics, &$private, &$retry): string {
173+
$topics = array_merge($topics, $update->getTopics());
174+
$private[] = $update->isPrivate();
175+
$retry[] = $update->getRetry();
176+
177+
return 'id';
178+
};
179+
180+
$listener = new PublishMercureUpdatesListener(
181+
$resourceClassResolverProphecy->reveal(),
182+
$iriConverterProphecy->reveal(),
183+
$resourceMetadataFactoryProphecy->reveal(),
184+
$serializerProphecy->reveal(),
185+
$formats,
186+
null,
187+
$publisher
188+
);
189+
190+
$uowProphecy = $this->prophesize(UnitOfWork::class);
191+
$uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert, $toInsertNotResource])->shouldBeCalled();
192+
$uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate, $toUpdateNoMercureAttribute])->shouldBeCalled();
193+
$uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete, $toDeleteExpressionLanguage])->shouldBeCalled();
194+
195+
$emProphecy = $this->prophesize(EntityManagerInterface::class);
196+
$emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
197+
$eventArgs = new OnFlushEventArgs($emProphecy->reveal());
198+
199+
$listener->onFlush($eventArgs);
200+
$listener->postFlush();
201+
202+
$this->assertSame(['http://example.com/dummies/1', 'http://example.com/dummies/2', 'http://example.com/dummies/3', 'http://example.com/dummy_friends/4'], $topics);
203+
$this->assertSame([false, false, false, true], $private);
204+
$this->assertSame([null, null, null, 10], $retry);
205+
}
206+
207+
public function testNoPublisher(): void
119208
{
120209
$this->expectException(InvalidArgumentException::class);
121210
$this->expectExceptionMessage('A message bus or a publisher must be provided.');
@@ -134,7 +223,7 @@ public function testNoPublisher()
134223
public function testInvalidMercureAttribute()
135224
{
136225
$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.');
226+
$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.');
138227

139228
$toInsert = new Dummy();
140229

0 commit comments

Comments
 (0)