sandermuller/laravel-fluent-validation-rector

Rector rules for migrating Laravel validation to sandermuller/laravel-fluent-validation

Maintainers

Package info

github.com/SanderMuller/laravel-fluent-validation-rector

Homepage

Type:rector-extension

pkg:composer/sandermuller/laravel-fluent-validation-rector

Statistics

Installs: 206

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

0.5.0 2026-04-15 13:28 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status GitHub PHPStan Action Status

Rector rules for migrating Laravel validation to sandermuller/laravel-fluent-validation. Pipe-delimited strings, array-based rules, Rule:: objects, and Livewire #[Rule] attributes all convert to FluentRule method chains.

// Before
public function rules(): array
{
    return [
        'email' => 'required|email|max:255',
        'tags'  => ['nullable', 'array'],
        'tags.*' => 'string|max:50',
    ];
}

// After
public function rules(): array
{
    return [
        'email' => FluentRule::email()->required()->max(255),
        'tags'  => FluentRule::array()->nullable()->each(
            FluentRule::string()->max(50),
        ),
    ];
}

Tested on a production codebase: 448 files converted, 3469 tests still passing.

Contents

Getting started

Usage

  • Sets — mix and match subsets of the migration pipeline
  • Individual rules — when you need one specific conversion

Output

Reference

Installation

composer require --dev sandermuller/laravel-fluent-validation-rector

Requires PHP 8.2+, Rector 2.4+, and sandermuller/laravel-fluent-validation ^1.8.1. The 1.8.1 constraint matters if you have Filament+Livewire components: the HasFluentValidationForFilament trait overrides four methods that also exist on Filament's InteractsWithForms / InteractsWithSchemas, so an insteadof adaptation is required. Rector emits the adaptation for you on direct Filament compositions.

Quick start

// rector.php
use Rector\Config\RectorConfig;
use SanderMuller\FluentValidationRector\Set\FluentValidationSetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withSets([FluentValidationSetList::ALL]);
vendor/bin/rector process --dry-run   # preview
vendor/bin/rector process             # apply
vendor/bin/pint                       # format

The ALL set runs the full migration pipeline (converters + grouping + trait insertion) on every file under app/. For most codebases that's enough; the output is ready to commit after Pint runs. If you want finer control, pick subsets via Sets or register individual rules.

Rules shipped

Grouped by the set that includes them. FluentValidationSetList::ALL runs everything in Converters + Grouping + Traits; SIMPLIFY is a separate post-migration cleanup set you opt into after verifying the initial conversion.

Converters (set CONVERT)

  • ValidationStringToFluentRuleRector converts pipe-delimited rule strings ('required|string|max:255') to fluent chains. Works in FormRequest rules(), $request->validate(), and Validator::make().
  • ValidationArrayToFluentRuleRector converts array-based rules (['required', 'string', Rule::unique(...)]), including Rule:: objects, Password::min() chains, conditional tuples, closures, and custom rule objects.
  • ConvertLivewireRuleAttributeRector strips Livewire #[Rule('...')] / #[Validate('...')] property attributes and generates a rules(): array method. Handles string, list-array, and keyed-array shapes; #[Validate(['todos' => 'required', 'todos.*' => '...'])] expands into one rules() entry per key. Maps as: / attribute: to ->label() in both string and array forms (when both are present, attribute: wins on conflict). For #[Validate] properties, it keeps an empty #[Validate] marker on the property so wire:model.live real-time validation survives conversion. Opt out via the preserve_realtime_validation => false config. Bails on edge cases (hybrid $this->validate([...]) calls, final parent rules() methods, unsupported attribute args, numeric keyed-array keys) and logs each one to the skip file — see Diagnostics.

Grouping (set GROUP)

  • GroupWildcardRulesToEachRector folds flat wildcard and dotted keys into nested each() / children() calls. Applies to FormRequests and Livewire components alike. On Livewire, the HasFluentValidation trait's getRules() override flattens the nested form back to wildcard keys at runtime, so the grouping is safe. When a dot-notation key has no explicit parent rule, the rector synthesizes a bare FluentRule::array() parent so nested required children still fire.

