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

Jolinotif Laravel Package

jolicode/jolinotif

JoliNotif is a cross-platform PHP library for displaying desktop notifications from CLI scripts and CRON jobs on Linux, Windows (including WSL), and macOS. Install via Composer and send customizable notifications with title, body, icon, and driver-specific options. Requires PHP 8.3 or newer.

View on GitHub
Deep Wiki
Context7

Getting Started

JoliNotif is a CLI-only library — treat it as a channel for operator feedback, not for end-user notifications. Install it with Composer:

composer require jolicode/jolinotif

Requires PHP 8.3+ (v3.3+), and pulls in symfony/process and psr/log. Older tutorials reference NotifierFactory, Notifier, OsHelper and ToasterNotifier — all of that was removed in 3.0. The only public entry point you should build against is:

use Joli\JoliNotif\DefaultNotifier;
use Joli\JoliNotif\Notification;

$notifier = new DefaultNotifier();
$notifier->send(
    (new Notification())
        ->setTitle('Backup finished')
        ->setBody('42 files archived in 3m12s')
        ->setIcon(base_path('resources/icons/ok.png'))
);

Where to look first: run the bundled CLI binary to prove the host can actually display a notification before writing any Laravel code:

vendor/bin/jolinotif --title "Hello" --body "World" -v

The -v flag reports which driver was selected and surfaces the underlying symfony/process error output — this single command resolves ~80% of "it doesn't work" reports.

First real use case: finish-signal for long Artisan commands and deploys. Inside your command, $this->components->info() is invisible when you've tabbed away; a desktop toast is not. Same for php artisan queue:work supervisors and sail-based dev loops.

Then register it once in a service provider so nothing in your app news up the notifier directly:

// app/Providers/AppServiceProvider.php
$this->app->singleton(DefaultNotifier::class, fn () => new DefaultNotifier());

Implementation Patterns

Wrap it behind your own interface (do this first)

DefaultNotifier shells out to system binaries and cannot be exercised in CI. Hide it behind a one-method contract so tests and future providers (Slack, Ntfy) are swappable:

interface DesktopNotifier
{
    public function notify(string $title, string $body = '', ?string $icon = null): bool;
}

final class JoliNotifNotifier implements DesktopNotifier
{
    public function __construct(private DefaultNotifier $notifier) {}

    public function notify(string $title, string $body = '', ?string $icon = null): bool
    {
        if (! config('jolinotif.enabled')) {
            return false;
        }

        $notification = (new Notification())->setTitle($title)->setBody($body);

        if ($icon && is_file($icon)) {
            $notification->setIcon($icon); // absolute, canonical, readable
        }

        return $this->notifier->send($notification);
    }
}

Bind DesktopNotifier to JoliNotifNotifier in production and to a NullDesktopNotifier / spy in tests/. Never let a test pop a real toast — it can block on a hung binary.

Config gate

// config/jolinotif.php
return [
    'enabled' => env('JOLINOTIF_ENABLED', env('APP_ENV') === 'local'),
];

Notifications are noise on a shared staging box and meaningless on a headless CI runner. Gate on app()->runningInConsole() too — see Gotchas.

A custom Laravel notification channel

The idiomatic Laravel integration is a channel, so desktop toasts sit beside mail and slack on the same Notification class:

final class DesktopChannel
{
    public function __construct(private DesktopNotifier $notifier) {}

    public function send(object $notifiable, DesktopMessage $message): void
    {
        $this->notifier->notify($message->title, $message->body, $message->icon);
    }
}
// app/Notifications/DeployFinished.php
public function via(object $notifiable): array
{
    return config('jolinotif.enabled') ? ['desktop', 'mail'] : ['mail'];
}

public function toDesktop(object $notifiable): DesktopMessage
{
    return (new DesktopMessage)
        ->title("Deploy {$this->environment} finished")
        ->body("Tag {$this->tag} shipped in {$this->duration}s");
}

Then Notification::route('desktop', 'ops')->notify(new DeployFinished(...)). Keep it as a secondary channel — email or Slack remains the durable record; the toast is the glanceable one.

Queued jobs and the scheduler

Laravel's queue worker runs under the CLI SAPI, which is exactly what JoliNotif needs. So dispatching from an FPM request is fine as long as the actual send() happens in the worker:

// in a controller / listener — safe, the work happens in the worker
NotifyDeployFinished::dispatch($deploy);

Inside the job, make it the last thing you do, and implement ShouldQueue with $tries = 1 — a retried notification is a duplicate toast. Same for the scheduler:

