Notification subscriptions & multi-channel delivery framework - #1959
Draft
markus-moser wants to merge 13 commits into
Draft
Notification subscriptions & multi-channel delivery framework#1959markus-moser wants to merge 13 commits into
markus-moser wants to merge 13 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>
Add the email channel's greeting, CTA and footer for de/es/fr/it/no/sv, sitting inline with the other keys, and drop the section comment across every catalog (incl. en) so the keys read like the rest of the file. The title and message still come from the notification; en is the fallback for any missing locale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
markus-moser
force-pushed
the
feat/notification-subscriptions
branch
from
August 11, 2026 13:43
9a40369 to
f628c8d
Compare
In a core-only install the sole notification type is the built-in "info" catch-all, which never allows external delivery — so the email channel would be dead weight: an extra column on the preferences screen and an instantiated mailer no notification could ever reach. The dispatch compiler pass now evaluates the registered descriptors and, when none allow external delivery, drops the tagged transport channels entirely (the in-app "popup" substrate is always available and is not a tagged channel). Installing a bundle that contributes an externally-deliverable type — Collab's mention/task/discussion types — brings the channels back automatically, so nothing changes for a real Studio install. A descriptor wired with service references or that fails to construct is assumed external-capable, so a channel a bundle actually wants is never stripped on a false negative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symfony 8 removes the #[TaggedIterator] attribute in favour of #[AutowireIterator], which has an identical constructor. A drop-in rename in the two notification registries; behaviour is unchanged on Symfony 7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the definition-skip check and the channel apply/remove loop out of process(), bringing its cognitive complexity back under the threshold. No behaviour change — the gating and tagging are identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntion Every other Messenger message in the bundle declares its transport in a YAML file (execution_engine.yaml, config/prepend/*.yaml); only the notification email routing was inline PHP in the Extension. Move it to config/prepend/notification.yaml, loaded like the other prepend configs. Transport is unchanged (pimcore_core — a fire-and-forget delivery, not a job, so not pimcore_generic_execution_engine). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The template was a bare <div>. Wrap it in <!DOCTYPE html> + <html lang> + <head><meta charset> + <body>, matching Pimcore's own workflow notification email (Pimcore\Mail does not wrap fragments). Content is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add doc/03_Extending page: contributing a notification type descriptor and a delivery channel, with the dispatch flow, the 20-char type-id cap, the config envelope and the frontend-renderer pointer. Linked from the chapter index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The <head> needs a title; use the notification's title. Fixes the SonarCloud "Add a <title> tag to this page" reliability bug on the email template. 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 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. An email channel ships with it.
It pays off even with no contributing bundle: every notification Pimcore writes today is untyped and falls into a built-in catch-all type, turning today's unconditional toasting into a per-user pop-up choice — for workflow, user-to-user and any direct producer — without changing a single producer.
Key decisions
notifications.typeisVARCHAR(20)), enforced at container build — no core schema change.API
GET/PUT/notifications/subscriptions— the caller's effective preferences (merged with descriptor defaults) and a bulk store.NotificationMinimalgains additivepopup+payload.Related
Frontend lives in pimcore/studio-ui-bundle#3913 (draft) — it consumes these endpoints and should merge after a studio-backend release containing this change.
Draft · base
2026.x.🤖 Generated with Claude Code