LAMP Stack
Table of contents
- Overview
- Channel Registry
- Channels & Payloads
- A. Database Channel
- B. Log Channel
- C. Mail Channel
- Writing a Notification
- Making a Model Notifiable
- Events and Listeners
- CLI Commands
- Testing
- Notifications Model
- A. Fields
- B. Common Queries
- Troubleshooting
1. Overview Table of Contents
This guide shows you how to register channels, write notifications, send them from code, and exercise everything from the CLI.
2. Channels Registry Table of Contents
Channel registry is run automatically. Here is an overview of what happens.
- Use
ChannelRegistry::register('log', LogChannel::class)to add/override channels. ChannelRegistry::resolve('log')returns a channel driver.ChannelRegistry::has('log')to check registration.- Registry stores names as lowercase.
3. Channels & Payloads Table of Contents
We currently support 3 channels for notifications
- Database
- Log
A. Database Channel
- Persists records into
notificationstable viaCore\Models\Notifications. - Expects payload from
Notification::toDatabase()(array). - Requires
$notifiable->id.
To display information from this driver to a user in flash messages you can call the \Core\Services\NotificationService::flashUnreadNotifications function.
B. Log Channel
- Writes a structured JSON entry via Logger::log().
- Reads:
message(string or any JSON-encodable)level(defaultinfo)_meta/meta(array)- all other keys treated as “data” fields.
- Falls back to
Notification::toLog()ordata['message']or a synthesized message.
C. Mail Channel
Supports three payload shapes from toMail():
1. Template mode (preferred for templates)
```php
return [
'subject' => 'Welcome!',
'template' => 'welcome_user',
'data' => ['user' => $user],
// optional:
'layout' => 'default',
'attachments' => [...],
'layoutPath' => null,
'templatePath' => null,
'styles' => 'default',
'stylesPath' => null,
];
```
2. Raw HTML (with optional text fallback)
return [
'subject' => 'Subject',
'html' => '<p>Hello</p>',
'text' => 'Hello', // optional; triggers sendWithText()
'attachments' => [...], // optional
];
3. Custom mailer
return [
'mailer' => \Core\Lib\Mail\WelcomeMailer::class,
// optional overrides used by buildAndSend():
'layout' => null,
'attachments' => [],
'layoutPath' => null,
'templatePath'=> null,
'styles' => null,
'stylesPath' => null,
];
If none of template, html, or mailer is present, MailChannel throws:
“Mail payload must include one of: “template”, “html”, or “mailer”.”
4. Writing a Notification Table of Contents
Core\Lib\Notifications\Notification is the base class. Below is the default template for the make:notifications command:
<?php
namespace App\Notifications;
use App\Models\Users;
use Core\Lib\Notifications\Notification;
/**
* NewUser notification.
*/
class NewUser extends Notification {
protected $user;
/**
* Undocumented function
*
* @param Users $user
*/
public function __construct(Users $user) {
$this->user = $user;
}
/**
* Data stored in the notifications table.
*
* @param object $notifiable Any model/object that uses the Notifiable trait.
* @return array<string,mixed>
*/
public function toDatabase(object $notifiable): array {
return [
'user_id' => (int)$this->user->id,
'username' => $this->user->username ?? $this->user->email,
'message' => "Temp notification for user #{$this->user->id}",
'created_at'=> \Core\Lib\Utilities\DateTime::timeStamps(), // optional
];
}
/**
* Logs notification to log file.
*
* @param object $notifiable Any model/object that uses the Notifiable trait.
* @return string Contents for the log.
*/
public function toLog(object $notifiable): string {
return "";
}
/**
* Handles notification via E-mail.
*
* @param object $notifiable Any model/object that uses the Notifiable trait.
* @return array<string,mixed>
*/
public function toMail(object $notifiable): array {
return [];
}
/**
* Specify which channels to deliver to.
*
* @param object $notifiable Any model/object that uses the Notifiable trait.
* @return list<'database'|'mail'|'log'
*/
public function via(object $notifiable): array {
return Notification::channelValues();
}
}
Fallbacks
toArray()defaults totoDatabase().- If a
to{Channel}method is missing, that channel will receive['message' => null]plus any extra payload.
5. Making a Model Notifiable Table of Contents
Use the Notifiable trait on your model (e.g., Users):
use Core\Lib\Notifications\Notifiable;
class Users {
use Notifiable;
public int $id;
public string $email;
}
Send a notification:
$user->notify(new \App\Notifications\UserRegistered($user));
// or override channels/payload:
$user->notify(
new \App\Notifications\UserRegistered($user),
['log'], // channels override
['level' => 'warning', 'meta' => ['source' => 'cli']]
);
The trait ensures array payloads stay top-level (for Mail/DB) and strings are wrapped as ['message' => '...'] (for Log).
6. Events and Listeners Table of Contents
You can hook notifications into this framework’s events/listener and queue system. The events/listener example is as follows:
class SendRegistrationEmail {
/**
* Handles event for sending user registered E-mail.
*
* @param UserRegistered $event The event.
* @return void
*/
public function handle(UserRegistered $event): void {
$user = $event->user;
$shouldSendEmail = $event->shouldSendEmail;
NotificationService::notifyUsers(new UserRegisteredNotification($user));
if($shouldSendEmail) {
WelcomeMailer::sendTo($user);
}
}
}
The following example is for a queued event listener:
class SendWelcomeEmailListener implements ShouldQueue, QueuePreferences {
public function handle(UserRegistered $event) : void {
NotificationService::notifyUsers(new UserRegisteredNotification($event->user));
UserService::queueWelcomeMailer((int)$event->user->id, $this->viaQueue());
}
public function viaQueue(): ?string { return 'mail'; }
public function delay(): int { return 60; } // 1 min
public function backoff(): int|array { return [10, 30, 60]; }
public function maxAttempts(): int { return 5; }
}
This demonstrates two approaches: synchronous mail vs. queued mail. Both use NotificationService::notifyAdmins(new UserRegisteredNotification($user)).
7. CLI Commands Table of Contents
A. Test a Notification (no side-effects by default)
php console notification:test DummyNotification --dry-run
Options:
--user=<id|email|username>— target notifiable (falls back to"dummy"if omitted).--channels=log,database— override channels; omit to usevia().--with=key:value,key2:value2— attach additional payload fields (e.g.,level:warning).
Examples:
# Dry run with defaults (uses via()):
php console notification:test UserRegistered --dry-run
# Dry run with channel override and payload overrides
php console notification:test UserRegistered --dry-run --channels=log,database --with=level:warning,tag:cli
# Actually send (remove --dry-run)
php console notification:test UserRegistered --user=42 --channels=mail
The command uses Tools::setOutput($output) so output is captured in tests.
On --dry-run, it prints the “Would send … via [X,Y]” line plus the JSON payload.
B. Generate a Notification class
php console make:notification UserRegistered --channels=log,database,mail
- Creates
app/Notifications/UserRegistered.php. - Includes channel methods matching the provided list and a
via()that returns them. - If you omit
--channels, it scaffolds with all available channels.
C. Generate the notifications migration
php console notifications:migration
- Writes a migration class that creates the notifications table with useful indexes.
D. Prune old notifications
php console notifications:prune --days=90
- Deletes notifications older than
Ndays (default 90). - Uses
Core\Models\Notifications::notificationsToPrune($days)internally.
8. Testing Table of Contents
- For console tests, use Symfony’s
CommandTesterand ensure:- Your command calls
Tools::setOutput($output)at the start ofexecute(). - The notification class exists under
App\Notifications\YourNotification.
- Your command calls
- Typical assertions:
- error path: prints “does not exist”
- dry-run path: prints
[DRY-RUN] … via [log] - payload echo contains “level”: “info”, etc.
9. Notifications Model Table of Contents
The Core\Models\Notifications model represents rows in the notifications table and gives you simple helpers to mark notifications as read and prune old rows. Most apps let the notification system write these rows automatically (via the Database channel). Use this model when you need to build an inbox, mark items read, or clean up.
A. Fields
| Property | Type | Notes | |
|---|---|---|---|
id |
string | Primary key (UUID or string). | |
type |
string | FQCN of the notification class that created the row. | |
notifiable_type |
string | FQCN of the target entity (e.g., App\Models\Users). |
|
notifiable_id |
int | string | ID of the target entity. |
data |
text (JSON) | Payload from Notification::toDatabase() (stringified JSON). |
|
read_at |
timestamp|null | null if unread; timestamp when marked read. |
|
created_at |
timestamp | Auto-managed in beforeSave(). |
|
updated_at |
timestamp | Auto-managed in beforeSave(). |
Common Queries. Fields
Get a user’s notifications
use Core\Models\Notifications;
use App\Models\Users;
$user = Users::findById(42);
// All (read + unread), newest first
$rows = Notifications::find([
'conditions' => 'notifiable_type = ? AND notifiable_id = ?',
'bind' => [Users::class, $user->id],
'order' => 'created_at DESC',
]);
Only unread
$unread = Notifications::find([
'conditions' => 'notifiable_type = ? AND notifiable_id = ? AND read_at IS NULL',
'bind' => [Users::class, $user->id],
'order' => 'created_at DESC',
]);
Count unread
$unreadCount = Notifications::count([
'conditions' => 'notifiable_type = ? AND notifiable_id = ? AND read_at IS NULL',
'bind' => [Users::class, $user->id],
]);
Marking as Read
1) Mark a single record (instance method)
$note = Notifications::findFirst(['conditions' => 'id = ?', 'bind' => [$id]]);
if ($note) {
$note->markAsRead(); // sets read_at = now and saves
}
2) Mark by ID (static helper)
$ok = Notifications::markAsReadById($id); // true if updated, false otherwise
Pruning Old Notifications Use this to reclaim space and keep the table fast. Basic prune (all rows older than N days):
// Delete notifications older than 90 days (read or unread)
$deleted = Notifications::notificationsToPrune(90);
Prune only read rows:
// Delete only read notifications older than 30 days
$deleted = Notifications::notificationsToPrune(30, true);
Return value: Number of rows deleted.
Constraints: $days must be ≥ 1 (throws InvalidArgumentException otherwise).
Indexing tip: Add indexes on created_at and (notifiable_type, notifiable_id) for best performance. If you plan to prune by “read only”, consider (read_at, created_at).
Working with the data JSON
The data column stores whatever your Notification::toDatabase() returned. Typical shape:
[
'user_id' => 42,
'username' => 'alice',
'message' => 'A new user has registered: alice',
'registered_at' => '2025-08-01 12:00:00'
]
To use it:
$note = Notifications::findFirst(['conditions' => 'id = ?', 'bind' => [$id]]);
if ($note) {
$payload = json_decode((string) $note->data, true) ?: [];
$message = $payload['message'] ?? '';
}
Quick Recipes Show last 10 unread messages for a user:
$rows = Notifications::find([
'conditions' => 'notifiable_type = ? AND notifiable_id = ? AND read_at IS NULL',
'bind' => [Users::class, $user->id],
'order' => 'created_at DESC',
'limit' => 10,
]);
foreach ($rows as $row) {
$data = json_decode((string)$row->data, true) ?: [];
echo $data['message'] ?? '[no message]', PHP_EOL;
}
Mark all of a user’s notifications as read (simple loop):
$rows = Notifications::find([
'conditions' => 'notifiable_type = ? AND notifiable_id = ? AND read_at IS NULL',
'bind' => [Users::class, $user->id],
]);
foreach ($rows as $row) {
$row->markAsRead();
}
10. Troubleshooting Table of Contents
- “Unsupported notification channel X”: Make sure
NotificationManager::boot()ran and your channel is registered inconfig('notifications.channels'). - MailChannel errors: Ensure
toMail()returns one of:template,html(optionallytext), ormailer. Missing these throwsInvalidPayloadException. - DatabaseChannel errors: Your notifiable must have a public
id. If you send to a dummy string in “simulate” mode, the helper logs instead of saving. - LogChannel message is empty: Provide
toLog()or include amessagekey in your payload/toArray(). - Namespaces & autoload: Place app notifications in
app/Notificationswith namespaceApp\Notifications;. Runcomposer dump-autoloadafter adding files.