$schedule->command('backup:run')
    ->dailyAt('02:00')
    ->then(fn () => app(DesktopNotifier::class)->notify('Backup done', 'nightly backup OK'));

then() callbacks run in the scheduler process, which is CLI — correct SAPI, but see the DBus caveat in Gotchas.

Per-driver options

addOption() is how you reach driver-specific features; unknown keys are silently ignored, so guard by OS if the distinction matters:

$notification->addOption('sound', 'Frog');        // macOS only
$notification->addOption('subtitle', 'staging');  // macOS AppleScript driver only
$notification->addOption('url', 'https://...');   // macOS TerminalNotifier only

Treat these as garnish. Never put information in an option that isn't also in the body.

Batch and throttle

A foreach over 500 rows calling send() spawns 500 subprocesses and paints the screen. Aggregate instead: one notification per batch ("3 of 500 imports failed — see storage/logs/import.log"), or funnel through a small rate-limited reporter that collapses duplicates within a window. Also remember desktop toasts often auto-dismiss in ~5s; anything that needs to survive belongs in the log, not the toast.

Gotchas and Tips

It cannot run in a web context. FPM/Octane/HTTP requests cannot display a desktop notification — there's no session bus and no display. The library deliberately disables the FFI libnotify driver when PHP_SAPI !== 'cli' (v3.1.1). Symptom: everything "works", send() returns false, nothing appears. Fix: only call it from Artisan commands, queue workers, and scheduler callbacks.

send() returning false is not an exception. It means no driver was found or the driver failed. In 3.x there's no NotifierFactory::createOrThrowException() anymore. Log the boolean and, during development, re-run the equivalent via vendor/bin/jolinotif -v to read the real stderr. Don't build business logic that assumes delivery.

DBus in cron and systemd services. The classic failure: notify-send works in your terminal but silently does nothing from cron. The cron/worker environment lacks DBUS_SESSION_BUS_ADDRESS and often DISPLAY. For a systemd user service running queue:work, inherit the graphical session:

[Service]
ExecStart=/usr/bin/php /var/www/app/artisan queue:work --queue=notifications
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

If the worker runs as root or as a system service, it will not reach the logged-in user's notification daemon at all — design around it (write to a log file the user tails).

Icons: absolute, canonical, readable. Relative paths and symlinks have historically broken (setIcon() canonicalises since 1.1.1, but a nonexistent or unreadable file just means no icon). Use resource_path(...) / base_path(...), PNG, and verify with is_file() before setting. On macOS, setIcon() maps to contentImage — an app-icon style square works better than a wide banner.

Bundled binaries and hardening. On Windows and macOS the library ships/executes helper binaries extracted to a temp directory; 3.4.1 specifically hardened extraction against cross-user tampering. If your PHP runs with open_basedir restrictions, a noexec /tmp, or a locked-down disable_functions (proc_open/exec), drivers will fail silently. Verify with vendor/bin/jolinotif -v and check proc_open availability.

Pin >= 3.4.1. 3.4.1 fixed argument injection in NotifySendDriver and script injection via Unicode quotes in PowerShellDriver. Related rule: never build a shell command string yourself around user input — always route text through the Notification object so the driver handles quoting. This matters the moment a toast includes a branch name, filename, or error message sourced from a pull request.

Windows/WSL expectations. Windows uses a PowerShell-based driver (3.2+) with a fallback path to powershell.exe under /mnt/c/... when it isn't on PATH (3.3.1). Windows toasts require an AppUserModelID context; from a bare console you may get nothing depending on the terminal host. If Windows is your primary target, test on the actual machine, not in WSL only.

Test doubles, not real toasts. Notification::fake() does not know about this channel. Fake your DesktopNotifier interface in TestCase::setUp() and assert on recorded calls. If you've skipped the wrapper interface, at minimum bind a no-op implementation in phpunit.xml via an env flag your provider reads.

No interactivity. There are no buttons, actions, or callbacks — title, body, icon, and a handful of macOS-only options. If you catch yourself wanting an "Open" action, you want Slack/email/a webhook, not JoliNotif. Keep the division: toast = "look at me now", log/mail = "the record".

Spare your teammates. In a shared dev environment, notifications fired by a queue:work running on someone else's laptop, or by a container reachable over SSH, either vanish or spam. Default enabled to false in .env.example and let developers opt in — the notification should be a courtesy, never a dependency of the command's success path.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource