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.
Download sandermuller/laravel-fluent-validation-rector
More information about sandermuller/laravel-fluent-validation-rector
Files in sandermuller/laravel-fluent-validation-rector
Package laravel-fluent-validation-rector
Short Description Rector rules for migrating Laravel validation to sandermuller/laravel-fluent-validation
License MIT
Homepage https://github.com/sandermuller/laravel-fluent-validation-rector
Informations about the package laravel-fluent-validation-rector
Laravel Fluent Validation Rector
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
- Installation
- Quick start
- Rules shipped — what gets converted and what stays
Usage
- Sets — mix and match subsets of the migration pipeline
- Individual rules — when you need one specific conversion
Operation
- Formatter integration — what the rector emits and how Pint / PHP-CS-Fixer finish the job
- Diagnostics — skip log + verbosity tiers
- Parity — runtime-equivalence harness for semantics-changing rectors
#[FluentRules]opt-in — per-method opt-in attribute
Reference
- Public API — frozen surface (symbols, wire keys, behavior)
- Auto-detected — supported shapes you don't need to configure
- Known limitations
- License
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.
- Where it fires: FormRequest
rules(),$request->validate(),Validator::make(), plusRuleSet::from([...])wrappers anywhere in PHP source (FormRequestrules(): RuleSetreturns, action methods, services, controllers, jobs — added 1.1.0). The wrapper itself stays intact; only the inner array converts.
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(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;(orHasFluentValidation) on the base once and every subclass inherits it. The trait rectors walk the ancestor chain viaReflectionClassand won't re-add the trait on subclasses, so nobase_classesconfiguration 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('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:
- You have a method on a non-FormRequest / non-Livewire / non-trait class that holds rules under a name other than
rules()— e.g. a custom Validator subclass'srulesWithoutPrefix(). The attribute qualifies the class for processing and tells the converter to walk that specific method's body. - You have an abstract class with
rules()whose subclasses you have audited to confirm none manipulateparent::rules()with array merges. The attribute is your assertion of audit-safety; the package's safety guard for abstract classes is bypassed for the attributedrules()method.
Do NOT use #[FluentRules] on:
- Methods named after Eloquent / Laravel framework hooks (
casts(),messages(),attributes(),toArray(),jsonSerialize(), etc.) — the denylist guard catches misapplied attributes, drops them silently for class-qualification AND conversion purposes, and emits a skip-log warning so you notice the mistake. - Abstract methods whose subclasses you have NOT audited. Converting the parent silently breaks subclasses that do
array_merge(parent::rules(), [...]). The attribute is per-method: applying it to a sibling helper does NOT lift the abstract-with-rules()guard for the unattributedrules()itself.
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+POLISHinto a single config call.SIMPLIFYruns after manual diff review of the initial conversion;POLISHneedsCONVERT's multi-pass output to stabilize. Each is a separatevendor/bin/rector processinvocation against its ownwithSets([...])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 bothSimplifyRuleWrappersRectorandUpdateRulesReturnTypeDocblockRector, 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.
- Imports are inserted at prepend position (not alphabetical). The
ordered_importsfixer resolves. - Unused imports may be left in place (e.g. a
Livewire\Attributes\Ruleimport after the attribute is stripped). Theno_unused_importsfixer resolves. - Generated
@returndocblocks emitIlluminate\Contracts\Validation\ValidationRuleas a fully-qualified reference. Thefully_qualified_strict_typesfixer hoists it to ausestatement + 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 yourrector.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, themethod_chaining_indentationfixer (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]
ConvertLivewireRuleAttributeRectorverifies the generatedrules(): arrayis 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 droppedmessage:(useMIGRATE_MESSAGESto opt in), explicitonUpdate:, ortranslate: falseargs (logged to the skip file) that need manual migration to Livewire'smessages(): arrayhook or project config.messages:(plural, not a Livewire-documented arg) surfaces its own "unrecognized, likely typo formessage:?" 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):
SimplifyRuleWrappersRector— promotesfield()->rule('accepted')to typed factory chains.GroupWildcardRulesToEachRector— folds wildcard sibling keys intoeach(...).PromoteFieldFactoryRector— rewritesfield()->required()->rule('string')tostring()->required().
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:
ImplicitTypeConstraint— typed rule attaches an implicit constraint absent from the pre-rector form.MessageKeyDrift— same fail outcome, different underlying message-key path.AttributeLabelDrift— same fail,:attributesubstitution renders differently.OrderDependentPipeline— same messages, different per-field order.
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:
Validator::validate(...)and the globalvalidator(...)helper (when prefixed with\or in the global namespace).- Custom-named rules methods (
editorRules(),rulesWithoutPrefix(), etc.) on classes that qualify as rules-bearing — FormRequest descendants, fluent-validation-trait users, Livewire components,#[FluentRules]-marked methods. - Dynamic args inside non-conditional tuples —
['max', $cond ? 15 : 20],['between', config('a'), config('b')],['max', $this->limit ?? 10]— convert via the permissive emittable-arg path (seeValidationArrayToFluentRuleRector). #[Validate]attribute args: rule string,as:/attribute:label (→->label()), andonUpdate: false(consumed as a real-time-validation opt-out marker).
Known limitations
- Namespace-less files. Classes at the file root without a
namespaceare silently skipped by the grouping and trait rectors. Laravel projects always use namespaces, so this rarely comes up in practice. - Rules built inside
withValidator()callbacks.withValidator()is a post-validation hook for adding custom errors via$validator->after(...), not a rules definition. No FluentRule equivalent — imperative code stays. - Rules built via
Collection::put()->merge()->all()chains. Runtime-resolved collection pipelines aren't statically determinable. Out of scope unless a narrow shape (pure literalput()chain ending in->all()) gathers consumer demand. - Multi-statement helper bodies. Auto-detection requires a single-statement
return [...];shape. Helpers likeprivate function buildRules() { $rules = [...]; return $rules; }stay untouched. Inline the return or convert by hand. - Ternary picking the rule NAME.
['nullable', $flag ? 'email' : 'url'](where the ternary chooses a different rule) 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. UseRule::when(...)or branch the rules array outside the ternary instead. (Ternaries / function calls / match / nullsafe fetches as a rule's argument convert fine — see Auto-detected.) #[Validate(..., onUpdate: true)]/translate: false. These attribute args have no FluentRule builder equivalent and no migration path. They land in the skip log so you can move them to Livewire's hooks or project config manually.message:is opt-in: enableMIGRATE_MESSAGESto migrate string and arraymessage:args into a generatedmessages(): arraymethod alongsiderules(). WithMIGRATE_MESSAGESoff (default),message:args also land in the skip log for manual migration.
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
rector/rector Version ^2.5.2
sandermuller/laravel-fluent-validation Version ^1.27.2
symplify/rule-doc-generator-contracts Version ^11.2