Traits (set TRAITS)

  • AddHasFluentRulesTraitRector adds use HasFluentRules; to FormRequests that use FluentRule.
  • AddHasFluentValidationTraitRector adds the fluent-validation trait to Livewire components that use FluentRule. Picks HasFluentValidation for plain Livewire components, or HasFluentValidationForFilament + a 4-method insteadof block when Filament's InteractsWithForms (v3/v4) or InteractsWithSchemas (v5) is used directly on the class. Ancestor-only Filament usage is skip-logged (PHP method resolution through inheritance is fragile; user must add the trait on the concrete subclass). If the wrong variant is already directly on a class, the rector swaps it to the right one and drops the orphaned import.

Tip

If your codebase has a shared FormRequest or Livewire base, declare use HasFluentRules; (or HasFluentValidation) on the base once and every subclass inherits it. The trait rectors walk the ancestor chain via ReflectionClass and won't re-add the trait on subclasses, so no base_classes configuration is needed.

Post-migration (set SIMPLIFY)

  • SimplifyFluentRuleRector cleans up FluentRule chains after migration: factory shortcuts (string()->url()url()), ->label() folded into the factory arg, min() + max()between(), redundant type removal. Run it as a separate pass after you've verified the initial conversion. It's not included in ALL by default.

Sets

Set Includes
ALL Convert + Group + Traits (full migration pipeline)
CONVERT String, array, and #[Rule] attribute converters
GROUP Wildcard/dotted-key grouping into each()
TRAITS Performance trait insertion for FormRequest and Livewire
SIMPLIFY Post-migration chain cleanup
// Just conversion, no grouping or traits
->withSets([FluentValidationSetList::CONVERT])

// Conversion + traits, skip grouping
->withSets([
    FluentValidationSetList::CONVERT,
    FluentValidationSetList::TRAITS,
])

// Post-migration cleanup (run separately after verifying)
->withSets([FluentValidationSetList::SIMPLIFY])

Individual rules

When you need a single conversion (a one-off migration of a specific codebase path, or running just the array-based converter on a subset of files), import and register the rule class directly:

use SanderMuller\FluentValidationRector\Rector\ValidationStringToFluentRuleRector;
use SanderMuller\FluentValidationRector\Rector\ValidationArrayToFluentRuleRector;

return RectorConfig::configure()
    ->withRules([
        ValidationStringToFluentRuleRector::class,
        ValidationArrayToFluentRuleRector::class,
    ]);

Configurable rules

Two rules accept configuration via withConfiguredRule():

  • ConvertLivewireRuleAttributeRectorPRESERVE_REALTIME_VALIDATION (bool, default true). When true, converted #[Validate] properties retain an empty #[Validate] marker so wire:model.live real-time validation survives conversion. Opt out with false on codebases that don't use wire:model.live and find the marker noisy in converted diffs.
  • AddHasFluentRulesTraitRectorBASE_CLASSES (list of strings). Opt-in list of FormRequest base class names that should receive the trait. Leave empty to skip the trait-insertion path entirely; set to a class list to target shared bases.
use SanderMuller\FluentValidationRector\Rector\ConvertLivewireRuleAttributeRector;

return RectorConfig::configure()
    ->withConfiguredRule(ConvertLivewireRuleAttributeRector::class, [
        ConvertLivewireRuleAttributeRector::PRESERVE_REALTIME_VALIDATION => false,
    ]);

Formatter integration

The rector's output is valid PHP but has three cosmetic seams that a formatter resolves automatically. The fixer names below are from PHP-CS-Fixer; Pint ships the same set under the same names as part of its default Laravel preset.

  1. Imports are inserted at prepend position (not alphabetical). The ordered_imports fixer resolves.
  2. Unused imports may be left in place (e.g. a Livewire\Attributes\Rule import after the attribute is stripped). The no_unused_imports fixer resolves.
  3. Generated @return docblocks emit Illuminate\Contracts\Validation\ValidationRule as a fully-qualified reference. The fully_qualified_strict_types fixer hoists it to a use statement + short-name reference.

