Typehinting Laravel validation rules using PHPStan's type aliases
Published on June 18, 2025 by Freek Van der Herten

Static analysis is a powerful tool that examines your code without actually running it, catching bugs and type mismatches before they reach production. For PHP developers, PHPStan has become the go-to solution, and Laravel developers often use Larastan - a wrapper that adds Laravel-specific understanding to PHPStan. In this post, I'll share how I'm using PHPStan's type aliases feature to make validation rule typehints more readable and maintainable in our Laravel application.
What is static analysis and why it matters
Static analysis tools like PHPStan analyze your codebase to find errors that might slip through testing. They check for type mismatches, undefined methods, incorrect return types, and more. While PHP has become more type-safe over the years, static analysis adds an extra layer of confidence, especially in large codebases where manual review becomes impractical.
A bit of history
When we launched Oh Dear almost 8 years ago, we were using the cutting-edge tools of the time: PHP 7, Laravel 5, and the first version of Laravel Spark. Over the years, we've diligently kept our stack modern, upgrading annually to run on the latest versions. Our extensive test suite (over 2,000 tests) has been crucial in making these upgrades smooth and ensuring everything continues to work correctly.
Despite our commitment to code quality, there was one area I'd been putting off: static analysis.
At Spatie, where I also work, static analysis has been a standard practice for years. Starting new projects with PHPStan from day one is straightforward. But Oh Dear was different - it's a mature application with many many classes. The thought of retrofitting static analysis into such a large existing codebase felt overwhelming. I didn't want to just suppress all the errors in a baseline file; I wanted to fix them properly. But finding the time for such a massive undertaking seemed impossible.
Recently, I finally decided to tackle this technical debt. I started with PHPStan at level 1, methodically fixing every issue before moving to the next level. Now I'm working through level 6, where PHPStan becomes particularly strict about type definitions. The most common issues at this level involve missing or incomplete type hints - and that's where today's story begins.
Adding typehints to Laravel form requests
Before diving into the problem, let me briefly explain Laravel's FormRequest objects. In Laravel, FormRequests are classes that encapsulate validation logic for HTTP requests. They keep your controllers clean by moving validation rules to dedicated classes.
Oh Dear has grown into a substantial application with over 100 web and API endpoints, each with its own FormRequest
object. Here's a typical example:
namespace App\Http\Api\Requests; use App\Domain\Site\Rules\SiteIdRule; use App\Domain\Team\Rules\TeamMemberRule; use Illuminate\Foundation\Http\FormRequest; class StoreStatusPageRequest extends FormRequest { public function rules(): array { return [ 'team_id' => ['required', new TeamMemberRule(currentUser())], 'title' => ['required', 'string', 'max:255'], 'sites' => ['required', 'array'], 'sites.*.id' => ['required', new SiteIdRule(currentUser())], 'sites.*.clickable' => ['bool'], ]; } }
This looks clean and works perfectly. But when PHPStan examines it at level 6, it's not satisfied:
------ --------------------------------------------------------------------------------------- Line StoreStatusPageRequest.php ------ --------------------------------------------------------------------------------------- 16 Method App\Http\Api\Requests\StoreStatusPageRequest::rules() return type has no value type specified in iterable type array. 🪪 missingType.iterableValue 💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type ------ ---------------------------------------------------------------------------------------
PHPStan wants to know what's inside the array. Let's start with a simple fix:
// ... /** @return array<string, array<string>> */ public function rules(): array { // ... }
This tells PHPStan we're returning an array with string keys and arrays of strings as values. But now we get a different error:
------ ------------------------------------------------------------------------------------------ Line StoreStatusPageRequest.php ------ ------------------------------------------------------------------------------------------ 19 Method App\Http\Api\Requests\StoreStatusPageRequest::rules() should return array<string, array<string>> but returns array<string, list<App\Domain\Site\Rules\SiteIdRule|App\Domain\Team\Rules\TeamMemberRule|string>>. 🪪 return.type ------ ------------------------------------------------------------------------------------------
PHPStan has analyzed our actual return value and found that we're not just returning strings - we're also returning custom rule objects. Let's be more specific:
/** @return array<string, array<\Illuminate\Contracts\Validation\ValidationRule|string>> */ public function rules(): array { // ... }
Since both SiteIdRule
and TeamMemberRule
implement Laravel's ValidationRule
interface, this type hint is more accurate and flexible. PHPStan is now satisfied:
[OK] No errors
But wait - Laravel's validation system is even more flexible than this. Rules don't always need to be wrapped in arrays, and there are multiple validation contracts. Here's the complete type hint that covers all possibilities:
/** @return array<string, array<\Illuminate\Contracts\Validation\Rule|\Illuminate\Contracts\Validation\ValidationRule|string>|string> */ public function rules(): array { // ... }
This type hint is accurate but... it's quite a mouthful, isn't it?
Type aliases to the rescue
Imagine copying that lengthy type hint to over 100 FormRequest classes. Not only would it be tedious, but it would also create a maintenance nightmare. What if Laravel adds a new validation contract? We'd need to update every single docblock.
Fortunately, PHPStan offers an elegant solution: type aliases. These allow you to define complex types once and reference them by a simple name throughout your codebase.
In your phpstan.neon
configuration file, you can define a type alias like this:
parameters: level: 6 typeAliases: ValidationRules: 'array<string, array<\Illuminate\Contracts\Validation\Rule|\Illuminate\Contracts\Validation\ValidationRule|string>|string>'
Now, instead of that complex type hint, you can simply use:
/** @return ValidationRules */ public function rules(): array { // ... }
In closing
Adding static analysis to a large existing codebase can feel daunting, but it doesn't have to be an all-or-nothing endeavor. By starting at level 1 and gradually working your way up, you can improve your code quality incrementally. In our case, implementing PHPStan has already helped us catch several subtle bugs that our tests missed.
Type aliases, in particular, have made the process much more manageable. What could have been a tedious copy-paste exercise across 100+ files became a simple, maintainable solution. The readable type hints also serve as inline documentation, making our codebase more approachable for new team members.