CoolCMS API

NotificationBell extends BaseComponent
in package

FinalYes

Notification bell in the admin header: an icon button with a red dot and a Bootstrap dropdown with the last {@see PANEL_LIMIT} notifications.

Rendered in layouts/parts/shared/_header.latte, so on every admin page; the component needs no registration, ComponentTrait resolves it from the name {control adminSystemNotificationBell} through the DI factory.

The list is filled at page render; the user requests fresh data with the refresh button (signal refresh!), which redraws the list and the badge.

IMPORTANT: the dropdown needs a single copy of Bootstrap JS. With two copies loaded, one click called Dropdown::toggle() twice and the panel opened and closed at once. See the comment in _head.latte.

Two snippets, not one:

  • notificationBadge: only the dot inside the button,
  • notificationPanel: panel header and list.

IMPORTANT: never redraw the whole wrapper. Bootstrap keeps the dropdown instance on the toggle element, so the panel would close under the user's hands on refresh. For the same reason the toggle has data-bs-auto-close="outside": a click on refresh must not close it.

A notification has a type, not a text. The text column is practically empty; the type (REGISTRATION_NEW, ORDER_NEW, ...) says what happened, and the id column carries the id of the foreign record (not the own notification_id). The item label is therefore the type name and the click-through leads straight to editing that record; the type-to-destination map lives in NotificationTargetResolver, shared with the notification list. A notification without a target (type without a page, or itemId NULL because the target record was deleted and the notification detached from it) is plain text without a link; the cogwheel still offers the notification detail.

Ownership: a row with receiverUserId = NULL is global (all admins see it and confirm it together), a row with receiverUserId = <id> targets one user. The bell therefore shows and marks only receiverUserId IS NULL OR receiverUserId = <logged-in user> (buildScopeFilters()); without a logged-in identity the panel is empty. Another user's targeted row is never shown or changed.

Table of Contents

Constants

DETAIL_DESTINATION  : mixed = ':Admin:System:Notification:edit'
Notification detail: target of the cogwheel and fallback when the type has nowhere to link.
FALLBACK_ICON  : mixed = 'fa-solid fa-circle-info'
Icon for a type without its own, so a row never has an empty icon.
LIST_DESTINATION  : mixed = ':Admin:System:Notification:show'
Notification list: target of the "show all" button.
PANEL_LIMIT  : mixed = 10
How many latest notifications the panel shows.
TIME_FORMAT  : mixed = 'j.n.Y H:i'
Time format of a panel item (same as `DateTimeRenderer` in grids).

Properties

$container  : Container
$routerDispatcher  : RouterDispatcher
$translator  : ITranslator
$appContext  : AppContext
$componentParameters  : array<string, mixed>
$currencyContext  : CurrencyContext|null
$deviceDetector  : DeviceDetector|null
$languageContext  : LanguageContext|null
$localizationContext  : LocalizationContext|null
$settingContext  : SettingContext
$themeContext  : ThemeContext|null
$translatorDomain  : string|null
$notificationManager  : NotificationManager
$notificationTargets  : NotificationTargetResolver

Methods

