Skip to content

Commit e764f2f

Browse files
committed
feat(graphql): reworked how collection subscriptions work, added opt in mechanism
1 parent c9cefd8 commit e764f2f

File tree

5 files changed

+188
-106
lines changed

5 files changed

+188
-106
lines changed

src/GraphQl/Serializer/ItemNormalizer.php

+42
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\GraphQl\State\Provider\NoopProvider;
1717
use ApiPlatform\Metadata\ApiProperty;
1818
use ApiPlatform\Metadata\GraphQl\Query;
19+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
1920
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
2021
use ApiPlatform\Metadata\IriConverterInterface;
2122
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -26,6 +27,7 @@
2627
use ApiPlatform\Metadata\Util\ClassInfoTrait;
2728
use ApiPlatform\Serializer\CacheKeyTrait;
2829
use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer;
30+
use Doctrine\Common\Collections\Collection;
2931
use Psr\Log\LoggerInterface;
3032
use Psr\Log\NullLogger;
3133
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -106,6 +108,11 @@ public function normalize(mixed $object, ?string $format = null, array $context
106108
$data[self::ITEM_IDENTIFIERS_KEY] = $this->identifiersExtractor->getIdentifiersFromItem($object, $context['operation'] ?? null);
107109
}
108110

111+
if ($context['graphql_operation_name'] === 'mercure_subscription' && is_object($object) && isset($data['id']) && !isset($data['_id'])) {
112+
$data['_id'] = $data['id'];
113+
$data['id'] = $this->iriConverter->getIriFromResource($object);
114+
}
115+
109116
return $data;
110117
}
111118

@@ -120,10 +127,45 @@ protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata,
120127
return [...$attributeValue];
121128
}
122129

130+
// Handle relationships for mercure subscriptions
131+
if ($operation instanceof QueryCollection && $context['graphql_operation_name'] === 'mercure_subscription' && $attributeValue instanceof Collection && !$attributeValue->isEmpty()) {
132+
$relationContext = $context;
133+
// Grab collection attributes
134+
$relationContext['attributes'] = $context['attributes']['collection'];
135+
// Iterate over the collection and normalize each item
136+
$data['collection'] = $attributeValue
137+
->map(fn($item) => $this->normalize($item, $format, $relationContext))
138+
// Convert the collection to an array
139+
->toArray();
140+
// Handle pagination if it's enabled in the query
141+
$data = $this->addPagination($attributeValue, $data, $context);
142+
return $data;
143+
}
144+
123145
// to-many are handled directly by the GraphQL resolver
124146
return [];
125147
}
126148

149+
private function addPagination(Collection $collection, array $data, array $context): array
150+
{
151+
if ($context['attributes']['paginationInfo'] ?? false) {
152+
$data['paginationInfo'] = [];
153+
if (array_key_exists('hasNextPage', $context['attributes']['paginationInfo'])) {
154+
$data['paginationInfo']['hasNextPage'] = $collection->count() > ($context['pagination']['itemsPerPage'] ?? 10);
155+
}
156+
if (array_key_exists('itemsPerPage', $context['attributes']['paginationInfo'])) {
157+
$data['paginationInfo']['itemsPerPage'] = $context['pagination']['itemsPerPage'] ?? 10;
158+
}
159+
if (array_key_exists('lastPage', $context['attributes']['paginationInfo'])) {
160+
$data['paginationInfo']['lastPage'] = (int) ceil($collection->count() / ($context['pagination']['itemsPerPage'] ?? 10));
161+
}
162+
if (array_key_exists('totalCount', $context['attributes']['paginationInfo'])) {
163+
$data['paginationInfo']['totalCount'] = $collection->count();
164+
}
165+
}
166+
return $data;
167+
}
168+
127169
/**
128170
* {@inheritdoc}
129171
*/

