Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FormRequest Inference

FormRequest inference is experimental and disabled by default. Enable it only when you want validated() and supported safe() projections on conventional concrete FormRequest classes.

parameters:
    phpstanLaravelValidation:
        formRequests:
            enabled: true

What is inferred

The extension resolves statically available return expressions from rules() and applies that shape to whole-payload and supported keyed validated() calls:

final class StorePersonRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string',
            'age' => 'integer',
        ];
    }
}

function store(StorePersonRequest $request): void
{
    \PHPStan\dumpType($request->validated());
    // array{name: string, age?: float|int|string|Stringable|true}

    \PHPStan\dumpType($request->safe(['name']));
    // array{name: string}
}

Literal returns, resolvable branches, inherited or trait-provided methods, class constants, typed method parameters, and declared custom-rule contracts can participate. If any possible return expression cannot be resolved, the call keeps Laravel’s broad return type rather than inferring from only part of the method.

Inherited rules that depend on late-bound static:: or $this:: references stay broad. Calls that compose another rules body, such as array_merge(parent::rules(), [...]), are not expanded unless PHPStan can expose the complete constant result.

Lifecycle hooks

FormRequest is a validator lifecycle, not just a rules() method. Inference falls back when the request overrides validated(), getValidatorInstance(), createDefaultValidator(), validationRules(), or passedValidation(), or declares validator(), a non-empty withValidator(), or after(). Those hooks can replace the validator, mutate its rules, or change what validated() returns.

A userland withValidator() body with no executable statements is a no-op, including when inherited or provided by a trait. Any executable statement, or a body the parser cannot verify, restores the conservative fallback.

Trusted classes

A project can assert that a particular class’s lifecycle hooks do not invalidate its rules() contract:

parameters:
    phpstanLaravelValidation:
        formRequests:
            trustedClasses:
                - App\Http\Requests\StorePersonRequest

Trust is exact: subclasses are not trusted implicitly. It bypasses lifecycle-hook checks. It does not make unresolved rule expressions resolvable and does not override a custom validated() implementation. A false trust declaration can produce an unsound type.

Discovery

FormRequest inference does not require Larastan. Concrete requests are discovered from PHPStan’s analysed and scan paths and from the root project’s Composer autoload and autoload-dev source mappings. Classes outside those paths, including undiscovered vendor requests, retain Laravel’s broad type.

Adding an exact class to trustedClasses also makes it discoverable, but that setting simultaneously asserts that its lifecycle hooks are safe. It is not a risk-free discovery-only option.

validated($key) and safe()

Constant string and integer keys, ordinary dotted paths, finite constant-key unions, and explicit defaults participate in validated($key, $default) inference. Optional paths include the default type; an omitted default is null. Dynamic keys, wildcard or first/last traversal, segment arrays, object-property traversal, and Closure defaults remain mixed.

Constant string and integer paths passed to safe([...]) are projected from the same validated shape. Direct safe()->all(), safe()->toArray(), and safe()->only([...]) chains retain that shape for registry-verified FormRequests.

Validator instances retain Laravel’s declared safe() types because Factory::resolver() may return a custom Validator whose virtual validated() implementation changes the payload. The ValidatedInput wrapper is mutable: Laravel exposes array-offset and property writes and unsets. Once a wrapper is stored in a variable, later accessors keep Laravel’s broad declared array type. Dynamic selectors and selector expressions that may execute user code also remain broad.

Residual assumptions

The inferred contract assumes callers do not replace a FormRequest’s resolved validator through the inherited public setValidator() method before calling validated() or safe(). A custom safe() override retains its declared return type.

Configuration keys are documented in Configuration.