tomatophp/tomato-notifications
tomatophp/tomato-notifications adds a ready-made notification layer to TomatoPHP and Laravel apps, with database-backed alerts, Filament-friendly UI hooks, and configurable channels to keep users informed.
This is an ecosystem add-on, not a standalone package. Confirm before you composer require:
tomato-admin, and likely tomato-settings / tomato-roles) already installed and bootingIf you are not on Filament/Tomato, stop and read "Fork and prune" under Gotchas first. Pulling a full admin panel in as a transitive dependency to get one template table plus one channel class is a bad trade.
composer require tomatophp/tomato-notifications
php artisan vendor:publish --provider="TomatoPHP\TomatoNotifications\TomatoNotificationsServiceProvider"
php artisan migrate
Pin the exact version from the start — edit composer.json to "tomatophp/tomato-notifications": "1.1.16" rather than letting ^1.1 float. Last release was 2024-06-12 and there is no upgrade cadence to rely on.
Before migrating, run:
composer why-not laravel/framework
composer why-not filament/filament
and diff the published migration's table names against your existing notifications table and any template tables.
config/notifications.php or the Notification facade.{{ $var }}-style placeholders are resolved. If you see Blade::render or eval, that is a security decision, not a code-style preference. Flag it immediately.Notification::send() or replaces it, and whether failures bubble to your exception handler.Do not start with your production notification suite. Start with one low-stakes, ops-composed announcement:
user_name).That last step tells you more about operational fitness than the happy path does. Budget half a day.
Keep using Laravel's native Notification class as your entry point. The package's value is the template lookup, not a replacement send API:
class OrderShipped extends Notification implements ShouldQueue
{
public function __construct(public Order $order) {}
public function via($notifiable): array
{
return ['database']; // the package's channel, once verified
}
public function toDatabase($notifiable): array
{
return app(TemplateResolver::class)
->render('order-shipped', [
'user_name' => $notifiable->name,
'order_ref' => $this->order->reference,
]);
}
}
Wrap the package's resolver behind your own interface from day one. It is 20 lines and it means a fork, a swap, or an in-house replacement never touches a call site.
This is the actual product win: someone in support edits copy in Filament, hits send, users get an in-app notification, no deploy. Use it for:
Keep a hard boundary: ops-composed templates are for internal and MVP surfaces only. Anything transactional or regulated (receipts, password resets, legal notices) stays in code where it is version-controlled and tested. Encode that boundary in your template naming convention (ops.* vs tx.*) so it is visible in the admin list.
The package persists notifications with read state, so your inbox is mostly a read:
// controller
$notifications = auth()->user()
->notifications() // Laravel's DatabaseNotification relation
->latest()
->paginate(20);
$unreadCount = auth()->user()->unreadNotifications()->count();
Mark-as-read on view or on explicit action — decide once, document it, and be consistent. Do not rely on the package's admin views for the user-facing inbox; you almost certainly want that in your own UI, matched to your design system.
If you are replacing hardcoded notification strings:
No big-bang switch. A silently wrong template renders as plausible-looking text and will not page anyone.
There is no evidence of a test suite in the package. Write contract tests for the parts you depend on:
it('renders a template with all variables supplied', function () {
$out = app(TemplateResolver::class)->render('order-shipped', [
'user_name' => 'Ada', 'order_ref' => 'A-1',
]);
expect($out)->toContain('Ada')->toContain('A-1');
});
it('fails loudly on a missing variable', function () {
app(TemplateResolver::class)->render('order-shipped', ['user_name' => 'Ada']);
})->throws(/* whatever it actually does — pin the behaviour */);
That second test is the one that matters. Whatever the package does today becomes the contract you maintain, including after a fork.
If templates are a single row per name with no locale column, do not fight it inside the package. Resolve locale before the lookup — name templates order-shipped.en, order-shipped.fr and select the key from app()->getLocale(). That keeps you free to move to a proper (name, locale) unique index later.
If templates compile through Blade::render or eval, then template-edit permission is code-execution permission. That reframes the GUI selling point as a security review item. Check this before you give anyone but a developer access to the admin composer. Safe answer is restricted substitution against a whitelisted variable set; anything else needs sign-off from whoever owns your security posture.
If a queued job resolves the template when it runs rather than when it is dispatched, then editing or deleting a template mid-flight changes what already-queued users receive. Test it explicitly: dispatch, edit the template, then work the queue. If it resolves late, either snapshot the rendered subject/body into the payload yourself or accept that edits are retroactive — and document that behaviour, because it will surprise someone at the worst moment.
The package publishes migrations. Verify the table names do not shadow a notifications, notification_templates, or templates table you already own. Same for config/tomato-notifications.php — check it does not shadow a config/notifications.php of your own, and that the provider's auto-discovery does not register routes or views that collide.
There is no evidence of save-time variable validation. Assume a missing placeholder surfaces at send time as a blank, a literal {{ $foo }}, or an exception — and assume it will be the third one only on the day it matters. Add a validation rule on your own template form that parses placeholders and diffs them against a known variable list.
Confirm failures reach your error tracker. If the channel catches and Log::errors, you will find out a notification did not go out from a support ticket rather than an alert. Wire the channel into your existing exception reporting before you rely on it.
The admin resource ties you to a Filament major version. When you upgrade Filament, this package may or may not follow — the release cadence suggests you should not assume it will. Run composer why-not as part of every Filament bump, and add composer audit plus a dependency-review gate to CI.
MIT means forking is legally clean and your downside is bounded to engineering time, not lock-in. If you are not already on Tomato admin, the recommended shape is:
The in-house equivalent — a notification_templates table, a cached resolver, and a channel over a whitelisted variable set — is roughly 150–250 lines. Get that estimate from your team in the same window as the spike; it is the real buy-vs-build threshold.
Near-zero day-to-day if you consume it unmodified, but every Laravel/Filament major accrues incompatibility risk with no guaranteed upstream fix. Budget 1–3 engineer-days per quarter and name an owner before you merge the install. If nobody will own it, do not adopt it.
log channel first, before database — you see the rendered output without a queue round-trip or a DB query.toDatabase() rather than in the channel class; that isolates "template rendered wrong" from "channel delivered wrong".How can I help you explore Laravel packages today?