meanify-co / laravel-notifications
A PHP library to handle multichannel notifications in Laravel
Package info
github.com/meanify-co/laravel-notifications
pkg:composer/meanify-co/laravel-notifications
Requires
- php: ^8.0
- guzzlehttp/guzzle: ^7.9
- league/commonmark: ^2.8.2
- mailgun/mailgun-php: ^4.3
- meanify-co/laravel-obfuscator: dev-master
- nyholm/psr7: ^1.8
- sendgrid/php-http-client: 4.1.1
- sendgrid/sendgrid: ^8.1
- sendpulse/rest-api: ^2.0
- symfony/http-client: ^7.2
- symfony/http-foundation: ^7.3.7
Requires (Dev)
- illuminate/support: ^10.0|^11.0|^12.0
- orchestra/testbench: ^8.0 || ^9.0
This package is auto-updated.
Last update: 2026-04-02 12:33:46 UTC
README
Laravel Notifications
A complete library for centralized multichannel notifications in Laravel, supporting multiple email drivers, real-time broadcasting, dynamic templates with multi-language support, and recipient filtering.
Table of Contents
- Features
- Requirements
- Installation
- Configuration
- Database Setup
- Usage
- Template System
- Recipient Filter
- Notification Statuses
- Artisan Commands
- Models Reference
- License
Features
- Multiple email drivers: SMTP, Mailgun, SendGrid, SendPulse
- Email attachments: file path or base64-encoded content
- Real-time broadcasting: in-app notifications via Laravel Broadcasting
- Custom broadcast channels: obfuscated model-based channels or simple string channels
- Multi-language: locale-based template translations (e.g.
pt_BR,en_US) - Dynamic templates: flexible interpolation engine with conditionals, loops, and isset blocks
- Queue processing: async dispatch via Laravel queues with retries and backoff
- Immediate sending: bypass queue when needed
- Recipient filtering: block emails by domain patterns, encrypted or base64-encoded addresses
- Payload encryption: sensitive notification data is encrypted at rest
- Preview mode: render notifications without dispatching
- Scheduled delivery: send notifications at a future date/time
Requirements
- PHP >= 8.0
- Laravel 10.x, 11.x, or 12.x
Installation
composer require meanify-co/laravel-notifications
The service provider is auto-discovered. Then publish the assets:
# Publish config php artisan vendor:publish --tag=meanify-configs # Publish models php artisan vendor:publish --tag=meanify-models # Publish migrations php artisan vendor:publish --tag=meanify-migrations # Publish seeders php artisan vendor:publish --tag=meanify-seeders # Run migrations php artisan migrate
Configuration
After publishing, the config file is at config/meanify-laravel-notifications.php.
return [ 'default_queue_name' => 'meanify.queue.notifications', 'user_model' => \App\Models\User::class, 'default_email_layout' => 'default', 'email' => [ 'enabled' => true, 'queue' => 'meanify.queue.notifications.emails', 'tries' => 3, 'backoff' => 30, 'verify_ssl' => false, 'send_immediately' => false, 'recipient_filter' => [ 'enabled' => false, 'blocked_domains' => [], 'block_encrypted' => true, 'block_base64' => true, 'on_block_status' => 'simulated', ], ], 'in_app' => [ 'enabled' => true, 'queue' => 'meanify.queue.notifications.in_app', ], 'broadcast' => [ 'channel_prefix' => 'mfy_channel_', 'enabled' => true, ], ];
Email Channel
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable email sending globally |
queue |
string | meanify.queue.notifications.emails |
Queue name for email jobs |
tries |
int | 3 |
Max retry attempts on failure |
backoff |
int | 30 |
Seconds between retries |
verify_ssl |
bool | false |
SSL verification for API drivers |
send_immediately |
bool | false |
If true, bypasses the queue globally |
In-App Channel
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable in-app notifications |
queue |
string | meanify.queue.notifications.in_app |
Queue name for broadcast jobs |
Broadcasting
| Key | Type | Default | Description |
|---|---|---|---|
channel_prefix |
string | mfy_channel_ |
Prefix for obfuscated broadcast channels |
enabled |
bool | true |
Enable/disable broadcasting |
Recipient Filter
Allows blocking email delivery based on recipient patterns. See Recipient Filter for details.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable/disable recipient filtering |
blocked_domains |
array | [] |
Domain patterns to block (supports wildcards) |
block_encrypted |
bool | true |
Block emails that appear encrypted |
block_base64 |
bool | true |
Block emails that appear base64-encoded |
on_block_status |
string | simulated |
Status when blocked: simulated or skipped |
Database Setup
Tables Overview
The migration creates the following tables:
emails_layouts
Stores reusable email layout templates (Blade-based).
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
name |
string | Layout display name |
key |
string (unique) | Identifier (e.g. default) |
metadata |
json | Colors, fonts, social links, etc. |
blade_template |
longText | Blade template with {{ $content }} |
notifications_templates
Master notification template definitions.
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
email_layout_id |
foreignId (nullable) | Associated email layout |
key |
string (unique) | Template identifier (e.g. user_registered) |
available_channels |
json | Channels array: ['email', 'in_app'] |
force_channels |
json (nullable) | Override available channels |
active |
boolean | Enable/disable template |
notifications_templates_translations
Localized content for each template.
| Column | Type | Description |
|---|---|---|
notification_template_id |
bigint | Foreign key |
locale |
string(10) | Language code (e.g. pt_BR) |
subject |
string | Email subject |
title |
string | Email title |
body |
text | Email body with template variables |
short_message |
string | In-app notification message |
notifications_templates_variables
Documents expected variables per template.
| Column | Type | Description |
|---|---|---|
notification_template_id |
bigint | Foreign key |
key |
string | Variable name (e.g. user_name) |
description |
text | Human description |
example |
string | Example value |
notifications
Log of all dispatched notifications.
| Column | Type | Description |
|---|---|---|
notification_template_id |
foreignId (nullable) | Template reference |
user_id |
bigint (nullable) | Recipient user |
application_id |
bigint (nullable) | Application context |
account_id |
bigint (nullable) | Account context |
session_id |
bigint (nullable) | Session context |
channel |
string | email or in_app |
payload |
json (encrypted) | Full notification data |
status |
enum | pending, processing, sent, failed, simulated, skipped |
scheduled_to |
timestamp (nullable) | Scheduled delivery time |
sent_at |
timestamp (nullable) | Actual send time |
failed_at |
timestamp (nullable) | Failure time |
failed_log |
json (nullable) | Exception details |
Note: The
application_id,account_id, andsession_idforeign keys are conditionally added only if their corresponding tables exist in the database.
Seeders
After publishing, run the seeders to populate default data:
# Seeds the default email layout php artisan db:seed --class=EmailLayoutSeeder # Seeds pre-built notification templates (pt_BR + en_US) php artisan db:seed --class=NotificationTemplateSeeder
Included templates: sign_in_otp, forgot_password_otp, new_login_detected, email_change_verification, password_changed_notice, email_changed_notice, invite_new_user, in_app_only_1, combined_notification.
Usage
NotificationBuilder (Fluent API)
The NotificationBuilder is the primary interface for creating and sending notifications.
use Meanify\LaravelNotifications\Support\NotificationBuilder; NotificationBuilder::make('template_key', $user, 'pt_BR') ->onAccount($accountId) // optional: account context ->onApplication($applicationId) // optional: application context ->onSession($sessionId) // optional: session context ->forEmail('smtp', $configs, $recipients) ->with(['user_name' => 'John']) // template variables ->withAttachments([...]) // optional: file attachments ->toBroadcastChannels([...]) // optional: custom broadcast channels ->scheduledTo(now()->addHours(1)) // optional: delay delivery ->send(); // dispatch!
Available Methods
| Method | Description |
|---|---|
make($templateKey, $user, $locale) |
Static factory. Creates a builder instance. |
onAccount($id) |
Sets account context. |
onApplication($id) |
Sets application context. |
onSession($id) |
Sets session context. |
forEmail($driver, $configs, $recipients, $sendImmediately) |
Configures email delivery. |
with($data) |
Sets dynamic template variables. |
setSubject($subject) |
Overrides the email subject. |
withRenderedHtml($html, $subject, $payload) |
Uses pre-rendered HTML instead of template. |
withAttachments($attachments) |
Adds file attachments. |
toBroadcastChannels($channels) |
Sets custom broadcast channels for in-app. |
scheduledTo($carbon) |
Schedules delivery for a future time. |
preview($interpolate) |
Renders the notification without sending. Returns array. |
send() |
Dispatches the notification. Returns bool. |
Helper Function
A global helper is available for convenience:
// With arguments: returns NotificationBuilder meanify_notifications('template_key', $user, 'pt_BR') ->forEmail('smtp', $configs, ['user@example.com']) ->with(['code' => '123456']) ->send(); // Without arguments: returns NotificationUtils $email = meanify_notifications()->decryptRecipientToUnsubscribe($encryptedToken);
Sending Emails
NotificationBuilder::make('welcome_email', $user, 'pt_BR') ->forEmail('smtp', [ 'smtp_host' => 'smtp.example.com', 'smtp_port' => 587, 'smtp_encryption' => 'tls', 'smtp_username' => 'notifications@example.com', 'smtp_password' => 'your-encrypted-password', 'from_address' => 'noreply@example.com', 'from_name' => 'My App', ], ['recipient@example.com']) ->with([ 'user_name' => 'John Doe', 'activation_link' => 'https://example.com/activate/abc123', ]) ->send();
To send immediately (bypassing queue):
NotificationBuilder::make('otp_email', $user, 'en_US') ->forEmail('smtp', $configs, ['user@example.com'], sendImmediately: true) ->with(['otp' => '123456']) ->send();
Email Drivers
SMTP
->forEmail('smtp', [ 'smtp_host' => 'smtp.example.com', 'smtp_port' => 587, 'smtp_encryption' => 'tls', 'smtp_username' => 'user@example.com', 'smtp_password' => 'encrypted-password', 'from_address' => 'noreply@example.com', 'from_name' => 'App Name', ], $recipients)
Mailgun
->forEmail('mailgun', [ 'mailgun_domain' => 'mg.example.com', 'mailgun_api_key' => 'key-xxx', 'mailgun_endpoint' => 'https://api.mailgun.net', 'from_address' => 'noreply@example.com', 'from_name' => 'App Name', ], $recipients)
SendGrid
->forEmail('sendgrid', [ 'sendgrid_api_key' => 'SG.xxx', 'sendgrid_endpoint' => 'https://api.sendgrid.com', 'from_address' => 'noreply@example.com', 'from_name' => 'App Name', ], $recipients)
SendPulse
->forEmail('sendpulse', [ 'sendpulse_client_id' => 'client-id', 'sendpulse_client_secret' => 'client-secret', 'sendpulse_endpoint' => 'https://api.sendpulse.com', 'from_address' => 'noreply@example.com', 'from_name' => 'App Name', ], $recipients)
Attachments
Supports two formats: file path and base64 content.
NotificationBuilder::make('invoice_email', $user, 'pt_BR') ->forEmail('smtp', $configs, ['finance@example.com']) ->with(['invoice_number' => 'INV-001']) ->withAttachments([ // From file path [ 'path' => '/storage/invoices/inv-001.pdf', 'name' => 'Invoice-001.pdf', 'mime' => 'application/pdf', ], // From base64 content [ 'content' => base64_encode($fileData), 'name' => 'Report.xlsx', 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ], ]) ->send();
In-App Notifications & Broadcasting
Templates with in_app in available_channels will automatically dispatch broadcast events.
// Simple string channels NotificationBuilder::make('task_assigned', $user, 'pt_BR') ->with(['task' => 'Review PR #42']) ->toBroadcastChannels([ 'user.123', 'admin.dashboard', ]) ->send(); // Model-based obfuscated channels NotificationBuilder::make('project_update', $user, 'pt_BR') ->toBroadcastChannels([ ['model' => User::class, 'id' => $manager->id], ['model' => Team::class, 'id' => $team->id], ]) ->send(); // Channels with custom event names NotificationBuilder::make('alert', $user, 'en_US') ->toBroadcastChannels([ ['channel' => 'project.alerts', 'event' => 'project.completed'], ]) ->send();
Broadcast payload structure:
{
"id": 1,
"type": "task_assigned",
"short_message": "You were assigned to Review PR #42",
"created_at": "2025-01-15T10:30:00Z"
}
Default event name: in-app.notification (customizable per channel).
Scheduling
use Carbon\Carbon; NotificationBuilder::make('weekly_report', $user, 'pt_BR') ->forEmail('sendgrid', $configs, ['user@example.com']) ->with(['report_url' => 'https://...']) ->scheduledTo(Carbon::now()->addHours(2)) ->send();
Preview (Without Sending)
Render the notification HTML without dispatching:
// With template interpolation $preview = NotificationBuilder::make('welcome_email', $user, 'pt_BR') ->with(['user_name' => 'John', 'code' => '123456']) ->preview(); echo $preview['subject']; // Rendered subject echo $preview['html']; // Final HTML with layout // Without interpolation (raw placeholders preserved) $raw = NotificationBuilder::make('welcome_email', $user, 'pt_BR') ->preview(interpolate: false); echo $raw['body']; // "Hello {{ user_name }}, your code is {{ code }}"
Return format:
[
'subject' => 'Rendered subject',
'title' => 'Rendered title',
'body' => 'Rendered body content',
'html' => 'Complete HTML with email layout',
]
Pre-rendered HTML
Send custom HTML without using a template:
NotificationBuilder::make(null, $user, 'pt_BR') ->withRenderedHtml('<h1>Custom Email</h1><p>Hello!</p>', 'Custom Subject') ->forEmail('smtp', $configs, ['user@example.com']) ->send();
Setting Subject Override
Override the template's subject:
NotificationBuilder::make('generic_email', $user, 'pt_BR') ->setSubject('Urgent: Action Required') ->forEmail('smtp', $configs, ['user@example.com']) ->with(['message' => 'Please review immediately.']) ->send();
Template System
Template Interpolation Syntax
The TemplateInterpolator provides a lightweight template engine for notification content.
Variable Replacement
Hello {{ user_name }}, your code is {{ otp }}.
Conditionals
@if(is_premium)
Welcome, premium member!
@else
Upgrade to premium for more features.
@endif
Supported expressions:
- Simple:
@if(variable_name) - Negation:
@if(!variable_name) isset():@if(isset(optional_field))empty():@if(empty(comments))count():@if(count(items) > 0)- Logical
&&/||:@if(active && verified) - Combined:
@if((is_admin || is_moderator) && !banned)
Isset Blocks
@isset(discount_code)
Use code {{ discount_code }} for 10% off!
@endisset
Foreach Loops
Your recent orders:
@foreach(orders as $order)
- Order #{{ $order.id }}: {{ $order.total }}
@endforeach
Both {{ $item.key }} and {{ $item->key }} syntaxes are supported.
Complete Template Example
Hello {{ user_name }},
@if(has_orders)
Here are your recent orders:
@foreach(orders as $order)
- #{{ $order.id }}: ${{ $order.total }}
@endforeach
@else
You have no orders yet. Start shopping!
@endif
@isset(coupon_code)
Special offer: use code {{ coupon_code }} for a discount!
@endisset
Email Layouts
Layouts are Blade templates stored in the emails_layouts table. They wrap the notification body.
Available variables in layouts:
| Variable | Description |
|---|---|
$content |
Rendered email body |
$subject |
Email subject |
| Metadata keys | All keys from layout metadata JSON (e.g. $logo_url, $help_text) |
| Dynamic data | All keys from dynamic_data (template variables) |
Layout template example:
<html> <body> <header> <img src="{{ $logo_url }}" width="{{ $logo_width }}" /> </header> <main> {!! $content !!} </main> <footer> <p>{{ $help_text }}</p> <a href="{{ $privacy_url }}">{{ $privacy_text }}</a> </footer> </body> </html>
Environment variables for default layout seeder:
| Variable | Description |
|---|---|
APP_EMAIL_SRC |
Logo URL |
APP_EMAIL_WIDTH |
Logo width |
APP_EMAIL_HEIGHT |
Logo height |
APP_EMAIL_SHORT_CTA |
Button text (default: "Click here") |
APP_EMAIL_CTA_HELP |
Help text under CTA |
APP_EMAIL_HELP_TEXT |
Footer text |
APP_EMAIL_SOCIAL_LINK_* |
Social media URLs |
APP_EMAIL_PRIVACY_URL |
Privacy policy link |
APP_EMAIL_UNSUBSCRIBE_URL |
Unsubscribe link |
Recipient Filter
The recipient filter prevents email delivery to certain addresses. When enabled, it evaluates each recipient before sending.
Configuration
'email' => [ 'recipient_filter' => [ 'enabled' => true, 'blocked_domains' => ['example.com', 'mailinator.com', '*.test'], 'block_encrypted' => true, 'block_base64' => true, 'on_block_status' => 'simulated', // or 'skipped' ], ],
How It Works
Each recipient is checked in order:
- Encrypted detection — blocks if the email has no
@, starts witheyJ(JWT-like), or has a high ratio of non-alphanumeric characters. - Base64 detection — blocks if the local part (before
@) is a valid base64 string (8+ chars that decode and re-encode identically). - Domain matching — blocks if the domain matches any pattern in
blocked_domains. Supports exact match (example.com) and wildcard (*.testmatchesanything.test).
Behavior When Blocked
When all recipients are blocked:
on_block_status |
Notification Status | sent_at |
Use Case |
|---|---|---|---|
simulated |
simulated |
Filled (current time) | Testing/staging — simulates successful delivery |
skipped |
skipped |
null |
Production — clearly marks as not executed |
When some recipients are blocked, only the allowed recipients receive the email. Blocked recipients are logged.
Logging
All blocked recipients generate a Log::warning with:
Email notification simulated — recipients blocked by filter
or
Email notification skipped — recipients blocked by filter
Including: notification_id, blocked email list with reasons (blocked_domain, encrypted, base64_encoded).
Notification Statuses
| Status | Description |
|---|---|
pending |
Created, waiting to be dispatched |
processing |
Email job started, sending in progress |
sent |
Successfully delivered |
failed |
Delivery failed (see failed_log for details) |
simulated |
Blocked by recipient filter, saved as if sent |
skipped |
Blocked by recipient filter, marked as not executed |
Status constants are available on the Notification model:
use App\Models\Notification; Notification::NOTIFICATION_STATUS_PENDING; // 'pending' Notification::NOTIFICATION_STATUS_PROCESSING; // 'processing' Notification::NOTIFICATION_STATUS_SENT; // 'sent' Notification::NOTIFICATION_STATUS_FAILED; // 'failed' Notification::NOTIFICATION_STATUS_SIMULATED; // 'simulated' Notification::NOTIFICATION_STATUS_SKIPPED; // 'skipped'
Artisan Commands
Test Notification
Send a test notification from the command line:
php artisan meanify:notifications:test \
--template=welcome_email \
--user=1 \
--locale=pt_BR \
--vars='{"user_name":"John","activation_code":"123456"}'
Options:
| Option | Required | Description |
|---|---|---|
--template |
Yes | Notification template key |
--user |
Yes | User ID of the recipient |
--locale |
No | Language code (default: app.locale) |
--emails |
No | Comma-separated recipient emails (overrides default) |
--smtp |
No | SMTP settings ID from emails_settings table |
--account |
No | Account ID context |
--application |
No | Application ID context |
--session |
No | Session ID context |
--vars |
No | JSON object with template variables |
Examples:
# Basic test php artisan meanify:notifications:test --template=sign_in_otp --user=1 # With custom recipients php artisan meanify:notifications:test \ --template=test_email \ --user=1 \ --emails="test1@example.com,test2@example.com" # With context php artisan meanify:notifications:test \ --template=invite_new_user \ --user=1 \ --account=10 \ --application=20 \ --vars='{"first_name":"John","organization_name":"Acme"}'
Models Reference
All models are published to app/Models/ and can be customized.
| Model | Table | Key Features |
|---|---|---|
Notification |
notifications |
Encrypted payload, status tracking, user/template relations |
NotificationTemplate |
notifications_templates |
SoftDeletes, channel config, layout relation |
NotificationTemplateTranslation |
notifications_templates_translations |
SoftDeletes, locale-based content |
NotificationTemplateVariable |
notifications_templates_variables |
SoftDeletes, variable documentation |
EmailLayout |
emails_layouts |
SoftDeletes, metadata (array), Blade template |
Key Relationships
NotificationTemplate
├── layout() → belongsTo(EmailLayout)
├── translations() → hasMany(NotificationTemplateTranslation)
└── variables() → hasMany(NotificationTemplateVariable)
Notification
├── user() → belongsTo(User)
└── template() → belongsTo(NotificationTemplate)
License
MIT License. See LICENSE for details.