Download the PHP package sandermuller/laravel-fluent-validation-rector without Composer

On this page you can find all versions of the php package sandermuller/laravel-fluent-validation-rector. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-fluent-validation-rector

Laravel Fluent Validation Rector

Latest Version on Packagist GitHub Tests Action Status GitHub PHPStan Action Status Total Downloads

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.

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

Contents

Getting started

Usage

Operation

Reference

Installation

Requirements: PHP 8.3+, Rector 2.4+, sandermuller/laravel-fluent-validation ^1.27.2.

If you're on an older fluent-validation:

fluent-validation Pin rector to
1.17 – 1.19 ^0.8
1.20+ ^1.0 (latest)

Quick start

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 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.

ValidationArrayToFluentRuleRector

Converts array-based rules (['required', 'string', Rule::unique(...)]), including Rule:: objects, Password::min() chains, conditional tuples, closures, and custom rule objects. Same surface as the string converter — FormRequest rules(), $request->validate(), Validator::make(), plus RuleSet::from([...]) wrappers anywhere in PHP source.

Conditional tuples and dynamic-arg handling - **Conditional tuples accept**: - Explicit enum-value args: `['exclude_unless', 'type', Enum::CASE->value]` - In-tuple variadic spread on variadic fluent signatures: `['required_unless', $field, ...Enum::list()]` → `->requiredUnless($field, ...Enum::list())` - **Conditional tuples bail**: spread targeting non-variadic methods (`excludeWith`, `requiredIfAccepted`), or placed on the rule-name / field position. Array form preserved. - **Non-conditional tuples accept dynamic expressions**: `['max', $this->limit ?? 10]`, `['between', config('a'), config('b')]`, `['max', match($x) { ... }]`, via a permissive emittable-arg check on the fluent-lowering and `->rule([...])` escape-hatch paths. - **Non-conditional tuples bail on**: object/callable/array producers (`new Obj()`, `fn() => 5`, `[1, 2]`) and side-effectful mutators (`$x = 5`, `$i++`). Preserves the original failure mode. - **COMMA_SEPARATED conditional rules** keep strict string-like args to avoid `Closure|bool|string $field` overload ambiguity. - **Dynamic concat rule strings**: an array element built by concatenation (`'required_if_accepted:' . $field`) lowers to the native fluent method when the rule name is a static leading literal and the rule takes the post-colon tail as a single string (`->requiredIfAccepted($field)`). It stays on a string-coercion-safe `->rule()` escape hatch otherwise — when the tail is a single non-literal operand (the `.` coercion can't be proven), or for the value-appending `*If`/`*Unless` family where a single whole-tail arg would emit a spurious trailing comma. - **`Rule::` presence conditionals**: `Rule::requiredIf` / `requiredUnless` / `excludeIf` / `excludeUnless` / `prohibitedIf` / `prohibitedUnless` with a **closure or bool-literal** argument convert to the dedicated fluent method (`Rule::requiredIf(fn () => $this->isAdmin())` → `->requiredIf(fn () => $this->isAdmin())`); matching is case-insensitive. Any other argument shape (a variable or string the fluent method would read as a *field name*) is left as the native array. - **Composite `Rule::` builders bail**: `Rule::when()` / `Rule::unless()` (`ConditionalRules`) and `Rule::forEach()` (`NestedRules`) have no faithful fluent equivalent — the field's native array is preserved untouched.

InlineResolvableParentRulesRector

Inlines parent::rules() when it appears as a spread at index 0 of a child rules(). Unblocks the converter rectors, which otherwise bail on spread items. Runs first in CONVERT.

Supported shapes and bail conditions - **Handles**: - `...parent::rules()` when the parent is a plain `return [...];` - `...$base` when `$base` is the method's only top-level assignment and its RHS is a literal array or `parent::rules()`. Covers the `$base = parent::rules(); return [...$base, 'new' => '...'];` idiom. - **Bails on**: parents that merge, concatenate, or call methods over their return; methods with peer top-level assignments, nested-scope assignments (`if` / `foreach` / `try`), or multi-use variables.

ConvertLivewireRuleAttributeRector

Strips Livewire #[Rule('...')] / #[Validate('...')] property attributes and generates a rules(): array method. See config keys for KEY_OVERLAP_BEHAVIOR, MIGRATE_MESSAGES, PRESERVE_REALTIME_VALIDATION.

Supported shapes and bail conditions - **Handles**: - String, list-array, and keyed-array shapes. `#[Validate(['todos' => 'required', 'todos.*' => '...'])]` expands into one `rules()` entry per key. - Constructor-form rule objects (`new Password(8)`, `new Unique('users')`, `new Exists('roles')`) lower to `FluentRule::password(8)` / `->unique(...)` / `->exists(...)` the same as their static-factory counterparts. - Maps `as:` / `attribute:` to `->label()` in both string and array forms. When both are present, `attribute:` wins on conflict. - Keeps an empty `#[Validate]` marker on converted properties so `wire:model.live` real-time validation survives conversion. Opt out via [`PRESERVE_REALTIME_VALIDATION => false`](#convertlivewireruleattributerector-config). - **Bails on**: hybrid `$this->validate([...])` calls (softenable via `KEY_OVERLAP_BEHAVIOR`), final parent `rules()` methods, unsupported attribute args, numeric keyed-array keys, and the `HasFluentValidation`-trait compose conflict (an ancestor uses the trait AND the child carries `#[Rule]` / `#[Validate]` — the trait's `getRules()` reads only `rules(): array` so the attribute is silently ignored at runtime, and rector-side conversion would override the parent's `rules()` and drop parent-owned fields). Each bail logged to the skip file (see [Diagnostics](#diagnostics)). Direct trait use on the class itself is **not** a bail — the rector merges the attribute rule into a local `rules()` array (or installs one), since neither failure mode applies in that shape.

Grouping (set GROUP)

GroupWildcardRulesToEachRector

Folds flat wildcard and dotted keys into nested each() / children() calls. Applies to FormRequests and Livewire components alike.

Bail conditions and edge-case handling - **Bails on** (each emits a specific skip-log entry under [`=actionable`](#diagnostics)): - Wildcard group has non-FluentRule entries — `'items' => ['required', ...]` next to `'items.*' => FluentRule::...`. - Parent rule's factory doesn't support `each()` / `children()` — only `FluentRule::array()` and `FluentRule::field()` do. - Wildcard parent (`items.*`) has type-specific rules that grouping would silently drop. - Double wildcard (`**`) or non-first `*` in a key suffix. - Concat-keyed wildcard (`$prefix . '.*.foo'`) where the prefix isn't a static class constant. - **Notes**: - On Livewire, the `HasFluentValidation` trait's `getRules()` override flattens the nested form back to wildcard keys at runtime, so grouping is safe. - When a dot-notation key has no explicit parent rule, synthesizes a bare `FluentRule::array()` parent so nested `required` children still fire. - A `FluentRule::field()->…->rule(Rule::array())` (or `->rule('array')`) parent is promoted to `array()` before folding — the array factory seeds the same implicit `array` rule, but only `array()` exposes `each()`. Gated to provably behavior-preserving chains: the `field()` root has no label arg, the only rule hop is the bare array rule, and every other hop is a presence/dependency modifier native to both `FieldRule` and `ArrayRule`. Labeled parents, `FieldRule`-only methods, conditionable closures, object-valued `->rule(...)` hops, a `message()` bound to the array rule, keyed `Rule::array([...])`, and macro-based size constraints keep the escape hatch. - Wildcard-prefix concat keys (`'*.' . CONST_NAME => …`) fold into `'*' => array()->children([CONST_NAME => …, …])` when every sibling in the group resolves the suffix from a self/static class constant. Mixed groups with literal `'*.foo'` siblings keep the literal-keyed entries unchanged and bail-and-log the const branch (no rule loss; partial conversion). - `rules()` methods returning `RuleSet::from([...])` (the canonical `sandermuller/laravel-fluent-validation` shape) are folded by descending into the array argument. The `RuleSet::from(...)` wrapper stays intact; only the wrapped array is rewritten. Branched-return bodies (multiple top-level returns) bail-with-log uniformly to avoid partial cross-branch rewrites.

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 the plain or Filament variant based on direct trait usage.

Variant selection and bail conditions - **Variant picking**: - Plain Livewire component → `HasFluentValidation`. - Filament's `InteractsWithForms` (v3/v4) or `InteractsWithSchemas` (v5) used **directly** on the class → `HasFluentValidationForFilament` + a 4-method `insteadof` block. - Wrong variant already directly on a class → swaps to the right one and drops the orphaned import. - **Bails on**: ancestor-only Filament usage. PHP method resolution through inheritance is fragile, so the user must add the trait on the concrete subclass. Skip-logged.

[!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)

SIMPLIFY is opt-in, not bundled into ALL. Run it as a separate pass after you've verified the initial conversion.

PromoteFieldFactoryRector

Promotes FluentRule::field() to a typed factory (::string(), ::numeric(), etc.) when every ->rule(...) wrapper in the chain resolves to a v1-scope rule whose target method lives on exactly one typed FluentRule subclass. Unblocks SimplifyRuleWrappersRector's next pass — FluentRule::field()->rule('max:61') becomes FluentRule::string()->max(61) instead of staying on the escape hatch. Runs first in SIMPLIFY.

Promotion targets, bail conditions, semantic notes - **Also promotes**: `FluentRule::string()->rule(Password::default())` / `->rule(Email::default())` → `FluentRule::password()` / `::email()` (same zero-arg source, single Password/Email match, no Conditionable hops). - **Also promotes**: `field()->required()->rule('accepted')` → `FluentRule::accepted()->required()` (and the `declined` analog). The dedicated `AcceptedRule` / `DeclinedRule` factories seed the corresponding constraint in their constructors, so the `->rule('')` hop is spliced out. Distinct from intersection-based promotion: promotion to `boolean()` stays blocked because boolean's implicit constraint rejects `"yes"` / `"on"` / `"true"` (`accepted`) and `"no"` / `"off"` / `"false"` (`declined`). Triggers only when the chain has exactly one `->rule(...)` payload and the remaining hops all exist on the dedicated target class. - **Bails on**: - Conditionable hops in the chain. - Chains whose compatible-class intersection isn't a singleton. - `field()->rule('accepted'|'declined')` chains carrying additional `->rule(...)` payloads — the dedicated factory has no typed methods for further constraints, so the escape hatch stays. - **Semantic note**: `StringRule` adds Laravel's implicit `string` rule (likewise `numeric` for `NumericRule`); `FieldRule` adds neither. Promoting therefore changes validation behavior on non-string inputs. Intent matches in nearly all `max(N)` cases, but review the diff.

SimplifyFluentRuleRector

Cleans up FluentRule chains after migration: factory shortcuts (string()->url()url()), ->label() folded into factory args, min() + max()between(), redundant type removal.

Bail conditions - `min()` + `max()` fold when either method carries `messageFor('min'/'max')` or a positional `message()`. Would silently drop the message binding. - Factory-shortcut promotion when the chain has a `label()` call OR the shortcut method isn't adjacent to the factory (preserves user intent and message-binding slots).

SimplifyRuleWrappersRector

Rewrites escape-hatch ->rule(...) calls into native typed-rule methods. Runs after SimplifyFluentRuleRector so factory shortcuts apply first.

Rewrite table, conditional-tuple handling, receiver inference - **Handles**: | Rule family | Receivers | Notes | |---------------------------------|-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| | `in` / `notIn` | `String`/`Numeric`/`Email`/`Field`/`Date` | `HasEmbeddedRules` consumers | | `min` / `max` / `between` | per-class allowlist | `EmailRule` has only `max` | | `regex` | `StringRule` only | | | `size` → `exactly` | `String`/`Numeric`/`Array`/`File` | Laravel's `size:` renamed in fluent-validation per `TypedBuilderHint` | | `enum` | `HasEmbeddedRules` consumers | typed-rule allowlist | | Literal-zero comparison helpers | `NumericRule` | `gt:0` → `->positive()`, `gte:0` → `->nonNegative()`, `lt:0` → `->negative()`, `lte:0` → `->nonPositive()`. Non-zero literals + field refs stay escape. | | Zero-arg string tokens | typed receivers with matching method | `'accepted'`, `'declined'`, `'present'`, `'prohibited'`, `'nullable'`, `'sometimes'`, `'required'`, `'filled'` | - **COMMA_SEPARATED conditional rules** (array, string, and concat forms): `->rule(['required_if', 'field', 'value'])`, `->rule('required_if:field,value')`, and `->rule('required_with:' . self::FIELD)` all lower to native methods (`->requiredIf('field', 'value')` / `->requiredWith(self::FIELD)`). Covers field-plus-variadic-values rules (`required_if` / `exclude_unless`) and pure variadic-fields rules (`required_with` / `prohibits` / `required_array_keys`). String form splits the tail on commas (arity ≥ 1 for pure-field, ≥ 2 for field-values); the fluent variadic re-joins with `,`, so escaped/quoted commas round-trip to the identical rule string. Concat form is scoped to the pure-field family — it passes the whole post-colon expression as a single field arg (provably equivalent via that same re-join) and gates to statically-simple, string-oriented operands (string literals, variables, class constants, enum `->value`); a method call / arithmetic / ternary operand stays an escape hatch. BackedEnum cases in array tail positions auto-wrap with `->value`. Category C `required_if_accepted` and Category D `exclude_with` stay as escape hatch. - **`Rule::` facade conditional rules**: `->rule(Rule::requiredIf($cond))` → `->requiredIf($cond)`, plus the `requiredUnless` / `excludeIf` / `excludeUnless` / `prohibitedIf` / `prohibitedUnless` siblings. The facade form takes a single `Closure|bool` condition, passed through verbatim. Bails on a multi-arg call (not valid facade usage) and on a literal-`null` condition (valid Laravel — normalizes to `false` — but `->requiredIf(null)` would `TypeError` against the `Closure|bool|string` signature). Named args on any facade call (`Rule::in(values: [...])`) also bail, since param names differ between the facade and the fluent builder. - **Receiver-type inference**: walks the chain back to the `FluentRule::*()` factory. Steps through `Conditionable` proxy hops (`->when(...)` / `->unless(...)` / `->whenInput(...)`) when the closure body is a bare-return / no-return / `fn ($r) => $r` identity. Proxy hops with other closure shapes bail. - **Bails on**: variable receivers, methods absent from the resolved typed-rule class.

InlineMessageParamRector

Collapses ->message('...') / ->messageFor('key', '...') chain calls into the inline message: named parameter on FluentRule factories and rule methods. Requires sandermuller/laravel-fluent-validation ^1.20 (earlier floors get zero rewrites via the reflection-time surface probe).

Rewrite predicates and skip categories - **Three rewrite predicates**: - **Factory-direct**: `FluentRule::email()->message('Bad')` → `FluentRule::email(message: 'Bad')`. Requires `->message()` immediately on the factory with no intervening rule method or Conditionable hop. - **Rule-method matched-key**: `->min(3)->messageFor('min', 'Too short.')` → `->min(3, message: 'Too short.')`. - **Rule-object**: `->rule(new In([...]))->messageFor('in', 'Pick one.')` → `->rule(new In([...]), message: 'Pick one.')`. - **Skip categories** (each emits a user-facing log entry): | Category | Examples | Why | |----------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------| | Variadic-trailing | `requiredWith` / `contains` | inline binds to wrong slot | | Composite | `digitsBetween` / `DateRule::between` / `ImageRule::dimensions` | inline binds to last sub-rule | | Mode-modifier | `EmailRule::strict` / `PasswordRule::letters` | don't call `addRule` | | Deferred-key factories | `date` / `dateTime` | | | L11/L12-divergent `Password` | `getFromLocalArray` shortRule lookup is L12+ only | template lists `password.letters` / `password.mixed` sub-key alternatives for L11 consumers | | No-implicit-constraint factories | `field` / `anyOf` | | - **Pre-existing user misbindings** (`->min(3)->messageFor('max', ...)`) stay chained silently. Not rector's job to fix.

Docblock polish (set POLISH)

POLISH is opt-in, not bundled into ALL. Run it as a separate pass after CONVERT stabilizes (multi-pass convergence requires the final shape).

UpdateRulesReturnTypeDocblockRector

Narrows the @return PHPDoc annotation on rules() methods from the wide array<string, ValidationRule|string|array<mixed>> union down to array<string, \SanderMuller\FluentValidation\Contracts\FluentRuleContract> when every value in the returned array is a FluentRule::*() call chain. Cosmetic (runtime behavior untouched), but gives PHPStan and editors a narrower type to reason about. Run as a separate pass after CONVERT stabilizes.

Qualifying classes, narrow conditions, skip conditions - **Qualifying classes**: `FormRequest` subclasses (anywhere in the ancestor chain, aliased imports included) and classes using `HasFluentRules` / `HasFluentValidation` / `HasFluentValidationForFilament` directly or via ancestors. - **Narrowed only**: methods with no existing `@return`, `@return array`, or the wide-union annotation this package's converters emit. - **Respected (left untouched)**: user-customized annotations, `@inheritDoc`, widened unions/intersections, any non-prose suffix. - **Skipped when**: - The returned array isn't a single literal `Array_` (multi-return, builder variants, `RuleSet::from(...)`, collection pipelines). - Any value isn't a FluentRule chain (`Rule::in(...)`, `new Custom()`, closures, string rules, ternary / match). - The method has `): ?array` or unkeyed items. - Rector's multi-pass convergence means it eventually fires on the final shape, but a single-invocation rector run that mixes `CONVERT` + `POLISH` may require a second invocation if any file had string-rule items mid-convert.

Opting in: #[FluentRules] attribute

#[FluentRules] is a per-method opt-in attribute (defined in sandermuller/laravel-fluent-validation) that signals "convert this method's rule array, even though my class doesn't fall under one of the auto-qualifying shapes (FormRequest / fluent-validation trait / Livewire)." It also lifts the abstract-class safety guard when applied to rules() itself, treating the attribute as the user's audit assertion that subclasses don't manipulate parent::rules() as a plain array.

Use #[FluentRules] when:

Do NOT use #[FluentRules] on:

Scoping rules + what `#[FluentRules]` does NOT lift **Per-method scoping.** The audit assertion is per-method, not class-wide. `#[FluentRules]` on `rulesWithoutPrefix()` qualifies the class for processing and converts that specific method, but does not enable class-wide auto-detection of unrelated rule-shaped helpers — those would still need their own `#[FluentRules]` attribute to convert. This narrowing prevents "stray rule token in an unrelated helper gets rewritten as validation rules" regressions. **What `#[FluentRules]` does NOT do.** The attribute is a narrow per-method opt-in for rule conversion. It does NOT lift the package's other safety guards: - **Cross-class parent-safety.** If any subclass manipulates `parent::rules()` with array functions (`array_merge`, `array_search`, bracket assignment, `collect()->merge*()`), the parent stays unsafe and refuses conversion — even when the parent's `rules()` method carries `#[FluentRules]`. The attribute is the user's claim about *their own* method's audit-safety, not a license to override the cross-class scan. Audit your subclass usage and refactor the merge points if you need the parent to convert. - **Shape-changing transformations on Validator subclasses.** When a class qualifies solely via `#[FluentRules]` and is a subclass of a Validator (`extends FluentValidator extends Validator`), the converter rectors run but `GroupWildcardRulesToEachRector` skips with a documented log message. The fold rewrites `'*.foo' + '*.bar'` into `'*' => array()->children([...])`, which is structurally equivalent under FormRequest dispatch but breaks Validator subclasses whose parent class postprocesses `rulesWithoutPrefix()` output (e.g. `JsonImportValidator::rulesWithPrefix()` walks the array and prepends a per-key prefix — the nested-children shape doesn't round-trip). Wrap the wildcard rules manually if you have audited the parent's behavior; the rector won't silently fold in this case. - **The denylisted-method guard.** `#[FluentRules]` on `casts()`, `messages()`, `attributes()`, `toArray()`, `jsonSerialize()`, etc. is silently dropped for class-qualification AND conversion purposes, and a skip-log warning fires so you notice the mistake. The denylist always wins regardless of whether the attribute is present.

Sets

Set Rules
ALL CONVERT + GROUP + TRAITS (the full migration pipeline)
CONVERT ConvertLivewireRuleAttributeRector
GROUP GroupWildcardRulesToEachRector
TRAITS AddHasFluentValidationTraitRector
SIMPLIFY InlineMessageParamRector — post-migration cleanup, run as a separate pass after verifying the initial conversion
POLISH UpdateRulesReturnTypeDocblockRector — narrow @return docblocks to FluentRuleContract

[!NOTE] Don't bundle ALL + SIMPLIFY + POLISH into a single config call. SIMPLIFY runs after manual diff review of the initial conversion; POLISH needs CONVERT's multi-pass output to stabilize. Each is a separate vendor/bin/rector process invocation against its own withSets([...]) block.

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:

The full rule list (any of these can be registered individually without pulling the whole set):

Rule Set (opt-in) Purpose
InlineResolvableParentRulesRector CONVERT (included in ALL) inline ...parent::rules() spread when parent is plain return [...]
ValidationStringToFluentRuleRector CONVERT (included in ALL) pipe-delimited rule strings → FluentRule chains
ValidationArrayToFluentRuleRector CONVERT (included in ALL) array-based rules + Rule::/Password:: objects → FluentRule chains
ConvertLivewireRuleAttributeRector CONVERT (included in ALL) Livewire #[Rule] / #[Validate] → generated rules() method
GroupWildcardRulesToEachRector GROUP (included in ALL) flat wildcard/dotted keys → nested each() / children()
AddHasFluentRulesTraitRector TRAITS (included in ALL) adds use HasFluentRules; to FormRequests that use FluentRule
AddHasFluentValidationTraitRector TRAITS (included in ALL) adds Livewire trait (plain or Filament variant) to Livewire components
PromoteFieldFactoryRector SIMPLIFY (not in ALL) FluentRule::field()->rule('max:61')FluentRule::string() when wrappers narrow to one typed subclass
SimplifyFluentRuleRector SIMPLIFY (not in ALL) factory shortcuts, ->between(), redundant-type cleanup
SimplifyRuleWrappersRector SIMPLIFY (not in ALL) ->rule('in:a,b') / ->rule(Rule::in([...])) / ->rule('size:N') → native typed-rule methods (->in([...]), ->exactly(N), etc.)
InlineMessageParamRector SIMPLIFY (not in ALL) ->message('x') / ->messageFor('key', 'x') on factories + rule methods → inline message: named param (requires fluent-validation ^1.20)
UpdateRulesReturnTypeDocblockRector POLISH (not in ALL) narrow @return on pure-fluent rules() to FluentRuleContract

Configurable rules

Four rules accept configuration via withConfiguredRule().

ConvertLivewireRuleAttributeRector config

Key Type Default What it does
PRESERVE_REALTIME_VALIDATION bool 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.
MIGRATE_MESSAGES bool false When true, message: attribute args migrate into a generated messages(): array method alongside rules(). String message: 'X''<prop>' => 'X'; array message: ['rule' => 'X']'<prop>.<rule>' => 'X' (full-path keys passthrough verbatim for keyed-array first-arg attributes). Opt-in: expands class surface; some consumers centralize messages in lang files. Bails on unmergeable existing messages().
KEY_OVERLAP_BEHAVIOR 'bail' | 'partial' 'bail' Controls what happens when a class has #[Validate] attrs AND an explicit $this->validate([...]) call. 'bail' skips the whole class. 'partial' converts attrs whose predicted emit keys don't appear in any explicit validate([...]) array; overlapping attrs + the explicit call stay intact. Only direct Array_ / RuleSet::compileToArrays(<literal>) accepted; anything else forces classwide bail.

SimplifyRuleWrappersRector config

Key Type Default What it does
TREAT_AS_FLUENT_COMPATIBLE list<string> [] Consumer-declared allowlist of rule-factory FQCNs whose output is FluentRule-compatible. Patterns support * (single namespace segment) and ** (recursive). Silences "rule payload not statically resolvable" skip log on shapes rector can't introspect — e.g. ->rule(App\Rules\Domain\DutchPostcodeRule::create()).
ALLOW_CHAIN_TAIL_ON_ALLOWLISTED bool false When a chain ends in ->someMethod() after an allowlisted factory call, default preserves the tail. Flip on if your allowlist covers factories whose tails always return another FluentRule-compatible node.

UpdateRulesReturnTypeDocblockRector config

Same two keys as SimplifyRuleWrappersRector (TREAT_AS_FLUENT_COMPATIBLE, ALLOW_CHAIN_TAIL_ON_ALLOWLISTED). Allowlisted items count as FluentRule for the narrow-@return-tag decision. Mixed arrays (allowlisted items + string/array entries) with an existing narrow FluentRuleContract tag emit a stale-narrow skip-log warning.

[!NOTE] Per-rector configuration. Each rector receives its own configuration array via withConfiguredRule(...); the values are not pooled across rectors. When the same wire key appears on both SimplifyRuleWrappersRector and UpdateRulesReturnTypeDocblockRector, pass the key on each rector that consumes it — configuring only one will leave the other running with a default-empty allowlist (silent partial config; docblocks won't narrow on your custom factories). The DTO builder section below shows the recommended shared-instance pattern.

AddHasFluentRulesTraitRector config

Key Type Default What it does
BASE_CLASSES list<string> [] Opt-in list of FormRequest base class names that should also receive the trait. Default is auto-detection on concrete FormRequests that use FluentRule — this list adds named shared bases on top of that path. Leave empty to use auto-detection only.

Typed configuration (DTO builders)

Each configurable rector has an opt-in DTO builder under SanderMuller\FluentValidationRector\Config\ that produces the same wire-key array via a ->toArray() terminal step. The builders give you compile-time type safety, IDE autocomplete, and immutable composition without changing anything on the rector side — the rector's configure(array) signature is unchanged and the array shape is identical. The constant-array form keeps working alongside the DTO form; pick whichever fits your rector.php style.

Rector DTO Shared types
ConvertLivewireRuleAttributeRector Config\LivewireConvertOptions Config\Shared\OverlapBehavior (enum)
SimplifyRuleWrappersRector Config\RuleWrapperSimplifyOptions Config\Shared\AllowlistedFactories
UpdateRulesReturnTypeDocblockRector Config\DocblockNarrowOptions Config\Shared\AllowlistedFactories
AddHasFluentRulesTraitRector Config\HasFluentRulesTraitOptions Config\Shared\BaseClassRegistry

Cross-rector shared DTOs are the canonical multi-rector form. The example above shows the lockstep pattern: a single $allowlist instance feeds both SimplifyRuleWrappersRector::with(...) and UpdateRulesReturnTypeDocblockRector's DocblockNarrowOptions::with(...). Adding a class to the allowlist updates both surfaces atomically. Configuring only one of the two rectors leaves the other running with an empty allowlist — the kind of silent-partial-config that produces no error but quietly skips your custom factories on the un-configured rector's surface.

The ::with(...) named constructor is shorthand for ::default()->withAllowlistedFactories(...) — both produce identical output. Use whichever reads better at the call site; mixed-style is fine.

Same pattern for the trait-add rector's base-class allowlist:

Formatter integration

The rector emit is not formatter-clean by design. Run a formatter (Pint, PHP-CS-Fixer, or equivalent) after vendor/bin/rector process to normalize output. The recommended pipeline:

Three cosmetic seams 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:

[!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

The skip log is opt-in. In default runs, bail-capable rules still count skips and the end-of-run summary reports the total, but no file is written to your project root:

FLUENT_VALIDATION_RECTOR_VERBOSE accepts three values (case-insensitive):

Value Surfaces
unset / empty off — only always-actionable entries get counted; no file is written.
actionable recommended — adds verbose entries labeled actionable (payloads that need manual migration, stale @return docblocks, etc.); suppresses structural noise like "trait already present" / "class is Livewire, routed to the other rector".
1 / true / all everything — legacy behavior, includes the structural noise. =1 kept as alias so existing CI scripts keep working.

[!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*).

Log file location, format, and parallel-worker rationale Env-only is deliberate. The flag has to reach parallel workers (fresh PHP processes spawned via `proc_open`), and shell-exported env inherits automatically; an in-process `putenv()` wrapper would not. Exporting the variable one step above the rector invocation keeps a single source of truth that every worker sees. Any opt-in tier writes `.cache/rector-fluent-validation-skips.log` (plus a `.session` sentinel used to coordinate truncation across parallel workers) and the end-of-run line points at it: At the legacy `=1` / `=all` tier, the same line appends a tip pointing at the actionable filter — the `=all` firehose typically dominates with structural noise (trait-already-present, parent-inherits-trait, Livewire-detected) which `=actionable` filters out. Production dogfood on a 5-Livewire-component Laravel 12 / Filament v5 app measured 110 entries at `=all` vs. 5 at `=actionable` on the same surface: The `.cache/` subdir matches Rector's own cache directory convention — most projects already gitignore it. The first line of the log is a per-run header recording the package version, ISO-8601 UTC timestamp, and verbose tier, useful for cross-release diff stability in CI: The header is always emitted when verbose mode is on, even on zero-entry runs, so the file's existence stays stable across runs. 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.

[!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: (use MIGRATE_MESSAGES to opt in), explicit onUpdate:, or 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.

Parity

A small subset of rectors changes which Laravel rule object handles validation at runtime. The functional test suite proves source→source AST shape; the parity harness under tests/Parity/ proves the resulting rule sets produce equivalent error bags when Laravel runs them. Together they cover both structural and behavioral correctness.

In-scope rectors (semantics may change):

Pure-refactor rectors (Validation*ToFluentRule, AddHasFluent*Trait, ConvertLivewireRuleAttribute, Inline*, UpdateRulesReturnTypeDocblock, SimplifyFluentRule) ship with structural coverage only — their transformations don't change which rule class handles validation.

Authoring a fixture. Each fixture lives at tests/Parity/Fixture/<RectorName>/<case>.php and returns:

The harness runs Validator::make($payload, $rules_before) and Validator::make($payload, $rules_after), then diffs the resulting error bags. Outcomes: MATCH, BEFORE_REJECTS_AFTER_PASSES, AFTER_REJECTS_BEFORE_PASSES, BOTH_REJECT_DIFFERENT_MESSAGES, BOTH_REJECT_DIFFERENT_ORDER, or SKIPPED (DB / closure denylist).

Allowed divergences. Some transformations legitimately change behavior — e.g. boolean()->accepted() rejects 'yes' / 'on' strings that bare accepted accepts because of boolean()'s implicit type pre-check. Categorize via DivergenceCategory enum:

The category constrains the allowed runtime outcome; mismatched category fails the test. The free-text rationale lives next to the divergence so future readers see why it's acceptable.

Coverage gate. tests/Parity/CoverageTest.php asserts every in-scope rector has ≥1 fixture. New semantics-changing rectors must extend the in-scope list AND ship at least one fixture before merging.

Auto-detected (no config needed)

The converters detect rules-shaped methods by content signature — a string-keyed return [...]; whose values include a recognized rule string, Rule::*() call, FluentRule chain, or constructor-form rule object. No consumer config needed for any of these:

Known limitations

Testing

The suite runs against Orchestra Testbench — no host Laravel app required.

Changelog

Release notes live in GitHub releases page.

Contributing

See CONTRIBUTING.md for the dev setup, test workflow, and PR conventions.

Security

Disclose vulnerabilities privately — see SECURITY.md for the GitHub Security Advisories link or PGP-accepting email.

Credits

License

MIT — see LICENSE.md.


All versions of laravel-fluent-validation-rector with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
rector/rector Version ^2.5.2
sandermuller/laravel-fluent-validation Version ^1.27.2
symplify/rule-doc-generator-contracts Version ^11.2
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package sandermuller/laravel-fluent-validation-rector contains the following files

Loading the files please wait ...