hirethunk/verbs-commands
Command support for the Verbs event sourcing package for Laravel. Dispatch and handle commands alongside your aggregates and projections, keeping write-side intent explicit and your event-sourced domain organized and testable.
Treat this as a small, early-stage package you are evaluating rather than adopting blind. At 1 star, zero dependents, and a last release of 2024-06-05, there is effectively no community surface area — the source code is the documentation, so budget for reading it before you write a line of application code.
Verify installability against your app first. Check composer.json in the repo for php and illuminate/* constraints before running anything. A package last tagged in mid-2024 may not declare support for Laravel 11/12. Run:
composer require hirethunk/verbs-commands --dry-run
and read the resolution output carefully. If it fails, you already have your answer about fit.
Confirm how it boots. Look for extra.laravel.providers in its composer.json. If package discovery is wired up, the service provider registers automatically; if not, add it to bootstrap/providers.php (Laravel 11+) or config/app.php. This is the single most common reason a package like this appears "broken" on first install — nothing is bound in the container.
Read src/ in this order: the service provider (what it binds and whether it publishes config), any config/*.php (the actual knobs), then the command/handler contracts. For a package this small, that is a 15-minute read and worth more than any README.
Publish and diff config if a publishable config exists:
php artisan vendor:publish --provider="Hirethunk\\VerbsCommands\\...ServiceProvider"
Read the published file before changing anything — it usually reveals the intended dispatch mechanism (bus binding, handler resolution, queue connection).
First use case: pick one non-critical write path — a "create/update something" endpoint — and wire a single command DTO plus a single handler. Drive it from a feature test, not from a live controller. You want to learn the dispatch contract (synchronous bus? queued? return value?) in isolation before it touches domain logic.
Assuming the package follows the conventional CQRS shape its name implies (command objects + handlers resolved through the container), the day-to-day patterns are:
Thin commands, fat handlers. Keep command DTOs as readonly value objects — scalars, IDs, and primitives only. Everything else (model lookups, authorization, side effects) belongs in the handler. This keeps commands trivially serializable if you later queue them.
Dispatch from the edge, never from models. Controllers, console commands, and jobs are the only places that should construct and dispatch a command. Models dispatch events; they do not dispatch commands. Mixing the two produces circular write paths that are miserable to debug.
One handler per command, registered explicitly. Whatever registration mechanism the package exposes (attribute, $listen-style map, or container binding), make it explicit and greppable. If it relies on auto-resolution by naming convention, add a test that asserts every command in app/Commands resolves its handler — a missing binding should fail in CI, not in production.
Wrap in a transaction at the dispatch site, not the handler. If the handler touches multiple aggregates or fires events, the outer transaction belongs where the command is dispatched. This also means you can swap the handler implementation without touching transaction semantics.
Return a result DTO, not an Eloquent model. Handlers should return a small result object or the aggregate's ID. Returning models couples your command layer to the persistence layer and makes queued dispatch useless.
Test handler-invocation directly. Skip the bus in unit tests ($handler->handle(new CreateThing(...))) and exercise the bus only in one or two feature tests. This gives fast, readable tests and isolates bus misconfiguration to a single test file.
Integrate with Verbs deliberately. This package lives in the Hirethunk "Verbs" family, which is an event-sourcing toolkit. If you are also using the event store, the correct shape is: command → handler → aggregate method → domain events appended. Do not let the command bus and the event bus become two parallel write paths into the same aggregate.
Pin the exact version. With one tagged release and no activity since June 2024, use an exact constraint ("hirethunk/verbs-commands": "1.0.0") rather than ^1.0. A surprise patch from a dormant repo is unlikely but a surprise transitive break is not.
Assume you may need to fork it. Zero dependents means no one else is finding the bugs for you. Before it goes near a critical path, confirm you can vendor the package ("repositories" with a path or VCS entry) and that the code is small enough for one developer to own. MIT license means forking is legally trivial; the cost is maintenance attention, not permission.
Missing bindings are the #1 failure mode. If dispatch throws a container BindingResolutionException or "target is not instantiable," the cause is almost always: (a) provider not registered, (b) handler not registered with the bus, or (c) handler constructor has an unresolvable dependency. Work through those three in order before reading anything else.
Queued commands must be serializable. If the package supports queueing handlers, every property on your command must survive serialize(). No closures, no Eloquent models, no Carbon instances with custom state. Test this explicitly with a serialize(unserialize($command)) round-trip assertion — it catches problems that only surface under a real queue worker.
Check framework version support explicitly. Read the require block. If it caps at illuminate/support: ^10.0, composer why-not hirethunk/verbs-commands will tell you immediately on a Laravel 11/12 app. Do not try to force it with --ignore-platform-reqs on a production dependency.
Debugging dispatch: the three tools that actually help are php artisan about (confirms provider loaded), a Log::info() at the top of the handler (confirms it ran at all), and app()->bound(TheBusContract::class) in tinker. If the handler logs but the side effect doesn't persist, the problem is your transaction boundary, not the package.
Do not run two buses. If your app already has a command bus (Laravel's Bus, a custom CQRS layer, or Verbs' own), adding this alongside it produces ambiguity about which path a given write takes. Either make this the only command layer or scope it to a single bounded context and document that boundary in the codebase.
Extension points to prefer: decorating the bus/handler-resolver via a container binding in your own provider, and overriding published config. Avoid editing vendor/ — with no release cadence, your changes will not survive the next composer update and there is no upstream to send them to.
How can I help you explore Laravel packages today?