[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-laravel-form-request-validation-patterns":3},{"content":4,"lastModified":5},"\u003Cp>Last week I reviewed a pull request from a team that just inherited a Laravel codebase, and one controller line made me wince: \u003Ccode>Post::create($request->all())\u003C\u002Fcode>. It worked. It had worked for two years. It was also one spicy payload away from writing whatever columns a client felt like sending. That single line is why I keep insisting on form request validation, because the class is not really about tidy rules arrays. It is about drawing a hard line between what comes in and what touches your database.\u003C\u002Fp>\n\n\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1618477388954-7852f32655ec?w=1200&q=80&auto=format&fit=crop\" alt=\"A developer typing form validation code on a laptop at a desk\" loading=\"lazy\">\n\n\u003Ch2>validated() is the whole point\u003C\u002Fh2>\n\n\u003Cp>Most tutorials show you \u003Ccode>rules()\u003C\u002Fcode> and stop there. The part that actually protects you is what you read back out in the controller:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">public function rules(): array\n{\n    return [\n        'title' =&gt; ['required', 'string', 'max:120'],\n        'body' =&gt; ['required', 'string', 'min:50'],\n        'tags' =&gt; ['array', 'max:5'],\n        'tags.*' =&gt; ['string', 'max:20'],\n    ];\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cpre>\u003Ccode class=\"language-php\">public function store(StorePostRequest $request): RedirectResponse\n{\n    $post = $request-&gt;user()-&gt;posts()-&gt;create($request-&gt;validated());\n\n    return redirect()-&gt;route('posts.show', $post);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>validated()\u003C\u002Fcode> returns only the keys that have rules. Anything extra in the payload, a stray \u003Ccode>is_admin\u003C\u002Fcode>, a hidden \u003Ccode>user_id\u003C\u002Fcode>, gets dropped before it reaches Eloquent. Pair this with \u003Ccode>Rule::unique()\u003C\u002Fcode> handling and the mass assignment errors I wrote about earlier mostly evaporate. I ran into the same habit myself, honestly. Years of typing \u003Ccode>$request-&gt;input('title')\u003C\u002Fcode> is a tough reflex to break, but every field you pull that way skipped the checks you just wrote. If you need a subset, \u003Ccode>$request-&gt;safe()-&gt;only(['title', 'body'])\u003C\u002Fcode> gives you a filtered bag instead of raw input.\u003C\u002Fp>\n\n\u003Cp>One more detail people miss: the fields in \u003Ccode>rules()\u003C\u002Fcode> are the whitelist, so if you validate a field but never want it persisted, add \u003Ccode>exclude\u003C\u002Fcode> style rules like \u003Ccode>'terms' =&gt; ['accepted', 'exclude']\u003C\u002Fcode>. It passes validation and disappears from the validated output.\u003C\u002Fp>\n\n\u003Ch2>sometimes and nullable are not the same thing\u003C\u002Fh2>\n\n\u003Cp>I see these swapped constantly on PATCH endpoints. \u003Ccode>sometimes\u003C\u002Fcode> means: skip every rule on this field if the key is absent from the request. \u003Ccode>nullable\u003C\u002Fcode> means: the field may be null, but the rest of the rules still run when it is present.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">'bio' =&gt; ['sometimes', 'string', 'max:500'],\n'website' =&gt; ['nullable', 'url', 'max:255'],\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>With \u003Ccode>sometimes\u003C\u002Fcode>, omitting \u003Ccode>bio\u003C\u002Fcode> leaves the old value alone. Sending \u003Ccode>\"bio\": null\u003C\u002Fcode> still fails the string check, which surprises people. With \u003Ccode>nullable\u003C\u002Fcode>, an explicit null sails through and your model writes it. Get this wrong and either your partial updates blow up or your \"clear this field\" feature silently does nothing. I have debugged both flavors more than once.\u003C\u002Fp>\n\n\u003Ch2>prepareForValidation cleans up before the rules run\u003C\u002Fh2>\n\n\u003Cp>Clients send messy data. Empty strings where you expect null, slugs with random casing, phone numbers full of spaces. Instead of throwing rules at the mess, normalize it first:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">protected function prepareForValidation(): void\n{\n    $this-&gt;merge([\n        'slug' =&gt; Str::slug($this-&gt;input('slug') ?: $this-&gt;input('title', '')),\n        'phone' =&gt; preg_replace('\u002F\\s+\u002F', '', (string) $this-&gt;input('phone')),\n    ]);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Now the rules run against clean input and you do not need a \u003Ccode>sometimes\u003C\u002Fcode> flag on a field that only exists half the time. I used to do this normalization in the controller. Then I had three endpoints that all created posts and only one of them generated slugs. Fun bug to find in production.\u003C\u002Fp>\n\n\u003Ch2>Unique rules break on updates\u003C\u002Fh2>\n\n\u003Cp>Everyone writes \u003Ccode>Rule::unique('users')\u003C\u002Fcode> for registration and then discovers their profile update form rejects the user's own email. You have to ignore the current record:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">use Illuminate\\Validation\\Rule;\n\npublic function rules(): array\n{\n    return [\n        'email' =&gt; [\n            'required',\n            'email:rfc',\n            Rule::unique('users')-&gt;ignore($this-&gt;user()),\n        ],\n    ];\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Passing the model works, passing the id works, but be careful which id you pass. \u003Ccode>$this-&gt;route('user')\u003C\u002Fcode> pulls it from the route parameter, which matters when an admin edits someone other than themselves. I once ignored the wrong record in a multi-tenant admin panel and the duplicate-email check effectively stopped existing for those requests. The database unique index caught it eventually. The index is the last line of defense, not the first, so keep both.\u003C\u002Fp>\n\n\u003Ch2>When to pull logic out into a rule object\u003C\u002Fh2>\n\n\u003Cp>Closures inside \u003Ccode>rules()\u003C\u002Fcode> are fine for one-off checks. Once the same check appears in a second form request, or it needs a constructor argument, it earns its own class:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">use Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass ActiveCoupon implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        $coupon = Coupon::where('code', $value)-&gt;first();\n\n        if (! $coupon || $coupon-&gt;expires_at?-&gt;isPast()) {\n            $fail('That coupon code is invalid or has expired.');\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Now it is unit-testable without spinning up an HTTP request, and your rules array reads like a sentence. My cutoff is simple: closure in one place, object in two. Database queries in inline closures are the thing I rip out fastest during refactors, because they hide Eloquent calls inside a giant array where nobody thinks to look for them.\u003C\u002Fp>\n\n\u003Cp>None of this is clever. That is sort of the point. Form requests are the boring scaffolding that keeps \u003Ccode>$request-&gt;all()\u003C\u002Fcode> out of your codebase, and boring is what you want between raw client input and your tables. I keep the snippets for these patterns in Snippet Ark since they repeat on every Laravel project I start, and pasting the skeleton in takes ten seconds versus reconstructing it from memory.\u003C\u002Fp>\n\n\u003Cp>If you want the companion piece, \u003Ca href=\"\u002Fposts\u002Flaravel-mass-assignment-fillable-error-fix\u002F\">the mass assignment post\u003C\u002Fa> pairs directly with this one. Validation decides what gets in. \u003Ccode>$fillable\u003C\u002Fcode> decides what gets written. You want both doors locked.\u003C\u002Fp>\n","2026-10-29",1793589940179]