@@ -804,17 +804,22 @@ to allow a client to receive pushed realtime data from the server.
804804In API Platform, the built-in subscription support is handled by using
805805[Mercure](https://mercure.rocks/) as its underlying protocol.
806806
807- # ## Enable Update Subscriptions for a Resource
807+ # ## Enable Subscriptions for a Resource
808808
809- To enable update subscriptions for a resource, these conditions have to be met :
809+ To enable GraphQL subscriptions for a resource, these conditions have to be met :
810810
811811- the
812812 [Mercure hub and bundle need to be installed and configured](mercure.md#installing-mercure-support).
813813- Mercure needs to be enabled for the resource.
814814- the `update` mutation needs to be enabled for the resource.
815- - the subscription needs to be enabled for the resource.
815+ - at least one subscription operation needs to be enabled for the resource.
816816
817- For instance, your resource should look like this :
817+ API Platform provides two GraphQL subscription operation types :
818+
819+ - `Subscription` : subscribes to updates for a specific item.
820+ - `SubscriptionCollection` : subscribes to future events affecting a collection.
821+
822+ For instance, your resource could expose both an item subscription and a collection subscription :
818823
819824<code-selector>
820825
@@ -826,10 +831,12 @@ namespace App\Entity;
826831use ApiPlatform\M etadata\A piResource;
827832use ApiPlatform\M etadata\G raphQl\M utation;
828833use ApiPlatform\M etadata\G raphQl\S ubscription;
834+ use ApiPlatform\M etadata\G raphQl\S ubscriptionCollection;
829835
830836#[ApiResource(mercure: true, graphQlOperations: [
831837 new Mutation(name: 'update'),
832- new Subscription()
838+ new Subscription(name: 'update'),
839+ new SubscriptionCollection(name: 'update_collection'),
833840])]
834841class Book
835842{
@@ -844,7 +851,10 @@ resources:
844851 graphQlOperations:
845852 ApiPlatform\M etadata\G raphQl\M utation:
846853 name: update
847- ApiPlatform\M etadata\G raphQl\S ubscription: ~
854+ ApiPlatform\M etadata\G raphQl\S ubscription:
855+ name: update
856+ ApiPlatform\M etadata\G raphQl\S ubscriptionCollection:
857+ name: update_collection
848858` ` `
849859
850860` ` ` xml
@@ -856,30 +866,29 @@ resources:
856866 <resource class="App\E ntity\B ook">
857867 <graphQlOperations>
858868 <graphQlOperation class="ApiPlatform\M etadata\G raphQl\M utation" name="update" />
859- <graphQlOperation class="ApiPlatform\M etadata\G raphQl\S ubscription" />
869+ <graphQlOperation class="ApiPlatform\M etadata\G raphQl\S ubscription" name="update" />
870+ <graphQlOperation class="ApiPlatform\M etadata\G raphQl\S ubscriptionCollection" name="update_collection" />
860871 </graphQlOperations>
861872 </resource>
862873</resources>
863874` ` `
864875
865876</code-selector>
866877
867- # ## Subscribe
878+ # ## Subscribe to an Item
868879
869- Doing a subscription is very similar to doing a query :
880+ An item subscription is very similar to doing a query :
870881
871882` ` ` graphql
872- {
873- subscription {
874- updateBookSubscribe(input: { id: "/books/1", clientSubscriptionId: "myId" }) {
875- book {
876- title
877- isbn
878- }
879- mercureUrl
880- clientSubscriptionId
881- }
883+ subscription {
884+ updateBookSubscribe(input: {id: "/books/1", clientSubscriptionId: "myId"}) {
885+ book {
886+ title
887+ isbn
882888 }
889+ mercureUrl
890+ clientSubscriptionId
891+ }
883892}
884893` ` `
885894
@@ -889,28 +898,161 @@ As you can see, you need to pass the **IRI** of the resource as argument. See
889898You can also pass `clientSubscriptionId` as argument and can ask its value as a field.
890899
891900In the payload of the subscription, the given fields of the resource will be the fields you
892- subscribe to : if any of these fields is updated, you will be pushed their updated values.
901+ subscribe to.
902+
903+ An item subscription receives events for that specific item :
904+
905+ - ` update` events for the item
906+ - ` delete` events for the item
893907
894908The `mercureUrl` field is the Mercure URL you need to use to
895909[subscribe to the updates](https://mercure.rocks/docs/getting-started#subscribing) on the
896910client-side.
897911
898- # ## Receiving an Update
912+ The initial registration response contains the current item payload together with the Mercure
913+ metadata :
914+
915+ ` ` ` json
916+ {
917+ "book": {
918+ "title": "API Platform in Action",
919+ "isbn": "978-6-6344-4051-1"
920+ },
921+ "mercureUrl": "https://localhost/.well-known/mercure",
922+ "clientSubscriptionId": "myId"
923+ }
924+ ` ` `
925+
926+ # ## Subscribe to a Collection
927+
928+ A collection subscription registers interest in future events affecting a collection :
929+
930+ ` ` ` graphql
931+ subscription {
932+ update_collectionBookSubscribe(input: {id: "/books"}) {
933+ book {
934+ title
935+ isbn
936+ }
937+ mercureUrl
938+ clientSubscriptionId
939+ }
940+ }
941+ ` ` `
942+
943+ The initial registration response contains the Mercure metadata, but no item payload yet because no
944+ specific item event has happened :
945+
946+ ` ` ` json
947+ {
948+ "book": null,
949+ "mercureUrl": "https://localhost/.well-known/mercure",
950+ "clientSubscriptionId": null
951+ }
952+ ` ` `
953+
954+ Collection subscriptions receive events affecting items in the collection :
955+
956+ - ` create` events for new items in the collection
957+ - ` update` events for existing items in the collection
958+ - ` delete` events for deleted items from the collection
959+
960+ For `create` and `update`, the pushed payload contains the affected item with the fields requested
961+ in the subscription query.
962+
963+ # ## Receiving Updates
899964
900965On the client-side, you will receive the pushed updated data like you would receive the updated data
901966if you did an `update` mutation.
902967
903- For instance, you could receive a JSON payload like this :
968+ For item subscriptions receiving `update` events, and collection subscriptions receiving `create`
969+ or `update` events, you could receive a JSON payload like this :
904970
905971` ` ` json
906972{
907- "book": {
908- "title": "Updated title",
909- "isbn": "978-6-6344-4051-1"
910- }
973+ "book": {
974+ "title": "Updated title",
975+ "isbn": "978-6-6344-4051-1"
976+ }
977+ }
978+ ` ` `
979+
980+ When a subscribed item is deleted, or when a collection subscription receives a `delete` event,
981+ API Platform sends a lightweight payload instead of a regular normalized resource. By that point,
982+ the deleted object can no longer be normalized safely like a regular resource. This payload
983+ contains the information needed by the client to identify and evict the deleted item.
984+
985+ The delete payload shape is the same for item subscriptions and collection subscriptions. Even
986+ though an item subscription could infer the deleted item from the subscribed IRI, collection
987+ subscriptions cannot. Using one explicit delete event shape keeps client handling uniform.
988+
989+ For example, a delete event payload looks like this :
990+
991+ ` ` ` json
992+ {
993+ "type": "delete",
994+ "payload": {
995+ "id": "/books/1",
996+ "iri": "https://example.com/books/1",
997+ "type": "Book"
998+ }
911999}
9121000` ` `
9131001
1002+ # ## Private Subscription Delivery
1003+
1004+ GraphQL subscriptions use Mercure for transport. The `mercure` option controls how the subscription
1005+ is delivered :
1006+
1007+ - no `private` option : the subscription is public
1008+ - `mercure : ['private' => true]`: the subscription is private and requires Mercure authorization
1009+ - `mercure : ['private' => true, 'private_fields' => [...]]`: the subscription is private and
1010+ additionally partitioned by explicit fields on the resource
1011+
1012+ The `private_fields` option is specific to GraphQL subscriptions. It tells API Platform which
1013+ resource fields define the delivery scope of the subscription registration.
1014+
1015+ Concretely, API Platform :
1016+
1017+ - reads the configured field values from the resource when the subscription is registered
1018+ - builds a private delivery partition from those values
1019+ - stores subscriptions having the same partition together
1020+ - only publishes updates to subscriptions matching that same partition later
1021+
1022+ ` private_fields` does not change Mercure topics and it is not based on the authenticated user ID.
1023+ It is an application-defined partition key derived from resource data.
1024+
1025+ When a client subscribes, API Platform reads the values of these fields from the resource and uses
1026+ them to group subscriptions together. Two subscriptions sharing the same GraphQL field selection but
1027+ different `private_fields` values are stored separately and do not receive each other's updates.
1028+
1029+ This is useful when authorization alone is not precise enough for your application. For example, a
1030+ single authenticated user might belong to several organizations, accounts or workspaces, and you may
1031+ want subscriptions to be isolated by one of those scopes instead of by the user identity itself.
1032+
1033+ ` ` ` php
1034+ new Subscription(
1035+ name: 'update',
1036+ mercure: ['private' => true, 'private_fields' => ['organizationId']]
1037+ )
1038+ ` ` `
1039+
1040+ In this example :
1041+
1042+ - clients subscribed to resources with `organizationId = 12` share one private subscription partition
1043+ - clients subscribed to resources with `organizationId = 42` use another
1044+ - updates for one organization are not delivered to subscriptions registered for the other one
1045+
1046+ This is especially useful when Mercure authorization is necessary but not sufficient to describe the
1047+ actual business scope of the subscription. For example, a single authenticated client may be allowed
1048+ to access multiple organizations, projects, inboxes or workspaces, while each subscription should
1049+ still be isolated to only one of those scopes.
1050+
1051+ Use `private_fields` only together with `mercure.private = true`.
1052+
1053+ If you only need Mercure authorization and do not need this extra partitioning, use
1054+ `mercure : ['private' => true]` without `private_fields`.
1055+
9141056# ## Subscriptions Cache
9151057
9161058Internally, API Platform stores the subscriptions in a cache, using the
0 commit comments