square/laravel-hyrule
Laravel package from Square for defining expressive, reusable rules that determine whether a user can perform an action on a model. Rules live in their own classes with a fluent API, keeping authorization logic out of controllers and policies while integrating cleanly with Laravel.
composer require square/laravel-hyrule — package auto-discovery registers the service provider and the Hyrule facade, so there is no config to publish before your first build.Square\Hyrule\Hyrule::create(), which returns a root node you hang fields off; ->toArray() returns a plain Laravel rules array you can hand to Validator::make(), $request->validate(), or a FormRequest's rules().use Square\Hyrule\Hyrule;
$rules = Hyrule::create()
->string('name')->required()->max(255)->end()
->string('email')->required()->email()->end()
->integer('age')->nullable()->min(18)->end()
->toArray();
'items.*.price' => 'required|numeric|min:0' string soup, and the builder output is plain strings, so nothing about your existing validation tests changes.rules() and keep messages() / attributes() / withValidator() as-is — Hyrule only owns the rules array, not the request lifecycle, so cross-field logic still belongs in after() hooks or withValidator().array_merge() the result into the existing array. Assert the generated array in a unit test against your old hand-written array before deleting it; that catches regressions for free.->end(), so callers can keep chaining siblings:function addressRules($node) {
return $node->string('street')->required()->end()
->string('city')->required()->end()
->string('postcode')->required()->end();
}
$rules = addressRules(Hyrule::create()->object('address'))
->end()
->toArray();
items.*.sku) and hoping they stay in sync with the shape you validate.exclude_if, exclude_unless, exclude_with, exclude_without, prohibited_if, prohibited_unless, missing_if, missing_unless, missing_with let you model partial-update and "present but forbidden" endpoints inside the builder instead of dropping back to raw strings:Hyrule::create()
->string('nickname')->exclude_if('use_real_name', true)->max(50)->end()
->string('tax_id')->prohibited_if('country', 'US')->end()
->toArray();
exclude_* family). Keep raw rule strings or Rule objects for anything the builder doesn't cover yet rather than blocking on an upstream release.exists, unique) are why the Laravel wrapper exists as a separate package from square/hyrule — these need the application container/database manager to resolve connections and tables, so they behave differently from the pure-string rules. Confirm the exact method signatures in the README before relying on them in bulk.illuminate/* and PHP while dropping older versions. Before upgrading, diff the published composer.json constraints; on Laravel 8/9 you should stay on 2.3.x and pin it. Because rule-string output changed (new capabilities, new normalisation), re-run your full validation test suite — including HTTP-level assertInvalid() assertions — not just unit tests on toArray().exclude_* and required are contradictory. exclude_if removes the field from the validated data; pairing it with required produces behaviour that reads as a bug. Use exclude_if instead of required for conditional fields, and remember the excluded field is absent from validated(), so guard downstream code with ??.nullable() (or sometimes). The builder doesn't change Laravel's semantics: a non-required email rule still fails on a missing key. This is the most common "the builder broke my rules" false alarm.bail is not implicit. The builder doesn't reorder rules, so add bail() explicitly wherever you depended on short-circuiting.->end() is a silent structural bug. You silently keep chaining on the field node and get a BadMethodCallException (or a rule attached to the wrong key) at runtime. The builder is dynamically typed — add PHPStan/Psalm if you want the tree shape checked; otherwise dd($rules->toArray()) after each migration step is the fastest debugging tool, since the output is just plain Laravel strings.items.*.name) in a test. A mis-nested node produces a valid-looking array that simply never matches your payload, and validation silently passes.exists/unique resolution needs the container — those tests must extend the Laravel TestCase (createApplication()), not raw PHPUnit.How can I help you explore Laravel packages today?