Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fluent Validation Laravel Package

juststeveking/fluent-validation

Define Laravel validation rules with a fluent, chainable PHP API instead of pipe-delimited strings. juststeveking/fluent-validation keeps rules readable, composable, and easy to maintain across form requests and controllers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, then immediately prove it runs on your stack. The last release was 2022-11-10 — a Laravel 9 / PHP 8.1-era package with a single maintainer and zero dependents. Read the composer.json constraints, then verify on a throwaway branch against your actual Laravel/PHP matrix before it ever lands in a feature PR.

composer require juststeveking/fluent-validation

There is no config to publish and, in the normal case, no service provider to register — this is a set of rule builders that compiles to native Laravel rules, not a framework extension. If Composer resolves and the classes autoload, you are installed.

Where to look first, in order:

  1. src/ — the builder classes and the base/factory entry point. Ten minutes of reading tells you exactly which methods exist and what each one expands to. There is no magic; the value is the chain, not hidden behavior.
  2. tests/ — the de-facto spec. The README is thin, so tests are the authoritative answer to "what does Rules::string()->required()->max(255) actually return?"
  3. composer.json — framework constraints. This is your compatibility gate.

First use case: pick one login or registration FormRequest and port a single rules() method.

// before
public function rules(): array
{
    return [
        'email' => 'required|string|email|max:255|unique:users,email',
        'password' => 'required|string|min:8|confirmed',
    ];
}

// after
public function rules(): array
{
    return [
        'email' => Rules::string()->required()->email()->max(255)->unique('users', 'email')->toArray(),
        'password' => Rules::string()->required()->min(8)->confirmed()->toArray(),
    ];
}

The win is legibility and IDE autocompletion; the risk is behavior drift. Prove the compiled output is identical to the string version before merging — see Gotchas.

Implementation Patterns

Compile inside rules(), never store builder objects. Treat the fluent chain as an authoring DSL that collapses to native rules at the boundary. Do the ->toArray() call in rules() so FormRequest always receives a plain array. This keeps the package behind one seam and makes a native rollback a one-file change.

public function rules(): array
{
    return [
        'name'     => Rules::string()->required()->max(120)->toArray(),
        'email'    => Rules::string()->required()->email()->max(255)->unique('users')->toArray(),
        'age'      => Rules::integer()->nullable()->between(18, 120)->toArray(),
        'accepted' => Rules::boolean()->required()->toArray(),
    ];
}

Extract shared rule sets into small named helpers. The highest-leverage use is repeated rule fragments — passwords, emails, slugs, addresses. Wrap them so call sites are semantic and the package stays replaceable.

final class UserRules
{
    public static function email(): array
    {
        return Rules::string()->required()->email()->max(255)->unique('users', 'email')->toArray();
    }

    public static function password(): array
    {
        return Rules::string()->required()->min(8)->confirmed()->toArray();
    }
}

// FormRequest
public function rules(): array
{
    return [
        'email'    => UserRules::email(),
        'password' => UserRules::password(),
    ];
}

That indirection is the whole game plan from a risk standpoint: if you later fork, replace, or drop the package, UserRules is the only class you rewrite.

Mix with native rules at the same level, but don't mix inside the same rule. Fluent builders produce arrays; native strings, Rule:: objects, and custom Rule classes all coexist fine as sibling values. What you should avoid is trying to make one field's constraint half-fluent, half-string — that is where shapes get confusing.

public function rules(): array
{
    return [
        'email'    => UserRules::email(),
        'role'     => ['required', Rule::in(Role::cases())],           // native
        'avatar'   => ['nullable', 'image', new MaxFileSize(2048)],    // native + custom Rule
    ];
}

Keep conditionals and nesting native. sometimes, required_if, required_with, exclude_unless, nested array validation, and wildcards are where fluent helpers tend to thin out. If the builder supports them, verify the compiled output carefully; if it doesn't, leave that field as a native rule set. The two idioms can coexist in one rules() array — that is normal and fine.

Write regression tests that assert on the compiled array, not on behavior. This is the single most valuable habit with this package. Capture the native rules you expect, then assert the fluent builder produces the same shape. That converts "sensible defaults" from a promise into a CI contract, and it means a Laravel upgrade that changes toArray() output fails loudly in tests rather than softly in a 422 ticket.