src/GraphQl/State/Processor/SubscriptionProcessor.php

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\GraphQl\Subscription\OperationAwareSubscriptionManagerInterface;
1818
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface;
1919
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
20+
use ApiPlatform\Metadata\GraphQl\Subscription;
2021
use ApiPlatform\Metadata\Operation;
2122
use ApiPlatform\State\ProcessorInterface;
2223

@@ -49,6 +50,9 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
4950

5051
$hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null;
5152
$data['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub);
53+
if ($operation instanceof Subscription) {
54+
$data['isCollection'] = $operation->isCollection();
55+
}
5256
}
5357

5458
return $data;

src/GraphQl/Subscription/SubscriptionManager.php

+81-52
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function retrieveSubscriptionId(array $context, ?array $result, ?Operatio
5050
if (empty($iri)) {
5151
return null;
5252
}
53+
5354
$options = $operation->getMercure() ?? false;
5455
$private = $options['private'] ?? false;
5556
$privateFields = $options['private_fields'] ?? [];
@@ -59,33 +60,21 @@ public function retrieveSubscriptionId(array $context, ?array $result, ?Operatio
5960
$fields['__private_field_'.$privateField] = $this->getResourceId($privateField, $previousObject);
6061
}
6162
}
62-
$subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri));
63-
$subscriptions = [];
64-
if ($subscriptionsCacheItem->isHit()) {
65-
$subscriptions = $subscriptionsCacheItem->get();
66-
foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
67-
if ($subscriptionFields === $fields) {
68-
return $subscriptionId;
69-
}
70-
}
71-
}
72-
73-
$subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields);
74-
unset($result['clientSubscriptionId']);
75-
if ($private && $privateFields && $previousObject) {
76-
foreach ($options['private_fields'] as $privateField) {
77-
unset($result['__private_field_'.$privateField]);
78-
}
63+
if ($operation->isCollection()) {
64+
$subscriptionId = $this->updateSubscriptionCollectionCacheData(
65+
$iri,
66+
$fields,
67+
);
68+
} else {
69+
$subscriptionId = $this->updateSubscriptionItemCacheData(
70+
$iri,
71+
$fields,
72+
$result,
73+
$private,
74+
$privateFields,
75+
$previousObject
76+
);
7977
}
80-
$subscriptions[] = [$subscriptionId, $fields, $result];
81-
$subscriptionsCacheItem->set($subscriptions);
82-
$this->subscriptionsCache->save($subscriptionsCacheItem);
83-
84-
$this->updateSubscriptionCollectionCacheData(
85-
$iri,
86-
$fields,
87-
$subscriptions,
88-
);
8978

9079
return $subscriptionId;
9180
}
@@ -123,25 +112,9 @@ private function removeItemFromSubscriptionCache(string $iri): void
123112
}
124113
}
125114

126-
private function updateSubscriptionCollectionCacheData(
127-
?string $iri,
128-
array $fields,
129-
array $subscriptions,
130-
): void
115+
private function encodeIriToCacheKey(string $iri): string
131116
{
132-
$subscriptionCollectionCacheItem = $this->subscriptionsCache->getItem(
133-
$this->encodeIriToCacheKey($this->getCollectionIri($iri)),
134-
);
135-
if ($subscriptionCollectionCacheItem->isHit()) {
136-
$collectionSubscriptions = $subscriptionCollectionCacheItem->get();
137-
foreach ($collectionSubscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
138-
if ($subscriptionFields === $fields) {
139-
return;
140-
}
141-
}
142-
}
143-
$subscriptionCollectionCacheItem->set($subscriptions);
144-
$this->subscriptionsCache->save($subscriptionCollectionCacheItem);
117+
return str_replace('/', '_', $iri);
145118
}
146119

