fof/horizon
Brings Laravel Horizon's queue monitoring dashboard into Flarum's admin area, letting administrators inspect Redis-backed queues, recent jobs, throughput, and worker status. Requires Redis and proper Horizon setup, making it best suited to advanced or high-traffic forums.
fof/horizon is a Flarum extension that wires Laravel Horizon into a Flarum installation, so the Horizon dashboard (queues, throughput, wait times, failed jobs, retries, metrics) is available inside the Flarum admin panel instead of as a standalone Laravel app. You are effectively running Laravel's queue stack under Flarum's bootstrap, so everything you know about Horizon carries over — with Flarum's config conventions on top.
Minimal steps:
composer require fof/horizon
php flarum extension:enable fof/horizon
php flarum cache:clear
Prerequisites before you enable it:
database/sync will not work).ext-pcntl and ext-posix if you run Horizon supervisors on Linux.config.php (or the env vars it reads), e.g. 'queue' => ['driver' => 'redis', ...] with a valid redis connection block.php flarum queue:work in your process manager with Horizon's supervisor.Where to look first:
/admin/horizon). Confirm it renders and that "Processes" shows a supervisor.vendor/fof/horizon/src/ — the service provider is where the extension merges Horizon's config, registers the route, and (likely) gates access to admins. Read this before changing anything; it tells you which config keys are actually honored.vendor/laravel/horizon/config/horizon.php — the canonical defaults for environments, waits, balance, trim, and metrics. Any override the extension supports will be layered on top of this.First use case: run one real job through it — a queued notification or an email from another extension — then watch it appear in the "Recent Jobs" list and confirm it's tagged and completed. That single round trip proves wiring, supervisor, and Redis connectivity at once.
Run Horizon as the queue worker, not alongside it. The whole point is that php flarum horizon becomes your single supervisor process; remove any queue:work cron or systemd unit. Running both means two consumers competing for the same queues and confusing metrics.
[program:flarum-horizon]
command=php /var/www/flarum/flarum horizon
directory=/var/www/flarum
autostart=true
autorestart=true
user=www-data
numprocs=1
stopwaitsecs=3600
Design jobs around Horizon's visibility. Any job implementing ShouldQueue shows up automatically, but you get far more out of it if you:
class SendDigest implements ShouldQueue
{
public $queue = 'notifications';
public $tries = 3;
public $backoff = [30, 120, 300];
public function tags(): array
{
return ['digest', 'user:'.$this->user->id];
}
}
Tags are what make the "search by tag" and per-user filtering useful when a batch of queued notifications goes sideways.
Keep queue names in sync with environments. Horizon only supervises queues listed in the active environment's config. A job pushed ->onQueue('reports') when reports isn't in the supervisor config will sit invisible forever. Pick a small, explicit set of queue names (e.g. default, notifications, exports) and put them in both the job and the Horizon environment block.
Snapshot on a cron. Horizon's metrics graphs are populated by horizon:snapshot, and Flarum has no scheduler to do it for you. Add:
*/5 * * * * cd /var/www/flarum && php flarum horizon:snapshot >> /dev/null 2>&1
Without it the "Metrics" tab stays empty and you'll waste an afternoon assuming the extension is broken.
Terminate on deploy. Horizon supervisors hold your job code in memory. After every composer install / git pull, run:
php flarum horizon:terminate
Horizon's master process respawns workers with fresh code. Put this at the end of your deploy script next to php flarum cache:clear (order matters: clear the Flarum cache first, terminate second).
Use Horizon's own commands for triage rather than digging in Redis by hand:
php flarum horizon:status # is a master running?
php flarum horizon:list # supervisors and queue assignments
php flarum horizon:clear --queue=notifications
php flarum horizon:retry <id>
Isolate slow work. Anything doing HTTP calls or PDF generation belongs on its own queue with its own waits tolerance, otherwise a slow export blocks notification delivery and the dashboard's wait-time graph becomes meaningless.
database queue driver. If you're on a shared host without Redis, this package can't help you; if you're on Redis but Flarum's config.php still says database, jobs never reach Horizon.laravel/horizon's defaults. If you edit vendor/laravel/horizon/config/horizon.php directly, your changes survive until the next composer update — put overrides wherever the FoF provider reads them (inspect the provider to know exactly) and re-run php flarum cache:clear after every change, since Flarum caches config.php.queue:failed / queue:retry commands operating on the failed_jobs table, while Horizon keeps failures in Redis. Depending on config you can end up with two failure stores and two retry paths. Pick Horizon as the source of truth once enabled and don't script Flarum's failure commands against the same queue.Horizon::auth() gate isn't the thing protecting this route — the extension registers it under Flarum's admin middleware. Don't expose the route publicly by weakening that middleware; if non-admins need visibility, give them read-only metrics rather than dashboard access.horizon:status returning "inactive" while jobs still process usually means an orphaned queue:work from before the migration. Kill it and confirm only the Horizon master is consuming.memory limits and maxTime per worker in the supervisor config; Flarum jobs that pull large Eloquent graphs leak over time and an unbounded worker will OOM the box. horizon:terminate on a nightly cron is a cheap safety net.composer.json, read the source of the service provider (it's small), and have a fallback plan: uninstalling it and reverting to queue:work plus queue:failed is a valid exit strategy if a Flarum upgrade breaks it.JobProcessing / JobProcessed events still fire under Flarum, so if you want to pipe queue telemetry into an external APM, listen there rather than patching the extension.How can I help you explore Laravel packages today?