it('compiles the login email rules to the expected native shape', function () {
    expect(UserRules::email())->toBe([
        'required', 'string', 'email', 'max:255', 'unique:users,email',
    ]);
});

If the package emits a different array shape (rule => constraint map vs. flat list), normalize at the boundary with a single array_map() helper and test that — then every call site uses the same normalized shape and your test asserts a stable contract.

Wrap it once, early. Even if you don't write a full façade, create one small internal entry point (e.g., a Validation class exposing email(), password(), slug()) that internally calls the package. Day-to-day usage feels identical, and a future swap is a single-class rewrite instead of a codebase hunt.

Custom rules: implement them as native Rule objects and drop them in alongside fluent output. Don't try to force them through the builder's chain unless the package explicitly supports it — a native Rule object compiles to the array cleanly and is auditable.

Gotchas and Tips

Confirm the output shape before writing anything real. The most common surprise with fluent-validation-style packages is the shape of the compiled array: flat string lists ('required', 'string', 'max:255') versus keyed maps ('rule' => 'max', 'value' => 255) versus a mixed form with objects. Whatever it is, pick it, normalize it at the FormRequest boundary, and never let the shape leak into serialization, OpenAPI generation, logging, or error-message customisation. One shape, one helper, one test.

Rule order still matters — fluently or not. bail, nullable, sometimes, and required semantics are order-sensitive in Laravel. A fluent chain implies order, so make sure the compiled array preserves it and matches what you intended with the string form. Diff the arrays; don't eyeball them.

Pin the version and commit the lockfile. Single maintainer, ~3 years without a release, zero dependents — you are the canary for Laravel major-upgrade breakage. composer require juststeveking/fluent-validation:^0.0.2 (or your resolved version), commit composer.lock, and add one dedicated CI job that runs the package's own tests plus your regression tests against each Laravel version you support. This is a five-minute setup that pays for itself the first time a Laravel 11/12 upgrade lands.

"Sensible defaults" can silently change behavior. Read the tests for each helper you use — specifically the exact constraints a helper like string() or email() expands into (max? between? unique scoping?). If a helper imposes a constraint you didn't ask for, either don't use that helper or override it. Silent defaults are the primary defect source with this package, and only the test suite catches them.

Localization and error messages are untouched. The package re-shapes authoring, not output. If your app customizes lang/*/validation.php or passes custom messages to validate(), nothing changes — but verify, because if the builder emits rules in a non-standard shape, message key lookup can break. Confirm with a $request->validate() test that asserts a specific error message string, not just that an error occurred.

Advanced surfaces stay native. sometimes, required_if, Password::defaults(), nested wildcards, after hooks, exclude_if, and conditional rules are where this package is weakest. If you need those, use native rules — even in the same rules() method. The package's job is the common 80%; using it everywhere is how you get a second, half-supported validation idiom.

Debugging: dump the compiled array first. When a rule "doesn't work," the cause is almost always a shape mismatch, not a behavior bug. Add a temporary dd($this->rules()) in the FormRequest or dd(UserRules::email()) at the call site and compare against the string form you'd have written. If the arrays match, the problem is upstream (request payload, middleware, authorize()); if they don't, you've found the defect.

Extension points worth knowing:

  • Custom rules: use native Rule objects as siblings; they compile cleanly.
  • Macro / factory wrapper: if the package exposes a static entry point (a Rules/Rule class), consider a thin app-level façade that returns your house style, so call sites are short and the package is one use statement deep.
  • Fork contingency: the MIT license means copying src/ into app/Support/Validation/ and dropping the dependency is an afternoon's work. Do that if the package ever blocks a Laravel upgrade — it's cheaper than maintaining a fork in a public repo, and it makes the code yours to edit.

Onboarding note: juniors and auditors read strings fine. A fluent chain is clearer for common rules and opaquer for people who don't know the package. Put the compiled-output regression test next to the fluent rule, or a short "how we write rules here" note in the repo — otherwise the DSL reads as magic to the next person.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource