Notification subscriptions & multi-channel delivery framework - #1959
Draft
markus-moser wants to merge 5 commits into
Draft
Notification subscriptions & multi-channel delivery framework#1959markus-moser wants to merge 5 commits into
markus-moser wants to merge 5 commits into
Conversation
Introduces a generic, extensible notification framework: bundles contribute notification *types* and delivery *channels* as tagged services, and each user chooses per type whether they are notified and through which channels. The immediate win needs no contributing bundle at all. Every notification Pimcore writes today is untyped, so it falls into a built-in catch-all type whose pop-up preference is honoured when the notification is published over Mercure. That turns today's unconditional toasting into a choice for workflow transitions, user-to-user messages and anything a bundle writes directly, without touching a single producer. Design notes worth keeping in mind when extending this: - A type declares only *whether* it may leave the application, never through which channel. Supported channels are derived from that capability, so a bundle contributing a Teams channel lights up for existing types without those bundles being edited. - The pop-up is modelled as a channel from the user's point of view but is not a transport: it is a preference read at publish time. Storing it in the same JSON set is what keeps the schema stable when channels are added. - No channel implementation ships here. The only type present is the catch-all, which deliberately allows no external delivery — a bucket of unclassified notifications is not something to email. Whichever bundle first contributes an externally-deliverable type contributes the channel alongside it. - Type ids are capped at 20 characters because notifications.type is VARCHAR(20) and MySQL truncates silently outside strict mode. The registry rejects violations at boot rather than letting a truncated id match nothing. - The catch-all reports a different label when it is the only registered type: there is nothing for it to be "everything else" to. NotificationMinimal gains popup and payload. Both are additive and popup defaults to true, so a client that has not adopted them behaves as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The catch-all was expected to arrive through the service tag like any other descriptor. When the tag was not applied the registry held nothing, so a bare installation reported no subscribable types and the preferences screen came up empty — found by calling the endpoint against a running app. Registering it directly is also the better design regardless of the tag: every notification ever written falls into this type, and on an installation with no contributing bundle it is the only one there is. Its presence should not be something wiring can break. Also fixes the channel translation key prefix, which did not match the keys shipped in studio-ui. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The module marks its internal service, repository and hydrator interfaces @internal; the new internal contracts were missing it. Adds it to the internal registry/subscription interfaces and the internal EffectiveSubscription value object, so the public surface stays limited to what is genuinely meant for external use — the descriptor and channel interfaces, the dispatcher, the DispatchableNotification producers build, the subscription-collection event, and the API schemas — all of which deliberately keep no @internal. Docblock only; no behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The framework's extensibility relies on bundles contributing type descriptors and delivery channels as tagged services, collected by the registries' tagged iterators. That tagging was expressed as #[AutoconfigureTag] on the interfaces — which, it turns out, does not tag implementers in Pimcore's container (the existing tagged collectors here, e.g. GDPR providers, are all tagged explicitly in YAML). The result was that no contributed descriptor or channel was ever collected: the type registry only ever saw the built-in catch-all, which it adds directly. Surfaced while wiring collab-bundle's notification types: they registered cleanly but never appeared. A compiler pass tags every implementer of the descriptor and channel interfaces. It runs after all bundle extensions load, so a type or channel from any bundle is picked up without that bundle knowing the tag name — which is what makes the framework actually extensible. Idempotent, so a bundle that tags explicitly is not tagged twice, and abstract definitions are skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The framework shipped the channel seam but no transport. This adds EmailChannel, the first ChannelInterface implementation, so externally-deliverable types (the Collab types today) can reach people by email as well as the bell and pop-up. - EmailChannel resolves the recipient, language and an absolute deep link inside the producing request, then hands a fully-resolved SendNotificationEmailMessage to the pimcore_core transport. The blocking send happens in the worker, so a slow mail server never delays the comment or assignment that triggered it. - The email mirrors the bell entry — the notification's own title and message plus a link, nothing from the payload — except one navigation hint: a producer may supply an app-relative deepLink (host-relative only, so a payload can never make the button off-site) to point at a better destination than the linked element, e.g. a Collab task or discussion in its Overview. - The body is a Twig template rendered in the recipient's language. It is overridable: point notifications.email.template at your own template, or drop a file at templates/bundles/PimcoreStudioBackendBundle/notification/email.html.twig. - Delivery rides the existing pimcore_core messenger transport (routing registered in the bundle extension), so the standard messenger:consume worker covers it. Registering EmailChannel makes the Email column appear in the preferences screen with no frontend change, respecting each type's allowsExternalDelivery and default channels. Unit-tested for enqueue-not-inline, message content, deep-link resolution and the host-relative guard; verified end-to-end into the mail catcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What this adds
A generic, extensible notification framework for Studio. Bundles contribute notification types and delivery channels as tagged services, and each user chooses, per type, whether they are notified and through which channels.
The immediate win needs no contributing bundle at all. Every notification Pimcore writes today is untyped, so it falls into a built-in catch-all type whose pop-up preference is honoured when the notification is published over Mercure. That turns today's unconditional toasting into a choice — for workflow transitions, user-to-user messages, and anything a bundle writes directly through the notification model — without touching a single producer.
Key design decisions
notifications.typeisVARCHAR(20)and MySQL truncates silently outside strict mode; the registry rejects violations at container build. No core schema change is required.API
Two endpoints under
/notifications/subscriptions(GET the caller's effective preferences merged with descriptor defaults; PUT to store them in bulk).NotificationMinimalgainspopupandpayload, both additive and defaulting so a client that has not adopted them behaves exactly as before.Layers
src/Notification/Dispatch/*(neutral, promotable namespace) —DispatchableNotification, dispatcher, descriptor + channel interfaces, tagged-iterator registries, subscription entity/repository/resolver — plus the subscription API (controllers, service, hydrator, events, schemas), an Installer table + migration, and the Mercure pop-up resolution.Tests & checks
popup: falsein the Mercure payload while its bell entry is still written.Cross-repo note
The matching frontend lives in pimcore/studio-ui-bundle#feat/notification-subscriptions (also draft). That PR is inert until this one ships the fields, and its preferences screen needs these endpoints; it should merge after a studio-backend release containing this change.
Status
Draft — opened for review of the design and contract. Branch base:
2026.x.🤖 Generated with Claude Code