- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel Form Requests: Validation That Keeps Your Controllers Clean
About Post
Open almost any Laravel project that's a few years old and find the biggest controller. I'll bet the store() method starts with a $request->validate([...]) block the size of a small novel, followed by some manual permission checks, a bit of input clean-up, and finally, somewhere near the bottom, the two lines that actually do the work.
It works. It's also the reason nobody wants to touch that controller.
Form requests are Laravel's answer, and most people only use a fraction of what they can do. Let's go through the whole thing with one realistic example: a tenant submitting a maintenance ticket from an app.
The basic move: rules out of the controller
Generate the class:
php artisan make:request StoreTicketRequest
Then move the rules in:
class StoreTicketRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Ticket::class);
}
public function rules(): array
{
return [
'unit_id' => ['required', 'integer', 'exists:units,id'],
'category' => ['required', Rule::in(['plumbing', 'electrical', 'ac', 'other'])],
'priority' => ['required', 'in:low,normal,high'],
'description' => ['required', 'string', 'max:2000'],
'preferred_date' => ['nullable', 'date', 'after_or_equal:today'],
];
}
}
And the controller shrinks to what it should have been all along:
public function store(StoreTicketRequest $request): RedirectResponse
{
$ticket = $request->user()->tickets()->create($request->validated());
return to_route('tickets.show', $ticket);
}
By the time your method runs, the request has already been authorized and validated. If validation fails, Laravel redirects back with the errors for a normal form, or returns a 422 JSON response for an API call. You don't write either path.
authorize(): the gotcha everyone hits once
The generated stub has an authorize() method that returns false. So the first time you wire up a new form request, every submission gets a 403 This action is unauthorized, and you spend ten confused minutes checking your routes.
Don't just flip it to true and move on. This method is a great place for the "is this user allowed to do this at all?" question, ideally by asking a policy rather than hard-coding a role check. For updates, you can reach the route-bound model directly:
public function authorize(): bool
{
return $this->user()->can('update', $this->route('ticket'));
}
If your project handles all authorization in policies called from the controller, it's fine to return true here. Just make it a decision, not an accident.
Messages people can actually understand
"The unit id field is required" is technically correct and slightly robotic. Two small methods fix it:
public function messages(): array
{
return [
'unit_id.exists' => 'We could not find that unit. Please pick it from the list.',
'preferred_date.after_or_equal' => 'Please choose today or a later date.',
];
}
public function attributes(): array
{
return ['unit_id' => 'unit', 'preferred_date' => 'preferred visit date'];
}
attributes() renames the field in every default message, so you don't have to rewrite them one by one. This matters more than it sounds when the messages are shown in a mobile app to people who have never seen a database column.
prepareForValidation(): clean the input first
Real clients send messy data. One app sends "High", another sends "high". A checkbox arrives as "on", "1" or not at all. A phone number has spaces and dashes.
Instead of making your rules tolerate all of that, normalise it before the rules run:
protected function prepareForValidation(): void
{
$this->merge([
'priority' => strtolower((string) $this->input('priority', 'normal')),
'is_urgent' => $this->boolean('is_urgent'),
'contact_phone' => preg_replace('/\D+/', '', (string) $this->input('contact_phone')),
]);
}
Two things to know. First, Laravel already trims strings and converts empty strings to null through its default middleware, so you don't need to do that here. Second, a field you merge in still needs a rule if you want it in validated(). Which brings us to the most important habit.
Use validated(), not all()
$request->validated() returns only the fields that have rules. $request->all() returns whatever the client sent, including the is_admin or status field someone added with their browser's dev tools.
When you need only part of the data, safe() is neat:
$ticket->update($request->safe()->only(['priority', 'description']));
$date = $request->validated('preferred_date');
Rule: if data goes into create() or update(), it comes from validated() or safe(). Mass-assignment protection on the model is your second line of defence, not your first.
after(): the checks that need more than one field
Some rules don't fit in a single field's array. "There's already an open ticket for this unit and category" depends on two inputs and a database lookup. The after() method returns callbacks that run once the normal rules are done:
use Illuminate\Validation\Validator;
public function after(): array
{
return [
function (Validator $validator) {
if ($validator->errors()->isNotEmpty()) {
return; // basic rules failed, skip the expensive check
}
$duplicate = Ticket::where('unit_id', $this->input('unit_id'))
->where('category', $this->input('category'))
->whereNull('closed_at')
->exists();
if ($duplicate) {
$validator->errors()->add('category', 'There is already an open request for this issue.');
}
},
];
}
That early return is the detail people miss. After-callbacks run even when earlier rules have failed, so without it you'd be querying with a unit_id that might be empty or invalid.
A few smaller tricks
protected $stopOnFirstFailure = true;stops at the first failing field, handy for cheap-then-expensive validation.passedValidation()runs after everything passes, a good place to transform validated data (for example, combining a date and a time field).- One request per action.
StoreTicketRequestandUpdateTicketRequestusually differ in small but important ways. Resist the urge to share one class withif ($this->isMethod('post'))branches. - Test them through HTTP. A feature test that posts bad data and asserts
assertInvalid(['unit_id'])covers the rules, messages and wiring in one go.
The checklist
- Rules live in the form request, not the controller.
authorize()is a real decision, ideally backed by a policy.- Messy input gets normalised in
prepareForValidation(). - Cross-field and database checks go in
after(), with an early return. - Only
validated()orsafe()data touches your models.
Your controller then reads like a sentence: authorized, validated, create, redirect. The next developer to open it will quietly thank you.
What's the strangest piece of input clean-up you've had to write for a real client app?

Be first to comment it...