welcomattic/messenger-central
Messenger Central integrates Symfony Messenger with Laravel, providing a central bus for dispatching messages and handling them through middleware, transports, and workers. Register handlers and route commands, events, and queries while leveraging Laravel's container and conventions.
Read the requirements before anything else — this one has a hard floor:
For a Laravel developer, the honest framing up front: this is not installable into a Laravel application. It drives messenger:consume processes from Symfony's Messenger component, which is a different abstraction from Laravel's queue/worker stack. You'll only get value from it if you also maintain a Symfony service (or are migrating handlers between the two). The conceptual equivalent in Laravel-land is Horizon — a supervising dashboard over your workers — but the interaction model here is a keyboard-driven TUI rather than a web UI.
First steps once installed as a dev dependency:
config/packages/messenger.yaml has at least one transport with a real routing map — the sidebar is only as useful as your routing config.bin/console messenger:central from a terminal (not from a process manager, not in CI).? immediately. Every key is listed there, and it's the fastest way to learn the scope of what the tool can do.Keep it in a dedicated tmux pane or split. It owns the terminal.
Replace your dev-loop messenger:consume calls. The workflow that pays off: run the TUI, then scale workers up and down from the keyboard as you exercise handlers, rather than killing and restarting consume by hand in several shells. The live view (incoming rate, per-worker message counts and handling durations, waiting messages, last handled/retried/failed) is the thing you'd otherwise reconstruct from logs and messenger:stats.
Use the transports sidebar as a routing sanity check. It shows the message classes routed to each transport alongside the transport options with credentials masked. That makes it safe to show on a screen share, and it catches routing mistakes early — e.g. a message class you thought was going to async that's actually falling through to sync.
Treat the failed-message browser as the debugging loop. The intended pattern is: open the paged failed list, read the error with the full stack trace, then either remove the message, resend it as-is, or edit its properties as JSON and resend. The JSON editing is the interesting part — it lets you fix a bad payload and requeue it against the same transport without writing a throwaway command. Work that loop while iterating on a handler that keeps failing on one poisoned message.
Don't treat worker scaling as supervision. Workers started from the TUI are ordinary messenger:consume processes, but their lifecycle is owned by the terminal session. This is not a replacement for Supervisor or systemd, and it persists nothing. Any pattern that assumes the supervisor outlives your shell is wrong here.
Rely on the injected MESSENGER_CONSUMER_NAME. Each spawned worker gets a unique value, which is what makes Redis transports with the consumer option behave (reserved/pending message tracking is per-consumer). Don't hardcode a fixed consumer name in transport options — you'll defeat the isolation the tool is providing and get workers fighting over the same pending set.
Concurrency is version-gated. Symfonly 8.2+ gets concurrent message handling per worker via amphp/parallel. On 8.1 you get the TUI but not the parallelism. If you're writing handlers that will run under concurrency, develop against 8.2 so you hit the shared-state issues early rather than in staging.
Integration tip: pair it with your test suite rather than trying to script it. The TUI is for interactive diagnosis; assertions about handler behaviour belong in tests that dispatch to an in-memory transport. Use the TUI when a test can't tell you why.
v0.1.0 means no backwards-compatibility promise. Pin the exact version in composer.json and expect keybindings, flags and the layout to move. Don't build team documentation around specific keypresses yet — point people at ?.
Debug-mode refusal is a feature, not an obstacle. It won't run when the kernel has debug off. Resist --force except on a throwaway environment; forcing it in anything prod-shaped gives you a terminal that spawns and respawns consumer processes outside your supervisor with no audit trail.
The auto-restart behaviour will mask your consume options. Workers that exit on their own are restarted immediately. So if you set --time-limit, --memory-limit or an idle timeout in your transport options, the TUI will silently respawn the worker each time it hits the limit — you lose the "recycle the worker" signal you were relying on. Set those deliberately, and don't be surprised by a worker count that never drops.
Crash backoff still restarts. Crashing workers come back after a growing delay. A poison message that kills the worker on deserialize will therefore loop forever at increasing intervals, and each restart adds output to the worker pane. If you see a worker cycling, go straight to the failed-messages view rather than watching the logs scroll.
pcntl changes stop semantics. With pcntl present, stopping a worker lets it finish the message in hand. Without it, stopping is abrupt — a handler can be interrupted mid-flight and you may see a message reappear or a partial side effect. If graceful shutdown matters to what you're testing, check php -m | grep pcntl first.
Concurrency breaks long-lived-process assumptions. Once several messages are handled at once inside a worker, anything you cached in a property, any static, any lazily-opened connection is shared. Watch for handlers that assume single-threaded execution: DB connections, HTTP clients with mutable state, dump() output interleaving from multiple messages. Also note that a worker under concurrency has more than one message in flight, so "stop" takes longer to drain.
Editing failed-message JSON is only practical for scalar properties. The edit is applied to the serialized properties document before it's sent back to the transport. Hand-edited JSON has to survive denormalization into the message class — nested objects, enums and anything relying on PHP native serialization are fragile here. If the edit fails validation, the message just fails again, which looks identical to the original bug. Resend as-is first to confirm the failure is payload-related, then edit.
Worker output is captured, not streamed cheaply. Logs and dump() land in the worker pane. Heavy dump() in a hot handler can make the TUI feel sluggish, which reads as a tool bug rather than a debugging artefact. Drop the dumps when you're done.
Credentials are masked in the UI but not in the environment. The sidebar masks transport options, but spawned consumers inherit the environment of the TUI process — so DSNs in env vars are still fully readable from the child processes' /proc entries. Masking is a shoulder-surfing guard, not a secrets boundary.
Narrow terminals truncate the sidebar. Give it a wide window or a large tmux pane; the transports pane and the live view compete for horizontal space, and the message-class list is the first thing to get clipped.
Nothing is recorded. There's no history, no metrics export, no persistence — close the terminal and the picture is gone. For anything you'll want to look at tomorrow, keep a real observability tool alongside it.
How can I help you explore Laravel packages today?