All three are in Pint's default Laravel preset, so most Laravel consumers have them without explicit configuration. PHP-CS-Fixer users on a custom ruleset should verify the three fixers are enabled. Without any formatter you'll see rougher-than-example output, but the code is still valid PHP.

Tip

For the cleanest pre-formatter output, enable ->withImportNames()->withRemovingUnusedImports() in your rector.php:

return RectorConfig::configure()
    ->withImportNames()
    ->withRemovingUnusedImports()
    ->withSets([FluentValidationSetList::ALL]);

Note

The rector doesn't insert line breaks between method calls. FluentRule::string()->required()->max(255) is valid PHP on a single line and keeps diffs minimal. If you prefer multi-line chains, the method_chaining_indentation fixer (Pint / PHP-CS-Fixer) reflows them after Rector runs.

Diagnostics

If a file you expected to convert wasn't touched, check .rector-fluent-validation-skips.log in your project root. Every bail-capable rule writes a one-line reason there: unsupported attribute args, a hybrid $this->validate([...]) call, a trait already present on an ancestor class, and so on.

The log is a file sink because Rector's withParallel(...) executor doesn't forward worker STDERR to the parent. A diagnostic line written via fwrite(STDERR, ...) from a worker would vanish on parallel runs (Rector's default). A file sink survives worker death and you can inspect it from the project root after the run finishes. If you're writing your own Rector rule and want similar diagnostics, the same gotcha applies: withParallel() + STDERR means silent data loss.

At the end of each Rector invocation, a single STDOUT line surfaces the log's existence:

[fluent-validation] 42 skip entries written to .rector-fluent-validation-skips.log — see for details

Tip

Rector caches per-file results. Files that hit a bail produce no transformation, so the skip entry is written once and the rule is not re-invoked on cached runs. To force every file to be revisited and every bail to be re-logged, run vendor/bin/rector process --clear-cache (or delete .cache/rector*).

Note

ConvertLivewireRuleAttributeRector verifies the generated rules(): array is syntactically correct, but it can't prove the converted rule is behaviorally equivalent to the source attribute. If a converted Livewire component has no feature test covering validation, review the diff by hand and watch for dropped message: / explicit onUpdate: / translate: false args (logged to the skip file) that need manual migration to Livewire's messages(): array hook or project config. messages: (plural, not a Livewire-documented arg) surfaces its own "unrecognized, likely typo for message:?" log entry.

Known limitations

  • Namespace-less files. Classes at the file root without a namespace are silently skipped by the grouping and trait rectors. Laravel projects always use namespaces, so this rarely comes up in practice.
  • Rules built outside rules(): array. The rector looks for rules(): array, $request->validate([...]), and Validator::make([...]). Rules built inside withValidator() callbacks, custom rulesWithoutPrefix() conventions, or Action-class Collection::put()->merge() chains are left alone.
  • Ternary rule strings. ['nullable', $flag ? 'email' : 'url'] is left alone. A ->when(cond, thenFn, elseFn) conversion is tractable in principle but wasn't worth it: three separate codebase audits turned up near-zero usage (single digits across a 100+ FormRequest corpus), and the closure-based fluent form loses the terseness users reach for ternaries to preserve. Use Rule::when(...) or branch the rules array outside the ternary instead.
  • #[Validate(..., message: '...')] / explicit onUpdate: true / translate: false. These attribute args have no FluentRule builder equivalent. The rule string, as:/attribute: label, and onUpdate: false (consumed as a real-time-validation opt-out marker) are migrated; the remaining args are written to the skip log so you can migrate them to Livewire's messages(): array hook or project config manually. Array-form message: [...] is deferred to a future release.

License

MIT