147120
private function getResourceId(mixed $privateField, object $previousObject): string
@@ -161,11 +134,11 @@ private function getCollectionIri(string $iri): string
161134
private function getCreatedOrUpdatedPayloads(object $object): array
162135
{
163136
$iri = $this->iriConverter->getIriFromResource($object);
164-
$subscriptions = $this->getSubscriptionsFromIri($iri);
165-
if ($subscriptions === []) {
166-
// Get subscriptions from collection Iri
167-
$subscriptions = $this->getSubscriptionsFromIri($this->getCollectionIri($iri));
168-
}
137+
// Add collection subscriptions
138+
$subscriptions = array_merge(
139+
$this->getSubscriptionsFromIri($this->getCollectionIri($iri)),
140+
$this->getSubscriptionsFromIri($iri)
141+
);
169142

170143
$resourceClass = $this->getObjectClass($object);
171144
$resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
@@ -190,7 +163,7 @@ private function getCreatedOrUpdatedPayloads(object $object): array
190163
}
191164
}
192165
$resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true];
193-
$operation = (new Subscription())->withName('update_subscription')->withShortName($shortName);
166+
$operation = (new Subscription())->withName('mercure_subscription')->withShortName($shortName);
194167
$data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext);
195168

196169
unset($data['clientSubscriptionId']);
@@ -219,8 +192,64 @@ private function getDeletePushPayloads(object $object): array
219192
return $payloads;
220193
}
221194

222-
private function encodeIriToCacheKey(string $iri): string
195+
private function updateSubscriptionItemCacheData(
196+
string $iri,
197+
array $fields,
198+
?array $result,
199+
bool $private,
200+
array $privateFields,
201+
?object $previousObject
202+
): string
223203
{
224-
return str_replace('/', '_', $iri);
204+
$subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri));
205+
$subscriptions = [];
206+
if ($subscriptionsCacheItem->isHit()) {
207+
/*
208+
* @var array<array{string, array<string, string|array>, array<string, string|array>}>
209+
*/
210+
$subscriptions = $subscriptionsCacheItem->get();
211+
foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) {
212+
if ($subscriptionFields === $fields) {
213+
return $subscriptionId;
214+
}
215+
}
216+
}
217+
218+
$subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields);
219+
unset($result['clientSubscriptionId']);
220+
if ($private && $privateFields && $previousObject) {
221+
foreach ($privateFields as $privateField) {
222+
unset($result['__private_field_' . $privateField]);
223+
}
224+
}
225+
$subscriptions[] = [$subscriptionId, $fields, $result];
226+
$subscriptionsCacheItem->set($subscriptions);
227+
$this->subscriptionsCache->save($subscriptionsCacheItem);
228+
return $subscriptionId;
229+
}
230+
231+
232+
233+
private function updateSubscriptionCollectionCacheData(
234+
string $iri,
235+
array $fields,
236+
): string
237+
{
238+
$subscriptionCollectionCacheItem = $this->subscriptionsCache->getItem(
239+
$this->encodeIriToCacheKey($this->getCollectionIri($iri)),
240+
);
241+
if ($subscriptionCollectionCacheItem->isHit()) {
242+
$collectionSubscriptions = $subscriptionCollectionCacheItem->get();
243+
foreach ($collectionSubscriptions as [$subscriptionId, $subscriptionFields]) {
244+
if ($subscriptionFields === $fields) {
245+
return $subscriptionId;
246+
}
247+
}
248+
}
249+
$subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields + ['__collection' => true]);
250+
$subscriptions[] = [$subscriptionId, $fields, []];
251+
$subscriptionCollectionCacheItem->set($subscriptions);
252+
$this->subscriptionsCache->save($subscriptionCollectionCacheItem);
253+
return $subscriptionId;
225254
}
226255
}

src/GraphQl/Type/TypeBuilder.php

+1
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ private function getResourceObjectTypeConfiguration(string $shortName, ResourceM
355355

356356
if ($operation instanceof Subscription) {
357357
$fields['clientSubscriptionId'] = GraphQLType::string();
358+
$fields['isCollection'] = GraphQLType::boolean();
358359
if ($operation->getMercure()) {
359360
$fields['mercureUrl'] = GraphQLType::string();
360361
}

0 commit comments

Comments
 (0)