__call()  : mixed
Dispatches `renderXyz()` to `render('Xyz', ...$args)`.
__construct()  : mixed
createComponent()  : IComponent|null
getAppContext()  : AppContext
getComponentParameter()  : mixed
Returns a component parameter, or null when it is not set.
getContainer()  : Container
getCurrencyContext()  : CurrencyContext|null
getLanguageContext()  : LanguageContext|null
getLocalizationContext()  : LocalizationContext|null
getRouterDispatcher()  : RouterDispatcher
getSettingContext()  : SettingContext
getSettings()  : array<int, array<string, mixed>>
getThemeContext()  : ThemeContext|null
handleMarkAllRead()  : void
Marks all unread notifications as read (the dot goes out).
handleRefresh()  : void
Manual list refresh (button in the panel header). The list is otherwise filled only at page load, so it gets stale in a long-open admin.
hasServiceByType()  : bool
Returns whether the container has a service of the given type.
initializeAllContexts()  : void
injectDeviceDetector()  : void
onCreate()  : void
render()  : void
setAppContext()  : $this
setComponentParameter()  : void
Sets a component parameter.
setCurrencyContext()  : $this
setLanguageContext()  : $this
setLocalizationContext()  : $this
setSettingContext()  : $this
setThemeContext()  : $this
setup()  : void
setupAppContext()  : void
typedComponent()  : T
Returns a subcomponent with a verified type.
frontPresenter()  : BasePresenter|null
Returns the front presenter the component is attached to (null outside the front end or when detached).
getTranslator()  : ITranslator
setTemplateFile()  : void
setTranslator()  : self
buildScopeFilters()  : array<int, Filter>
Bell scope: global rows plus rows targeted at the given user. The OR is one group filter, so Doctrine parenthesizes it and `read = 0` applies to both branches.
createMultiplierComponent()  : Multiplier<string|int, IComponent>
Creates a multiplier for the component.
createNewComponentInstance()  : IComponent|null
Creates a new component instance without adding it to the presenter; used by the multiplier.
createStandardComponent()  : IComponent|null
Creates a standard component (not a multiplier).
findUnread()  : array<int, Notification>
hasUnread()  : bool
Tells whether the dot is lit. The dot means "an unread notification exists", any one, not just among the displayed ones. When it is visible in the loaded ten, the answer is ready and no extra query runs; the query is paid only when the last ten are all read.
prepareItems()  : array<int, array{id: int, title: string, text: string, time: string, read: bool, failed: bool, icon: string, href: string|null, detailHref: string}>
Returns the panel items: the last {@see PANEL_LIMIT} notifications, newest first.
redrawPanel()  : void
Redraws the list and the badge. Without JS (naja did not catch the link) it ends with a redirect to the same page: the panel closes, but the data is fresh after the reload, so the action is not lost.
resolveHref()  : string|null
Returns the click-through of an item: editing of the record the notification was raised for (`itemId`, not `notification_id`). `null` makes the item plain text: a type without a target in the map, a target the admin has no privilege for, or a notification without `itemId`, which stays detached after the target record is deleted and must not lead to a 404.
resolveUserId()  : int|null
Returns the logged-in admin id, or null without an identity (then nothing is shown or marked).

Constants

DETAIL_DESTINATION

Notification detail: target of the cogwheel and fallback when the type has nowhere to link.

private mixed DETAIL_DESTINATION = ':Admin:System:Notification:edit'

FALLBACK_ICON

Icon for a type without its own, so a row never has an empty icon.

private mixed FALLBACK_ICON = 'fa-solid fa-circle-info'

Font Awesome comes from a CDN in the admin (_head.latte).

LIST_DESTINATION

Notification list: target of the "show all" button.

private mixed LIST_DESTINATION = ':Admin:System:Notification:show'

PANEL_LIMIT

How many latest notifications the panel shows.

private mixed PANEL_LIMIT = 10

TIME_FORMAT

Time format of a panel item (same as `DateTimeRenderer` in grids).

private mixed TIME_FORMAT = 'j.n.Y H:i'

Properties

$componentParameters

protected array<string, mixed> $componentParameters = []

component parameters set by setComponentParameter()

$translatorDomain

protected string|null $translatorDomain = 'admin.system.components.notificationBell'

Methods

__call()

Dispatches `renderXyz()` to `render('Xyz', ...$args)`.

public __call(string $name, array<string|int, mixed> $args) : mixed

Called only when the component has no renderXyz() method of its own.

Parameters
$name : string
$args : array<string|int, mixed>

createComponent()

public createComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

getComponentParameter()

Returns a component parameter, or null when it is not set.

public getComponentParameter(string $name) : mixed
Parameters
$name : string

getContainer()

public getContainer() : Container
Return values
Container

getSettings()

public getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>

handleMarkAllRead()

Marks all unread notifications as read (the dot goes out).

public handleMarkAllRead() : void

Without it 100+ old unread records could be handled only one by one and the dot would stay lit forever.

Attributes
#[Requires]
$methods: 'POST'

handleRefresh()

Manual list refresh (button in the panel header). The list is otherwise filled only at page load, so it gets stale in a long-open admin.

public handleRefresh() : void

hasServiceByType()

