Skip to content

Commit 46a0291

Browse files
committed
feat(graphql): added support for graphql subscriptions to work for all mutation types
1 parent a3e5e53 commit 46a0291

File tree

4 files changed

+146
-20
lines changed

4 files changed

+146
-20
lines changed

src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,21 @@ final class SubscriptionIdentifierGenerator implements SubscriptionIdentifierGen
2323
public function generateSubscriptionIdentifier(array $fields): string
2424
{
2525
unset($fields['mercureUrl'], $fields['clientSubscriptionId']);
26+
$fields = $this->removeTypename($fields);
2627

2728
return hash('sha256', print_r($fields, true));
2829
}
30+
31+
private function removeTypename(array $data): array
32+
{
33+
foreach ($data as $key => $value) {
34+
if ($key === '__typename') {
35+
unset($data[$key]);
36+
} elseif (is_array($value)) {
37+
$data[$key] = $this->removeTypename($value);
38+
}
39+
}
40+
41+
return $data;
42+
}
2943
}

src/GraphQl/Subscription/SubscriptionManager.php

Lines changed: 128 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,24 @@ public function __construct(private readonly CacheItemPoolInterface $subscriptio
4242

4343
public function retrieveSubscriptionId(array $context, ?array $result, ?Operation $operation = null): ?string
4444
{
45+
4546
/** @var ResolveInfo $info */
4647
$info = $context['info'];
4748
$fields = $info->getFieldSelection(\PHP_INT_MAX);
4849
$this->arrayRecursiveSort($fields, 'ksort');
4950
$iri = $operation ? $this->getIdentifierFromOperation($operation, $context['args'] ?? []) : $this->getIdentifierFromContext($context);
50-
if (null === $iri) {
51+
if (empty($iri)) {
5152
return null;
5253
}
54+
$options = $operation->getMercure() ?? false;
55+
$private = $options['private'] ?? false;
56+
$privateFields = $options['private_fields'] ?? [];
57+
$previousObject = $context['graphql_context']['previous_object'] ?? null;
58+
if ($private && $privateFields && $previousObject) {
59+
foreach ($options['private_fields'] as $privateField) {
60+
$fields['__private_field_'.$privateField] = $this->getResourceId($privateField, $previousObject);
61+
}
62+
}
5363
$subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri));
5464
$subscriptions = [];
5565
if ($subscriptionsCacheItem->isHit()) {
@@ -63,26 +73,129 @@ public function retrieveSubscriptionId(array $context, ?array $result, ?Operatio
6373

6474
$subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields);
6575
unset($result['clientSubscriptionId']);
76+
if ($private && $privateFields && $previousObject) {
77+
foreach ($options['private_fields'] as $privateField) {
78+
unset($result['__private_field_'.$privateField]);
79+
}
80+
}
6681
$subscriptions[] = [$subscriptionId, $fields, $result];
6782
$subscriptionsCacheItem->set($subscriptions);
6883
$this->subscriptionsCache->save($subscriptionsCacheItem);
6984

85+
$this->updateSubscriptionCollectionCacheData(
86+
$iri,
87+
$fields,
88+
$subscriptions,
89+
);
90+
7091
return $subscriptionId;
7192
}
7293

73-
public function getPushPayloads(object $object): array
94+
public function getPushPayloads(object $object, string $type): array
95+
{
96+
if ('delete' === $type) {
97+
$payloads = $this->getDeletePushPayloads($object);
98+
} else {
99+
$payloads = $this->getCreatedOrUpdatedPayloads($object);
100+
}
101+
102+
return $payloads;
103+
}
104+
105+
/**
106+
* @return array<array>
107+
*/
108+
private function getSubscriptionsFromIri(string $iri): array
109+
{
110+
$subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri));
111+
112+
if ($subscriptionsCacheItem->isHit()) {
113+
return $subscriptionsCacheItem->get();
114+
}
115+
116+
return [];
117+
}
118+
119+
private function removeItemFromSubscriptionCache(string $iri): void
120+
{
121+
$cacheKey = $this->encodeIriToCacheKey($iri);
122+
if ($this->subscriptionsCache->hasItem($cacheKey)) {
123+
$this->subscriptionsCache->deleteItem($cacheKey);
124+
}
125+
}
126+
127+
private function encodeIriToCacheKey(string $iri): string
128+
{
129+
return str_replace('/', '_', $iri);
130+
}
131+
132+
private function updateSubscriptionCollectionCacheData(
133+
?string $iri,
134+
array $fields,
135+
array $subscriptions,
136+
): void
137+
{
138+
$subscriptionCollectionCacheItem = $this->subscriptionsCache->getItem(
139+
$this->encodeIriToCacheKey($this->getCollectionIri($iri)),
140+
);
141+
if ($subscriptionCollectionCacheItem->isHit()) {
142+
$collectionSubscriptions = $subscriptionCollectionCacheItem->get();
143+
foreach ($collectionSubscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
144+
if ($subscriptionFields === $fields) {
145+
return;
146+
}
147+
}
148+
}
149+
$subscriptionCollectionCacheItem->set($subscriptions);
150+
$this->subscriptionsCache->save($subscriptionCollectionCacheItem);
151+
}
152+
153+
private function getResourceId(mixed $privateField, object $previousObject): string
154+
{
155+
$id = $previousObject->{'get' . ucfirst($privateField)}()->getId();
156+
if ($id instanceof \Stringable) {
157+
return (string)$id;
158+
}
159+
return $id;
160+
}
161+
162+
private function getCollectionIri(string $iri): string
163+
{
164+
return substr($iri, 0, strrpos($iri, '/'));
165+
}
166+
167+
private function getCreatedOrUpdatedPayloads(object $object): array
74168
{
75169
$iri = $this->iriConverter->getIriFromResource($object);
76170
$subscriptions = $this->getSubscriptionsFromIri($iri);
171+
if ($subscriptions === []) {
172+
// Get subscriptions from collection Iri
173+
$subscriptions = $this->getSubscriptionsFromIri($this->getCollectionIri($iri));
174+
}
77175

78176
$resourceClass = $this->getObjectClass($object);
79177
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
80178
$shortName = $resourceMetadata->getOperation()->getShortName();
81179

180+
$mercure = $resourceMetadata->getOperation()->getMercure() ?? false;
181+
$private = $mercure['private'] ?? false;
182+
$privateFieldsConfig = $mercure['private_fields'] ?? [];
183+
$privateFieldData = [];
184+
if ($private && $privateFieldsConfig) {
185+
foreach ($privateFieldsConfig as $privateField) {
186+
$privateFieldData['__private_field_'.$privateField] = $this->getResourceId($privateField, $object);
187+
}
188+
}
189+
82190
$payloads = [];
83191
foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
192+
if ($privateFieldData) {
193+
$fieldDiff = array_intersect_assoc($subscriptionFields, $privateFieldData);
194+
if ($fieldDiff !== $privateFieldData) {
195+
continue;
196+
}
197+
}
84198
$resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true];
85-
/** @var Operation */
86199
$operation = (new Subscription())->withName('update_subscription')->withShortName($shortName);
87200
$data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext);
88201

