onursimsek/precondition
Precondition is a Laravel package for declaring and checking preconditions before your application logic runs. Define requirements once, verify them automatically, and keep controllers and actions clean with reusable guard-style rules.
Start with the README and then the src/ directory—the package is small, so the contract interface (usually something like Precondition with a passes()/response() pair) is the whole mental model. Everything else is glue.
composer require onursimsek/precondition
php artisan vendor:publish --tag=precondition-config # if a config is shipped
First use case: guard a route with a single check you currently repeat in three controllers.
// app/Http/Preconditions/TeamIsActive.php
class TeamIsActive implements Precondition
{
public function __construct(private Team $team) {}
public function passes(Request $request): bool
{
return $this->team->is_active;
}
public function response(Request $request): Response
{
return response()->json(['message' => 'Team suspended'], 412);
}
}
Attach it where the route is declared, not inside the controller—the value of the package is that the failure path is declared next to the route and never reaches your handler. Verify it once by hitting the route with a failing fixture and confirming you get the configured status (412 by default) and not a 500 from a downstream null.
One precondition per concern. Resist the temptation to write CanAccessOrder that checks ownership, status, payment, and tenancy. Four small classes stay testable and reorderable; one fat class becomes the new god-controller.
Route-level composition for API endpoints. Preconditions read best as a declarative list on the route group, which makes the failure surface obvious in code review:
Route::get('/invoices/{invoice}', ShowInvoice::class)
->preconditions([OwnsInvoice::class, InvoiceIsFinalized::class]);
Optimistic locking for write endpoints. If the package exposes conditional-request support (ETag / If-Match), this is the killer day-to-day use: return an ETag on GET, require If-Match on PUT/PATCH, and let a stale client get a 412 instead of silently clobbering a colleague's edit. Pair it with Last-Modified/If-Modified-Since on read-heavy resources so repeat polls return 304 with no body.
Feature flags and maintenance gates. Preconditions are a clean home for "is this route enabled in this environment" logic that otherwise leaks into middleware stacks. Keep them in app/Http/Preconditions grouped by domain so the set doesn't sprawl.
Consistent error envelopes. Centralize the failing response in a base class or trait (ApiPrecondition) that emits your standard {message, code, errors} shape. Do not return raw strings from individual preconditions—that's how you end up with three JSON shapes across one API.
Test preconditions in isolation, then once through the route. Unit-test passes() with plain model factories—no HTTP. Then write a single feature test per route asserting the 412 and the message; that is enough to catch wiring regressions without duplicating every unit case.
$request, not from singletons. Anything bound as a singleton will serve stale data across Octane workers.php artisan config:cache in CI, not just locally. A published config referencing env() at the top level silently returns null in production—this is the single most common "works on my machine" bug with small packages.W/. ETag: abc123 is invalid; ETag: "abc123" is not. Compare with ===, not ==, and regenerate the ETag whenever the representation changes—not on every request.response('', 304) with headers set after the fact can leak content through middleware that appends to the body.passes() inputs, not by dumping the whole request. The usual culprit is a stale relation ($team->is_active on a cached model) rather than logic.vendor/—this package is small enough that a fork is tempting, but a container binding plus a base class covers 90% of customization.composer.json and keep a short integration test around the one behavior you depend on so an upgrade that changes default status codes or ordering fails loudly in CI.How can I help you explore Laravel packages today?