mattkingshott/quest
Quest is a lightweight state machine for Laravel. Define the states a model can occupy and the transitions allowed between them, then move models through your workflow with guard checks and events, keeping status logic expressive and out of your controllers.
Quest is a workflow/quest-definition package for Laravel: you describe a multi-step flow (steps, transitions, completion rules) and let the package drive models through it. v4.0.0's headline change is that behaviour which was previously hard-coded is now exposed through configuration, so the first thing to do on either a fresh install or an upgrade is locate that config.
Install and publish.
composer require caneara/quest
php artisan vendor:publish --tag="quest-config"
The tag name is worth confirming against the package README — v4 is the first release that ships a publishable config, so if the tag doesn't resolve, run php artisan vendor:publish interactively and pick the Quest provider.
Read config/quest.php before writing any quest code. This is the single most important step in v4. The config is where the package's defaults now live, and the values you see there tell you which knobs the maintainers consider supported versus incidental. Cross-reference it with the package's upgrade notes for v3 → v4, since a major version bump alongside "added configuration options" usually means some previously implicit behaviour is now only configurable, or a key was renamed rather than added.
Check the service provider's merge strategy. In config/quest.php you'll see whether the package uses mergeConfigFrom (your published file wins) or a full publish-only approach (you must re-publish to get new keys). This determines your upgrade procedure later.
First use case: one model, one short quest. Take an existing Eloquent model with an obvious lifecycle (an Order moving draft → paid → fulfilled, an Invoice moving issued → paid → void) and model it as a single quest with 3–4 steps. Resist the urge to model your entire domain on day one; the value of the package only becomes clear once you see how it handles the "what happens when a step fails / is skipped / is re-entered" cases, and those are easier to reason about on a small flow.
Verify the config actually takes effect. Change one obvious value in config/quest.php, run a quest, and confirm the behaviour changed. If it didn't, you're either cached (see Gotchas) or reading a config key the package doesn't consume in this version.
Where to look first, in order: config/quest.php → the package README's v4 section → the v3.0.0...v4.0.0 diff (it's small — one PR — and reading the actual diff is faster than inferring from release notes) → your own tests.
Config as the environment boundary. Now that options are configurable, keep the shape of your quests in code and the policy in config. A quest definition that hard-codes "retry 3 times" or "require approval" is a quest you'll fork later; the same definition reading config('quest.approval_required') is one you can vary between local, CI, and production without touching the class. This is the highest-leverage pattern v4 enables.
Publish-and-pin, don't publish-and-forget. Commit config/quest.php to the repo and treat it as application code. Because a major release changed the config surface, an unpinned, uncommitted config file is exactly the thing that silently diverges between developer machines.
Config overrides in tests, not quest subclassing. For test isolation, prefer setting config in the test (config()->set(...) in setUp, or Laravel's Config::set inside the test) over creating variant quest classes. This keeps the production class under test and makes the varying assumption explicit at the point of the assertion.
Keep quest definitions thin and push domain logic out. The steps should orchestrate; the business rules should live in your existing services/actions so they remain unit-testable without booting a quest. A quest step that is 40 lines of branching is a step you cannot test cheaply, and quest packages generally make it awkward to assert on intermediate step state.
Wire quest transitions to domain events. The natural integration point in a Laravel app is events/listeners: emit a domain event when a step completes and let listeners handle side effects (notifications, ledger entries, webhooks). This keeps the quest definition free of Mail:: and Http:: calls and means a config-driven behaviour change doesn't require re-testing your mailables.
Use it for orchestration, not for authorisation. Don't let quest state become your permission model. Check authorisation in policies/middleware as usual, and let the quest's config-driven rules decide sequencing. Mixing the two produces the classic bug where a config flag flips and users lose access they shouldn't have lost.
Upgrade workflow for v4 specifically. Treat this as a minor-looking major: bump the constraint, run composer update caneara/quest, re-publish the config to a temp location and diff it against your committed copy, then run your quest tests with config:clear first. Diffing the published config — rather than trusting the changelog — is the reliable way to find keys that were renamed or removed.
Config caching will bite you first. If you run php artisan config:cache in local or CI, edits to config/quest.php are invisible until you php artisan config:clear. Symptom: you change a value, behaviour doesn't change, you assume the config key is a no-op and go down a rabbit hole. Always config:clear before concluding anything about config.
"Added configuration options" in a major bump means something changed, not just appeared. The changelog is a one-liner. Read the actual PR/commit diff. Even if nothing was removed, a key that was previously a constant may now default to a value that differs from v3's hard-coded behaviour — meaning your tests pass but production behaviour subtly shifts. Pin the values explicitly in your committed config rather than relying on the package's new defaults.
Re-publishing overwrites your customisations. vendor:publish --force will clobber a committed config/quest.php. Publish to a temp path or diff before forcing. This is the most common way teams lose an afternoon after a package upgrade.
Don't rely on mergeConfigFrom for nested arrays. If Quest merges deeply-nested config, your published array can shadow sibling keys the package expects. Safer to publish the full file and keep it complete rather than publishing a partial override.
Debugging tip: dump the effective config, not the file. dd(config('quest')) inside a test or tinker session shows you what the package will actually read after merging and caching — which is frequently not what's in config/quest.php. When behaviour surprises you, this is the first thing to print.
Extension points to prefer. The stable extension surfaces are your own quest classes, your domain events/listeners, and the config file. Where possible, avoid extending the package's internals or overriding its service provider — v4 demonstrates that internals shift between majors, and code that subclasses them becomes the migration cost. If you need behaviour the config doesn't expose, open an issue upstream rather than monkey-patching; a one-PR release cadence suggests the maintainer is responsive.
Watch the first-contributor risk lightly. v4's config work came from a new contributor. That's healthy, but it means the config surface hasn't been battle-tested across many releases. Add one test that asserts your key config values hold, so a future patch release that changes a default fails loudly in CI rather than quietly in production.
How can I help you explore Laravel packages today?