fluent-validation-migrate-messages
Refactors Laravel validation messages from array overrides to inline chain methods.
Install
mkdir -p .claude/skills/fluent-validation-migrate-messages && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14661" && unzip -o skill.zip -d .claude/skills/fluent-validation-migrate-messages && rm skill.zipInstalls to .claude/skills/fluent-validation-migrate-messages
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Migrate FormRequest `messages(): array` to inline `message:` on fluent chains. Dry-run, then apply. Activates when: user mentions migrate messages, messages array, inline message, remove messages().Key capabilities
- →Rewrite FormRequest messages() overrides into colocated message: named args on the rule chain in rules().
- →Remove the now-empty messages() method when all keys port.
- →Keep only unportable entries behind a comment-stub.
- →Identify portable cases like 'field.rule' matching a chain method to inline.
- →Identify unportable cases such as multi-rule-per-factory or dynamic keys.
How it works
The skill analyzes each entry in the messages(): array method, determines its corresponding rule in the rules() method, and rewrites it as an inline message: named argument.
Inputs & outputs
When to use fluent-validation-migrate-messages
- →Refactoring validation logic
- →Cleaning up FormRequest
- →Migrating validation messages
About this skill
Migrate messages(): array → inline message:
Rewrite FormRequest messages(): array overrides into colocated message: named args on the rule chain in rules(). Remove the now-empty messages() method when all keys port. Keep only unportable entries behind a comment-stub.
When to Activate
- User asks to migrate
messages(): array, "kill messages arrays", "inline messages" - User mentions: migrate messages, messages array, inline message, remove messages(), pull messages into rules
- After Phase 3 of laravel-fluent-validation 1.19+ inline-message rollout lands in their app
Step 1: Verify installation
rg "sandermuller/laravel-fluent-validation" composer.json
Required: sandermuller/laravel-fluent-validation ^1.19 or newer (inline message: param). If older, tell the user to upgrade first — earlier versions lack the message: named arg.
Step 2: Find migration targets
rg "function messages\(\)" --type php -l
For each match, also verify it overrides rules(): array on the same class (FormRequests). Skip test files and non-FormRequest classes.
Step 3: Classify each messages(): array entry
For every 'field.rule' => 'msg' key, determine the migration path. Read the corresponding entry in rules() (same class) and walk the fluent chain.
Portable cases
| Shape | Rewrite |
|---|---|
Key 'field.rule' matches a chain method like ->rule(…) | Inline: ->rule(…, message: 'msg') |
Key 'field.ruleName' matches the factory's implicit constraint (e.g. 'field.email' on FluentRule::email()) | Factory-level: FluentRule::email(message: 'msg') |
Key matches ->rule(someRule, …) class-basename fallback | Stays as ->messageFor('someRule', 'msg'); inline unavailable |
Unportable cases — keep the messages() entry, flag with a comment
Each has a specific reason. Include the reason in the migration report so the user understands why.
- Multi-rule-per-factory: one
FluentRule::email()chain with threemessages()keys'email.required','email.string','email.email'. The factory emits multiple validator rules internally;message:carries one binding. Theemailkey can port viaFluentRule::email(message: '…');requiredports if the chain has->required();stringhas no method to attach to (it's a byproduct ofFluentRule::email()setting$constraints = ['string']). Keepstringinmessages()array. - Variadic-trailing methods:
'field.required_with'+ chain has->requiredWith('email', 'phone'). PHP forbids params after variadic. Rewrite to->requiredWith('email', 'phone')->message('msg')(shorter;->message()binds to$lastConstraintwhichaddRuleset to'required_with'). Not technically inlinemessage:, but removes themessages()entry. - Composite method, non-last sub-rule:
'field.integer'+ chain has->digits(5).->digits()addsintegerthendigits:N;message:binds to the last. Rewrite as->digits(5)->messageFor('integer', 'msg'). - DateRule build-time key:
'field.date_format'+ chain isFluentRule::date()->format('Y-m-d'). DateRule's key varies between'date'and'date_format:...'at build. Rewrite toFluentRule::date()->format('Y-m-d')->messageFor('date_format', 'msg'). - Dynamic key:
"{$field}.required" => 'msg',matchexpression, interpolated variable. Static analysis can't resolve. Leave entirely. - Wildcard key on nested
each()/children():'items.*.name.required'. Walk the outer chain's->each(...)closure / array to find the matching inner FluentRule. Rewrite there. Flag complex cases (deeply-nested / multi-level wildcards) for manual review. - Chain interrupted by
->when(…, fn ($r) => $r->required()): target rule lives inside the closure. Rewrite inside the closure:->when($cond, fn ($r) => $r->required(message: 'msg')). ->rule('x:args')escape-hatch string:'field.x' => 'msg'+->rule('x:args'). No named method. Rewrite to->rule('x:args', message: 'msg')(rule() accepts message: since Phase 3a).- Translated-value wrapper:
'field.required' => __('messages.required'). Value is still an expr, inline works:->required(message: __('messages.required')). Port normally. - Helper-method extraction:
'field.required' => 'msg'where the corresponding rule inrules()is'email' => $this->emailRules(). Cross-method resolution needed. Flag for manual review, tip: "inline the chain or move message into helper return." - Macroable method in chain: chain includes a method defined via
Macroable::macro(...)at runtime. Not statically resolvable. Leave withmessageFor/message. - Custom
ValidationRuleobject via->rule(new MyRule()): message key derived from class-basename at runtime ('myRule'fromMyRule). Rewrite to->rule(new MyRule(), message: 'msg')—rule()acceptsmessage:andaddRuleresolves the key correctly.
Step 4: Present the migration report (DRY-RUN)
Before any edits, output a summary for each target file:
## Migration report: app/Http/Requests/ClearSelectedVideoContentRequest.php
### Portable (4)
- items.*.action.type.required → ->required(message: '…') inside each() closure
- search.value.string → inline string(message: '…')
- name.max → ->max(255, message: '…')
- email.email → FluentRule::email(message: '…')
### Needs messageFor (2)
- items.*.qty.integer → ->digits(5)->messageFor('integer', '…') (composite non-last sub-rule)
- required_with on email/phone → ->requiredWith(…)->messageFor('required_with', '…') (variadic method)
### Unportable, stays in messages() (1)
- email_address.string → FluentRule::email() emits 'string' implicitly; no chain method to target. Keep.
### messages() method
After migration: keeps 1 key, method retained.
Ask the user to confirm per-file before applying.
Step 5: Apply
One file at a time. For each:
- Locate the target entry in
rules(). - Rewrite the chain with
message:/messageFor/ closure inline per the classification. - Remove the migrated key from
messages(). - If
messages()returns[]after all migrations, delete the method. - Run tests on that file's FormRequest immediately (
vendor/bin/pest --filter={ClassName}). - If tests fail, inspect and fix or revert — do not proceed to the next file until the current one is green.
Step 6: Preserve behaviour — test parity
Before migration, capture the actual error messages produced by each messages() entry:
# Write a snapshot test that exercises each rule with a failing input and asserts the custom message surfaces.
Run the snapshot test after migration. The custom messages must still surface identically. If they don't, the migration is wrong — the skip-log classification probably missed a case.
Guardrails
- Never delete
messages()if ANY key survives. Keep the method with just the unportable keys. - Do not migrate keys using dynamic expressions (interpolation,
match, property access). Report and skip. - Preserve translation wrappers.
__(),trans(),Lang::get()values stay intact — they're just PHP expressions on the value side. - One FormRequest at a time. Don't batch across files; each class may have subtle cross-references.
- Do not change rule semantics. Messages are cosmetic; any rewrite that changes WHICH rule fires or WHEN is out of scope for this skill.
Common Patterns
Simple unique key
// Before
public function rules(): array {
return ['route' => ['required', FluentRule::string()->unique('workshops')]];
}
public function messages(): array {
return ['route.unique' => __('WorkshopRouteAlreadyExists')];
}
// After
public function rules(): array {
return [
'route' => ['required', FluentRule::string()->unique('workshops', message: __('WorkshopRouteAlreadyExists'))],
];
}
// messages() method removed entirely.
each() inner closure
// Before
public function rules(): array {
return [
'items' => FluentRule::array()->required()->each([
'qty' => FluentRule::numeric()->required()->integer(),
]),
];
}
public function messages(): array {
return ['items.*.qty.required' => 'Qty required.'];
}
// After
public function rules(): array {
return [
'items' => FluentRule::array()->required()->each([
'qty' => FluentRule::numeric()->required(message: 'Qty required.')->integer(),
]),
];
}
Composite method — messageFor for first sub-rule
// Before — `digits` fires after `integer`; custom message targets `integer`.
public function rules(): array {
return ['code' => FluentRule::numeric()->digits(5)];
}
public function messages(): array {
return ['code.integer' => 'Must be a whole number.'];
}
// After — messageFor retains the distinction; messages() deletable.
public function rules(): array {
return [
'code' => FluentRule::numeric()->digits(5)->messageFor('integer', 'Must be a whole number.'),
];
}
Unportable — keep messages() entry
// FluentRule::email() emits 'required' + 'string' + 'email' under some chains.
// 'email.string' has no method to attach to.
public function rules(): array {
return ['email_address' => FluentRule::email()->when(..., fn ($r) => $r->required())];
}
public function messages(): array {
return [
'email_address.required' => 'Required.', // portable via closure inline
'email_address.email' => 'Invalid.', // portable via FluentRule::email(message: ...)
'email_address.string' => 'Must be text.', // UNPORTABLE — stays
];
}
// After
public function rules(): array {
return [
'email_address' => FluentRule::email(message: 'Invalid.')
->when(..., fn ($r) => $r->required(message: 'Required.')),
];
}
public funct
---
*Content truncated.*
When not to use it
- →When the sandermuller/laravel-fluent-validation package is older than version 1.19.
- →When static analysis cannot resolve dynamic keys like "{$field}.required".
Prerequisites
Limitations
- →Cannot port messages for rules without a direct chain method.
- →Cannot port messages for composite methods where the target is not the last sub-rule without using messageFor().
- →Cannot statically resolve messages for macroable methods in the chain.
How it compares
This skill automates the migration of validation messages, which otherwise requires manual identification and rewriting of each message entry.
Compared to similar skills
fluent-validation-migrate-messages side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| fluent-validation-migrate-messages (this skill) | 0 | 1mo | No flags | Intermediate |
| php-best-practices | 15 | 1mo | No flags | Beginner |
| laravel-architecture | 7 | 1mo | No flags | Intermediate |
| php-guidelines-from-spatie | 6 | 6mo | No flags | Beginner |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
php-best-practices
HoangNguyen0403
PHP best practices, PSR standards, and code quality guidelines.
laravel-architecture
HoangNguyen0403
Core architectural standards for scalable Laravel applications.
php-guidelines-from-spatie
freekmurze
Describes PHP and Laravel guidelines provided by Spatie. These rules result in more maintainable, and readable code.
php-pro
ngxtm
Use when building PHP applications with modern PHP 8.3+ features, Laravel, or Symfony frameworks. Invoke for strict typing, PHPStan level 9, async patterns with Swoole, PSR standards.
laravel-pdf
spatie
Generate PDFs from Blade views or HTML using spatie/laravel-pdf. Covers creating, formatting, saving, downloading, and testing PDFs with the Browsershot, Cloudflare, or DOMPDF driver.
php-testing
HoangNguyen0403
Unit and integration testing standards for PHP applications.