Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tomato Notifications Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites

This is an ecosystem add-on, not a standalone package. Confirm before you composer require:

  • PHP 8.1+, Laravel 10/11
  • Filament 3 and the Tomato admin stack (tomato-admin, and likely tomato-settings / tomato-roles) already installed and booting
  • A working queue worker — the package advertises queued delivery, and everything below assumes it

If 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.

Install and publish

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.

Where to look first (in this order, ~30 minutes)

  1. The service provider — what it auto-discovers, what routes/views/commands it registers, whether it touches config/notifications.php or the Notification facade.
  2. The template model + migration — column set, whether locale and variables are first-class, table name.
  3. The renderer — how {{ $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.
  4. The channel class — which Laravel channels it wraps, whether it delegates to Notification::send() or replaces it, and whether failures bubble to your exception handler.
  5. The Filament resource — the admin compose/view screens. This is the part you may end up dropping.

First use case

Do not start with your production notification suite. Start with one low-stakes, ops-composed announcement:

  1. Create a template in the admin panel with a single variable (e.g. user_name).
  2. Send it to yourself through the GUI.
  3. Confirm the row lands in the database, the read/unread flag renders in whatever inbox surface you wire up, and the queue worker processes it.
  4. Then break it deliberately: remove a variable from the payload and re-send. Watch whether you get a validation error, an exception, a silently mangled message, or a blank in the UI.

That last step tells you more about operational fitness than the happy path does. Budget half a day.


Implementation Patterns

Pattern 1 — Developer-authored notification, template-backed body

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.

Pattern 2 — Ops-composed broadcast via the admin panel

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:

  • Onboarding nudges and feature announcements
  • System/maintenance alerts
  • Copy that changes more often than the code around it

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.

Pattern 3 — Inbox read/unread surface

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.

Pattern 4 — Channel-by-channel migration behind flags

If you are replacing hardcoded notification strings:

  1. Run the templated path in parallel with the incumbent.
  2. Log both rendered outputs and diff them for a week of real traffic.
  3. Cut over one notification type at a time behind a config flag.

No big-bang switch. A silently wrong template renders as plausible-looking text and will not page anyone.

Pattern 5 — Testing

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.

Pattern 6 — Localization

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.


Gotchas and Tips

The renderer is the whole risk register

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.

Resolve-at-execution vs snapshot-into-payload

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.

Table-name collision

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.

Missing variables fail late, or not at all

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.

Silent channel failures

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.

Coupling to Filament majors

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.

Fork and prune is a legitimate first choice

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:

  • Keep: template model, migration, resolver, channel class
  • Drop: the Filament resource — expose templates through your own admin surface
  • Own: the render engine and the dependency surface

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.

Ownership is the actual cost

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.

Debugging quickly

  • Send through the log channel first, before database — you see the rendered output without a queue round-trip or a DB query.
  • If the payload looks wrong, dump inside toDatabase() rather than in the channel class; that isolates "template rendered wrong" from "channel delivered wrong".
  • After forking, keep the original repo as an upstream remote so you can eyeball diffs rather than rediscover changes.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator