diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php index 06ea9df299a..923ccd2dfe8 100644 --- a/src/GraphQl/Serializer/ItemNormalizer.php +++ b/src/GraphQl/Serializer/ItemNormalizer.php @@ -16,6 +16,7 @@ use ApiPlatform\GraphQl\State\Provider\NoopProvider; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -26,6 +27,7 @@ use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer; +use Doctrine\Common\Collections\Collection; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -106,6 +108,11 @@ public function normalize(mixed $object, ?string $format = null, array $context $data[self::ITEM_IDENTIFIERS_KEY] = $this->identifiersExtractor->getIdentifiersFromItem($object, $context['operation'] ?? null); } + if (isset($context['graphql_operation_name']) && 'mercure_subscription' === $context['graphql_operation_name'] && \is_object($object) && isset($data['id']) && !isset($data['_id'])) { + $data['_id'] = $data['id']; + $data['id'] = $this->iriConverter->getIriFromResource($object); + } + return $data; } @@ -120,10 +127,46 @@ protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, return [...$attributeValue]; } + // Handle relationships for mercure subscriptions + if ($operation instanceof QueryCollection && 'mercure_subscription' === $context['graphql_operation_name'] && $attributeValue instanceof Collection && !$attributeValue->isEmpty()) { + $relationContext = $context; + // Grab collection attributes + $relationContext['attributes'] = $context['attributes']['collection']; + // Iterate over the collection and normalize each item + $data['collection'] = $attributeValue + ->map(fn ($item) => $this->normalize($item, $format, $relationContext)) + // Convert the collection to an array + ->toArray(); + + // Handle pagination if it's enabled in the query + return $this->addPagination($attributeValue, $data, $context); + } + // to-many are handled directly by the GraphQL resolver return []; } + private function addPagination(Collection $collection, array $data, array $context): array + { + if ($context['attributes']['paginationInfo'] ?? false) { + $data['paginationInfo'] = []; + if (\array_key_exists('hasNextPage', $context['attributes']['paginationInfo'])) { + $data['paginationInfo']['hasNextPage'] = $collection->count() > ($context['pagination']['itemsPerPage'] ?? 10); + } + if (\array_key_exists('itemsPerPage', $context['attributes']['paginationInfo'])) { + $data['paginationInfo']['itemsPerPage'] = $context['pagination']['itemsPerPage'] ?? 10; + } + if (\array_key_exists('lastPage', $context['attributes']['paginationInfo'])) { + $data['paginationInfo']['lastPage'] = (int) ceil($collection->count() / ($context['pagination']['itemsPerPage'] ?? 10)); + } + if (\array_key_exists('totalCount', $context['attributes']['paginationInfo'])) { + $data['paginationInfo']['totalCount'] = $collection->count(); + } + } + + return $data; + } + /** * {@inheritdoc} */ diff --git a/src/GraphQl/State/Processor/SubscriptionProcessor.php b/src/GraphQl/State/Processor/SubscriptionProcessor.php index d4389499221..1c6ae28cac2 100644 --- a/src/GraphQl/State/Processor/SubscriptionProcessor.php +++ b/src/GraphQl/State/Processor/SubscriptionProcessor.php @@ -17,6 +17,7 @@ use ApiPlatform\GraphQl\Subscription\OperationAwareSubscriptionManagerInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; @@ -49,6 +50,9 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null; $data['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub); + if ($operation instanceof Subscription) { + $data['isCollection'] = $operation->isCollection(); + } } return $data; diff --git a/src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php b/src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php index 44afd26aa95..76c5ee248ba 100644 --- a/src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php +++ b/src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php @@ -23,7 +23,21 @@ final class SubscriptionIdentifierGenerator implements SubscriptionIdentifierGen public function generateSubscriptionIdentifier(array $fields): string { unset($fields['mercureUrl'], $fields['clientSubscriptionId']); + $fields = $this->removeTypename($fields); return hash('sha256', print_r($fields, true)); } + + private function removeTypename(array $data): array + { + foreach ($data as $key => $value) { + if ('__typename' === $key) { + unset($data[$key]); + } elseif (\is_array($value)) { + $data[$key] = $this->removeTypename($value); + } + } + + return $data; + } } diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index 8e2532aa33e..7f09eba0973 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -42,48 +42,140 @@ public function __construct(private readonly CacheItemPoolInterface $subscriptio public function retrieveSubscriptionId(array $context, ?array $result, ?Operation $operation = null): ?string { + $iri = $operation ? $this->getIdentifierFromOperation($operation, $context['args'] ?? []) : $this->getIdentifierFromContext($context); + if (empty($iri)) { + return null; + } + /** @var ResolveInfo $info */ $info = $context['info']; $fields = $info->getFieldSelection(\PHP_INT_MAX); $this->arrayRecursiveSort($fields, 'ksort'); - $iri = $operation ? $this->getIdentifierFromOperation($operation, $context['args'] ?? []) : $this->getIdentifierFromContext($context); - if (null === $iri) { - return null; + + $options = $operation ? ($operation->getMercure() ?? false) : false; + $private = $options['private'] ?? false; + $privateFields = $options['private_fields'] ?? []; + $previousObject = $context['graphql_context']['previous_object'] ?? null; + $privateFieldData = []; + if ($private && $privateFields && $previousObject) { + foreach ($options['private_fields'] as $privateField) { + $fieldData = $this->getResourceId($privateField, $previousObject); + $fields['__private_field_'.$privateField] = $fieldData; + $privateFieldData[] = $fieldData; + } } - $subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri)); - $subscriptions = []; + if ($operation instanceof Subscription && $operation->isCollection()) { + $subscriptionId = $this->updateSubscriptionCollectionCacheData( + $iri, + $fields, + $privateFieldData + ); + } else { + $subscriptionId = $this->updateSubscriptionItemCacheData( + $iri, + $fields, + $result, + $private, + $privateFields, + $previousObject + ); + } + + return $subscriptionId; + } + + public function getPushPayloads(object $object, string $type): array + { + if ('delete' === $type) { + $payloads = $this->getDeletePushPayloads($object); + } else { + $payloads = $this->getCreatedOrUpdatedPayloads($object); + } + + return $payloads; + } + + /** + * @return array + */ + private function getSubscriptionsFromIri(string $iri, array $fields = []): array + { + $subscriptionsCacheItem = $this->subscriptionsCache->getItem( + $this->generatePrivateCacheKeyPart( + $this->encodeIriToCacheKey($iri), + $fields + ) + + ); + if ($subscriptionsCacheItem->isHit()) { - $subscriptions = $subscriptionsCacheItem->get(); - foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) { - if ($subscriptionFields === $fields) { - return $subscriptionId; - } - } + return $subscriptionsCacheItem->get(); } - $subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields); - unset($result['clientSubscriptionId']); - $subscriptions[] = [$subscriptionId, $fields, $result]; - $subscriptionsCacheItem->set($subscriptions); - $this->subscriptionsCache->save($subscriptionsCacheItem); + return []; + } - return $subscriptionId; + private function removeItemFromSubscriptionCache(string $iri): void + { + $cacheKey = $this->encodeIriToCacheKey($iri); + if ($this->subscriptionsCache->hasItem($cacheKey)) { + $this->subscriptionsCache->deleteItem($cacheKey); + } } - public function getPushPayloads(object $object): array + private function encodeIriToCacheKey(string $iri): string { - $iri = $this->iriConverter->getIriFromResource($object); - $subscriptions = $this->getSubscriptionsFromIri($iri); + return str_replace('/', '_', $iri); + } + + private function getResourceId(mixed $privateField, object $previousObject): string + { + $id = $previousObject->{'get'.ucfirst($privateField)}()->getId(); + if ($id instanceof \Stringable) { + return (string) $id; + } + + return $id; + } + + private function getCollectionIri(string $iri): string + { + return substr($iri, 0, strrpos($iri, '/')); + } + private function getCreatedOrUpdatedPayloads(object $object): array + { $resourceClass = $this->getObjectClass($object); $resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass); $shortName = $resourceMetadata->getOperation()->getShortName(); + $mercure = $resourceMetadata->getOperation()->getMercure() ?? false; + $private = $mercure['private'] ?? false; + $privateFieldsConfig = $mercure['private_fields'] ?? []; + $privateFieldData = []; + if ($private && $privateFieldsConfig) { + foreach ($privateFieldsConfig as $privateField) { + $privateFieldData['__private_field_'.$privateField] = $this->getResourceId($privateField, $object); + } + } + + $iri = $this->iriConverter->getIriFromResource($object); + // Add collection subscriptions + $subscriptions = array_merge( + $this->getSubscriptionsFromIri($this->getCollectionIri($iri), $privateFieldData), + $this->getSubscriptionsFromIri($iri) + ); + $payloads = []; foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) { + if ($privateFieldData) { + $fieldDiff = array_intersect_assoc($subscriptionFields, $privateFieldData); + if ($fieldDiff !== $privateFieldData) { + continue; + } + } $resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - /** @var Operation */ - $operation = (new Subscription())->withName('update_subscription')->withShortName($shortName); + $operation = (new Subscription())->withName('mercure_subscription')->withShortName($shortName); $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); unset($data['clientSubscriptionId']); @@ -96,22 +188,97 @@ public function getPushPayloads(object $object): array return $payloads; } - /** - * @return array - */ - private function getSubscriptionsFromIri(string $iri): array + private function getDeletePushPayloads(object $object): array { - $subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri)); + $iri = $object->id; + $subscriptions = array_merge( + $this->getSubscriptionsFromIri($iri), + $this->getSubscriptionsFromIri($this->getCollectionIri($iri), $object->private), + ); + $payloads = []; + $payload = ['type' => 'delete', 'payload' => ['id' => $object->id, 'iri' => $object->iri, 'type' => $object->type]]; + foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) { + $payloads[] = [$subscriptionId, $payload]; + } + $this->removeItemFromSubscriptionCache($iri); + + return $payloads; + } + + private function updateSubscriptionItemCacheData( + string $iri, + array $fields, + ?array $result, + bool $private, + array $privateFields, + ?object $previousObject, + ): string { + $subscriptionsCacheItem = $this->subscriptionsCache->getItem($this->encodeIriToCacheKey($iri)); + $subscriptions = []; if ($subscriptionsCacheItem->isHit()) { - return $subscriptionsCacheItem->get(); + /* + * @var array, array}> + */ + $subscriptions = $subscriptionsCacheItem->get(); + foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) { + if ($subscriptionFields === $fields) { + return $subscriptionId; + } + } } - return []; + unset($result['clientSubscriptionId']); + if ($private && $privateFields && $previousObject) { + $subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields); + foreach ($privateFields as $privateField) { + unset($result['__private_field_'.$privateField]); + } + } else { + $subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields); + } + $subscriptions[] = [$subscriptionId, $fields, $result]; + $subscriptionsCacheItem->set($subscriptions); + $this->subscriptionsCache->save($subscriptionsCacheItem); + + return $subscriptionId; } - private function encodeIriToCacheKey(string $iri): string + private function updateSubscriptionCollectionCacheData( + string $iri, + array $fields, + array $privateFieldData, + ): string { + + $subscriptionCollectionCacheItem = $this->subscriptionsCache->getItem( + $this->generatePrivateCacheKeyPart( + $this->encodeIriToCacheKey($this->getCollectionIri($iri)), + $privateFieldData + ), + ); + $collectionSubscriptions = []; + if ($subscriptionCollectionCacheItem->isHit()) { + $collectionSubscriptions = $subscriptionCollectionCacheItem->get(); + foreach ($collectionSubscriptions as [$subscriptionId, $subscriptionFields, $result]) { + if ($subscriptionFields === $fields) { + return $subscriptionId; + } + } + } + $subscriptionId = $this->subscriptionIdentifierGenerator->generateSubscriptionIdentifier($fields + ['__collection' => true]); + $collectionSubscriptions[] = [$subscriptionId, $fields, []]; + $subscriptionCollectionCacheItem->set($collectionSubscriptions); + $this->subscriptionsCache->save($subscriptionCollectionCacheItem); + + return $subscriptionId; + } + + private function generatePrivateCacheKeyPart(string $iriKey, array $fields = []): string { - return str_replace('/', '_', $iri); + if (empty($fields)) { + return $iriKey; + } + return $iriKey.'_'.implode('_', $fields); } + } diff --git a/src/GraphQl/Subscription/SubscriptionManagerInterface.php b/src/GraphQl/Subscription/SubscriptionManagerInterface.php index 4064f068010..91c89049481 100644 --- a/src/GraphQl/Subscription/SubscriptionManagerInterface.php +++ b/src/GraphQl/Subscription/SubscriptionManagerInterface.php @@ -22,5 +22,5 @@ interface SubscriptionManagerInterface { public function retrieveSubscriptionId(array $context, ?array $result): ?string; - public function getPushPayloads(object $object): array; + public function getPushPayloads(object $object, string $type): array; } diff --git a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php index 7afeaeaef03..d895c1c1024 100644 --- a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php +++ b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php @@ -183,9 +183,11 @@ public function testGetPushPayloadsNoHit(): void $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); $cacheItemProphecy->isHit()->willReturn(false); + $cacheItemProphecy->isHit()->willReturn(false); $this->subscriptionsCacheProphecy->getItem('_dummies_2')->willReturn($cacheItemProphecy->reveal()); + $this->subscriptionsCacheProphecy->getItem('_dummies')->willReturn($cacheItemProphecy->reveal()); - $this->assertEquals([], $this->subscriptionManager->getPushPayloads($object)); + $this->assertEquals([], $this->subscriptionManager->getPushPayloads($object, 'update')); } public function testGetPushPayloadsHit(): void @@ -204,11 +206,18 @@ public function testGetPushPayloadsHit(): void ['subscriptionIdFoo', ['fieldsFoo'], ['resultFoo']], ['subscriptionIdBar', ['fieldsBar'], ['resultBar']], ]); + $cacheItemProphecyCollection = $this->prophesize(CacheItemInterface::class); + $cacheItemProphecyCollection->isHit()->willReturn(true); + $cacheItemProphecyCollection->get()->willReturn([ + ['subscriptionIdFoo', ['fieldsFoo'], []], + ['subscriptionIdBar', ['fieldsBar'], []], + ]); $this->subscriptionsCacheProphecy->getItem('_dummies_2')->willReturn($cacheItemProphecy->reveal()); + $this->subscriptionsCacheProphecy->getItem('_dummies')->willReturn($cacheItemProphecyCollection->reveal()); $this->normalizeProcessor->process( $object, - (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + (new Subscription())->withName('mercure_subscription')->withShortName('Dummy'), [], ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] )->willReturn( @@ -217,13 +226,13 @@ public function testGetPushPayloadsHit(): void $this->normalizeProcessor->process( $object, - (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + (new Subscription())->withName('mercure_subscription')->withShortName('Dummy'), [], ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] )->willReturn( ['resultBar', 'clientSubscriptionId' => 'client-subscription-id'] ); - $this->assertEquals([['subscriptionIdFoo', ['newResultFoo']]], $this->subscriptionManager->getPushPayloads($object)); + $this->assertEquals([['subscriptionIdFoo', ['newResultFoo']], ['subscriptionIdFoo', ['newResultFoo']]], $this->subscriptionManager->getPushPayloads($object, 'update')); } } diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 28680f54eee..0c74502c63b 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -355,6 +355,7 @@ private function getResourceObjectTypeConfiguration(string $shortName, ResourceM if ($operation instanceof Subscription) { $fields['clientSubscriptionId'] = GraphQLType::string(); + $fields['isCollection'] = GraphQLType::boolean(); if ($operation->getMercure()) { $fields['mercureUrl'] = GraphQLType::string(); } diff --git a/src/Metadata/GraphQl/Subscription.php b/src/Metadata/GraphQl/Subscription.php index 59a66fab0c7..31637c62266 100644 --- a/src/Metadata/GraphQl/Subscription.php +++ b/src/Metadata/GraphQl/Subscription.php @@ -76,63 +76,69 @@ public function __construct( mixed $rules = null, ?string $policy = null, array $extraProperties = [], + protected bool $collection = false, ) { parent::__construct( - resolver: $resolver, - args: $args, - extraArgs: $extraArgs, - links: $links, - securityAfterResolver: $securityAfterResolver, - securityMessageAfterResolver: $securityMessageAfterResolver, - shortName: $shortName, - class: $class, - paginationEnabled: $paginationEnabled, - paginationType: $paginationType, - paginationItemsPerPage: $paginationItemsPerPage, - paginationMaximumItemsPerPage: $paginationMaximumItemsPerPage, - paginationPartial: $paginationPartial, - paginationClientEnabled: $paginationClientEnabled, - paginationClientItemsPerPage: $paginationClientItemsPerPage, - paginationClientPartial: $paginationClientPartial, - paginationFetchJoinCollection: $paginationFetchJoinCollection, - paginationUseOutputWalkers: $paginationUseOutputWalkers, - order: $order, - description: $description, - normalizationContext: $normalizationContext, - denormalizationContext: $denormalizationContext, - collectDenormalizationErrors: $collectDenormalizationErrors, - security: $security, - securityMessage: $securityMessage, - securityPostDenormalize: $securityPostDenormalize, - securityPostDenormalizeMessage: $securityPostDenormalizeMessage, - securityPostValidation: $securityPostValidation, - securityPostValidationMessage: $securityPostValidationMessage, - deprecationReason: $deprecationReason, - filters: $filters, - validationContext: $validationContext, - input: $input, - output: $output, - mercure: $mercure, - messenger: $messenger, - elasticsearch: $elasticsearch, - urlGenerationStrategy: $urlGenerationStrategy, - read: $read, - deserialize: $deserialize, - validate: $validate, - write: $write, - serialize: $serialize, - fetchPartial: $fetchPartial, - forceEager: $forceEager, - priority: $priority, - name: $name ?: 'update_subscription', - provider: $provider, - processor: $processor, - stateOptions: $stateOptions, - parameters: $parameters, + resolver : $resolver, + args : $args, + extraArgs : $extraArgs, + links : $links, + securityAfterResolver : $securityAfterResolver, + securityMessageAfterResolver : $securityMessageAfterResolver, + shortName : $shortName, + class : $class, + paginationEnabled : $paginationEnabled, + paginationType : $paginationType, + paginationItemsPerPage : $paginationItemsPerPage, + paginationMaximumItemsPerPage : $paginationMaximumItemsPerPage, + paginationPartial : $paginationPartial, + paginationClientEnabled : $paginationClientEnabled, + paginationClientItemsPerPage : $paginationClientItemsPerPage, + paginationClientPartial : $paginationClientPartial, + paginationFetchJoinCollection : $paginationFetchJoinCollection, + paginationUseOutputWalkers : $paginationUseOutputWalkers, + order : $order, + description : $description, + normalizationContext : $normalizationContext, + denormalizationContext : $denormalizationContext, + collectDenormalizationErrors : $collectDenormalizationErrors, + security : $security, + securityMessage : $securityMessage, + securityPostDenormalize : $securityPostDenormalize, + securityPostDenormalizeMessage : $securityPostDenormalizeMessage, + securityPostValidation : $securityPostValidation, + securityPostValidationMessage : $securityPostValidationMessage, + deprecationReason : $deprecationReason, + filters : $filters, + validationContext : $validationContext, + input : $input, + output : $output, + mercure : $mercure, + messenger : $messenger, + elasticsearch : $elasticsearch, + urlGenerationStrategy : $urlGenerationStrategy, + read : $read, + deserialize : $deserialize, + validate : $validate, + write : $write, + serialize : $serialize, + fetchPartial : $fetchPartial, + forceEager : $forceEager, + priority : $priority, + name : $name ?: 'update_subscription', + provider : $provider, + processor : $processor, + stateOptions : $stateOptions, + parameters : $parameters, queryParameterValidationEnabled: $queryParameterValidationEnabled, - policy: $policy, - rules: $rules, - extraProperties: $extraProperties, + rules : $rules, + policy : $policy, + extraProperties : $extraProperties, ); } + + public function isCollection(): bool + { + return $this->collection; + } } diff --git a/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php index f0f9a213a6d..bc797e9a7c1 100644 --- a/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -50,6 +50,7 @@ final class PublishMercureUpdatesListener 'topics' => true, 'data' => true, 'private' => true, + 'private_fields' => true, 'id' => true, 'type' => true, 'retry' => true, @@ -217,10 +218,23 @@ private function storeObjectToPublish(object $object, string $property): void // We need to evaluate it here, because in publishUpdate() the resource would be already deleted $this->evaluateTopics($options, $object); + $privateData = []; + $mercureOptions = $operation ? ($operation->getMercure() ?? false) : false; + $private = $mercureOptions['private'] ?? false; + $privateFields = $mercureOptions['private_fields'] ?? []; + if ($private && $privateFields) { + foreach ($privateFields as $privateField) { + if (property_exists($object, $privateField)) { + $privateData[$privateField] = $this->getResourceId($privateField, $object); + } + } + } + $this->deletedObjects[(object) [ 'id' => $this->iriConverter->getIriFromResource($object), 'iri' => $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL), 'type' => 1 === \count($types) ? $types[0] : $types, + 'private' => $privateData, ]] = $options; return; @@ -293,11 +307,11 @@ private function evaluateTopics(array &$options, object $object): void */ private function getGraphQlSubscriptionUpdates(object $object, array $options, string $type): array { - if ('update' !== $type || !$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) { + if (!$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) { return []; } - $payloads = $this->graphQlSubscriptionManager->getPushPayloads($object); + $payloads = $this->graphQlSubscriptionManager->getPushPayloads($object, $type); $updates = []; foreach ($payloads as [$subscriptionId, $data]) { @@ -318,4 +332,14 @@ private function buildUpdate(string|array $iri, string $data, array $options): U { return new Update($iri, $data, $options['private'] ?? false, $options['id'] ?? null, $options['type'] ?? null, $options['retry'] ?? null); } + + private function getResourceId(string $privateField, object $object): string + { + $id = $object->{'get'.ucfirst($privateField)}()->getId(); + if ($id instanceof \Stringable) { + return (string) $id; + } + + return $id; + } } diff --git a/src/Symfony/Tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php b/src/Symfony/Tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php index 6780f53864e..f264b243c5b 100644 --- a/src/Symfony/Tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php +++ b/src/Symfony/Tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php @@ -327,7 +327,7 @@ public function testPublishGraphQlUpdates(): void $graphQlSubscriptionManagerProphecy = $this->prophesize(GraphQlSubscriptionManagerInterface::class); $graphQlSubscriptionId = 'subscription-id'; $graphQlSubscriptionData = ['data']; - $graphQlSubscriptionManagerProphecy->getPushPayloads($toUpdate)->willReturn([[$graphQlSubscriptionId, $graphQlSubscriptionData]]); + $graphQlSubscriptionManagerProphecy->getPushPayloads($toUpdate, 'update')->willReturn([[$graphQlSubscriptionId, $graphQlSubscriptionData]]); $graphQlMercureSubscriptionIriGenerator = $this->prophesize(GraphQlMercureSubscriptionIriGeneratorInterface::class); $topicIri = 'subscription-topic-iri'; $graphQlMercureSubscriptionIriGenerator->generateTopicIri($graphQlSubscriptionId)->willReturn($topicIri);