@@ -92,26 +205,24 @@ public function getPushPayloads(object $object): array
92205
$payloads[] = [$subscriptionId, $data];
93206
}
94207
}
95-
96208
return $payloads;
97209
}
98210

99-
/**
100-
* @return array<array>
101-
*/
102-
private function getSubscriptionsFromIri(string $iri): array
211+
private function getDeletePushPayloads(object $object): array
103212
{
104-
$subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri));
105-
106-
if ($subscriptionsCacheItem->isHit()) {
107-
return $subscriptionsCacheItem->get();
213+
$iri = $object->id;
214+
$subscriptions = $this->getSubscriptionsFromIri($iri);
215+
if ($subscriptions === []) {
216+
// Get subscriptions from collection Iri
217+
$subscriptions = $this->getSubscriptionsFromIri($this->getCollectionIri($iri));
108218
}
109219

110-
return [];
220+
$payloads = [];
221+
foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
222+
$payloads[] = [$subscriptionId, ['type' => 'delete', 'payload' => $object]];
223+
}
224+
$this->removeItemFromSubscriptionCache($iri);
225+
return $payloads;
111226
}
112227

113-
private function encodeIriToCacheKey(string $iri): string
114-
{
115-
return str_replace('/', '_', $iri);
116-
}
117228
}

src/GraphQl/Subscription/SubscriptionManagerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ interface SubscriptionManagerInterface
2222
{
2323
public function retrieveSubscriptionId(array $context, ?array $result): ?string;
2424

25-
public function getPushPayloads(object $object): array;
25+
public function getPushPayloads(object $object, string $type): array;
2626
}

src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ final class PublishMercureUpdatesListener
5050
'topics' => true,
5151
'data' => true,
5252
'private' => true,
53+
'private_fields' => true,
5354
'id' => true,
5455
'type' => true,
5556
'retry' => true,
@@ -293,11 +294,11 @@ private function evaluateTopics(array &$options, object $object): void
293294
*/
294295
private function getGraphQlSubscriptionUpdates(object $object, array $options, string $type): array
295296
{
296-
if ('update' !== $type || !$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) {
297+
if (!$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) {
297298
return [];
298299
}
299300

300-
$payloads = $this->graphQlSubscriptionManager->getPushPayloads($object);
301+
$payloads = $this->graphQlSubscriptionManager->getPushPayloads($object, $type);
301302

302303
$updates = [];
303304
foreach ($payloads as [$subscriptionId, $data]) {

0 commit comments

Comments
 (0)