Returns whether the container has a service of the given type.

public hasServiceByType(string $className) : bool

The name is composed dynamically (module plus component), so class_exists() runs first, and a true result guarantees a valid type.

Parameters
$className : string
Tags
phpstan-assert-if-true

class-string $className

Return values
bool

setComponentParameter()

Sets a component parameter.

public setComponentParameter(string $name, mixed $value) : void
Parameters
$name : string
$value : mixed

typedComponent()

Returns a subcomponent with a verified type.

public typedComponent(string $name, T> $type) : T

Use it instead of an inline @var above $this['x'] (which does not narrow the type) and instead of the generic IComponent that $this['x'] returns. See Form::typedComponent().

Parameters
$name : string
$type : T>
Tags
template
Return values
T

frontPresenter()

Returns the front presenter the component is attached to (null outside the front end or when detached).

protected frontPresenter() : BasePresenter|null
Return values
BasePresenter|null

getTranslator()

protected getTranslator() : ITranslator
Return values
ITranslator

setTemplateFile()

protected setTemplateFile(string $dir, string $filename) : void
Parameters
$dir : string
$filename : string

setTranslator()

protected setTranslator(ITranslator $translator) : self
Parameters
$translator : ITranslator
Return values
self

buildScopeFilters()

Bell scope: global rows plus rows targeted at the given user. The OR is one group filter, so Doctrine parenthesizes it and `read = 0` applies to both branches.

private buildScopeFilters(int $userId[, bool $onlyUnread = false ]) : array<int, Filter>
Parameters
$userId : int
$onlyUnread : bool = false
Return values
array<int, Filter>

createMultiplierComponent()

Creates a multiplier for the component.

private createMultiplierComponent(string $name) : Multiplier<string|int, IComponent>
Parameters
$name : string
Return values
Multiplier<string|int, IComponent>

createNewComponentInstance()

Creates a new component instance without adding it to the presenter; used by the multiplier.

private createNewComponentInstance(string $baseName[, int|string|null $uniqueKey = null ]) : IComponent|null
Parameters
$baseName : string
$uniqueKey : int|string|null = null
Return values
IComponent|null

createStandardComponent()

Creates a standard component (not a multiplier).

private createStandardComponent(string $name) : IComponent|null
Parameters
$name : string
Return values
IComponent|null

hasUnread()

Tells whether the dot is lit. The dot means "an unread notification exists", any one, not just among the displayed ones. When it is visible in the loaded ten, the answer is ready and no extra query runs; the query is paid only when the last ten are all read.

private hasUnread(array<int, array{read: bool}> $items, int $userId) : bool
Parameters
$items : array<int, array{read: bool}>

items of the panel loaded just now

$userId : int
Return values
bool

prepareItems()

Returns the panel items: the last {@see PANEL_LIMIT} notifications, newest first.

private prepareItems(int $userId) : array<int, array{id: int, title: string, text: string, time: string, read: bool, failed: bool, icon: string, href: string|null, detailHref: string}>

Each item has two targets: href is the record the notification was raised for (label on the left plus the type icon on the right; null means no target, plain text), detailHref is editing of the notification itself (cogwheel). icon is the Font Awesome class of the type, with the placeholder FALLBACK_ICON.

Parameters
$userId : int
Return values
array<int, array{id: int, title: string, text: string, time: string, read: bool, failed: bool, icon: string, href: string|null, detailHref: string}>

redrawPanel()

Redraws the list and the badge. Without JS (naja did not catch the link) it ends with a redirect to the same page: the panel closes, but the data is fresh after the reload, so the action is not lost.

private redrawPanel() : void

resolveHref()

Returns the click-through of an item: editing of the record the notification was raised for (`itemId`, not `notification_id`). `null` makes the item plain text: a type without a target in the map, a target the admin has no privilege for, or a notification without `itemId`, which stays detached after the target record is deleted and must not lead to a 404.

private resolveHref(Notification $notification) : string|null
Parameters
$notification : Notification
Return values
string|null

resolveUserId()

Returns the logged-in admin id, or null without an identity (then nothing is shown or marked).

private resolveUserId() : int|null
Return values
int|null
On this page

Search results