Skip to content

feat: add typed FormRequest accessors#10158

Open
memleakd wants to merge 7 commits intocodeigniter4:4.8from
memleakd:feat/formrequest-typed-accessors
Open

feat: add typed FormRequest accessors#10158
memleakd wants to merge 7 commits intocodeigniter4:4.8from
memleakd:feat/formrequest-typed-accessors

Conversation

@memleakd
Copy link
Copy Markdown
Contributor

@memleakd memleakd commented May 4, 2026

Description

This PR proposes typed access to validated data through a dedicated ValidatedInput object.

It builds on the new FormRequest feature while keeping FormRequest focused on its core responsibilities: validation rules, authorization, input preparation, and failure handling.

$input = $request->validatedInput();

$page        = $input->integer('page', 1);
$active      = $input->boolean('active', false);
$publishedAt = $input->date('published_at', 'Y-m-d');
$status      = $input->enum('status', PostStatus::class, PostStatus::DRAFT);

The same API is available after using the Validation service directly:

$input = $validation->getValidatedInput();

Design

Following the review discussion, typed access is split by responsibility:

CodeIgniter\Input\InputData
CodeIgniter\Validation\ValidatedInput extends InputData

InputData is a generic typed container for keyed input-like data. It provides simple accessors such as get(), has(), string(), integer(), float(), boolean(), and array().

InputData is intentionally fallback-friendly for raw input: if a present value cannot be read as the requested type, the default value is returned.

ValidatedInput extends InputData with validation-specific accessors such as date() and enum().

ValidatedInput is intentionally strict: missing values may still use defaults and explicit null stays null, but invalid present values throw an InvalidArgumentException because the data has already passed validation.

This keeps the current PR focused on validated data, while leaving room for future explicit request-source APIs such as getPostInput() or getQueryInput() to reuse the generic InputData object without making Validation depend on HTTP.

Behavior

The existing array-based APIs remain unchanged:

$validation->getValidated(); // array
$formRequest->validated();   // array

The new typed APIs are optional:

$validation->getValidatedInput(); // ValidatedInput
$formRequest->validatedInput();   // ValidatedInput

Typed methods read from validated data only. They do not replace validation rules.

Missing optional fields return the provided default value, or null when no default is provided. Fields present with a null value return null.

Services

This PR adds non-shared services for both typed input objects:

service('inputdata', $data, false);
service('validatedinput', $data, false);

They are used internally by Validation::getValidatedInput() and FormRequest::validatedInput(). Since these objects wrap a specific data array, they are non-shared by default.

Notes

This adds ValidationInterface::getValidatedInput(), so custom implementations of the interface will need to add the new method. The changelog lists this under interface changes.

Docs were updated so FormRequest shows concise controller usage, while the Validation docs provide the canonical behavior reference for typed validated input.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

- Add typed helpers for reading validated integer, boolean, date, and enum values
- Keep accessors scoped to validated FormRequest data only
- Document expected validation/accessor responsibilities
- Cover defaults, null values, invalid values, dot syntax, and enum variants

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label May 4, 2026
@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 5, 2026

Thanks, I like the idea. Typed access to validated data is useful and would make controller code nicer.

However, I don't think these accessors should be added directly to FormRequest. This feels like a separate responsibility: FormRequest should handle validation, authorization, preparation, and failure responses, while typed access should belong to an object representing validated input.

I would prefer a design like this:

$validation->getValidated(); // array, unchanged
$formRequest->validated(); // array, unchanged

$input = $validation->getValidatedInput();
$input = $formRequest->validatedInput();

$page = $input->integer('page', 1);
$active = $input->boolean('active', false);
$status = $input->enum('status', Status::class);

This keeps BC, makes the feature available outside FormRequest too, and avoids turning FormRequest into a growing collection of typed helper methods.

So I'm positive about the concept, but I think we should be careful with the design and not rush the implementation. I would like to hear what others think before we decide whether this should be reshaped into a dedicated ValidatedInput class.

@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 5, 2026

Thanks, this makes sense to me.

I originally kept the helpers directly on FormRequest to keep the follow-up small and controller usage concise, but I understand the concern about putting typed access on the FormRequest itself.

I’ll convert the PR to draft for now and wait for more feedback before reshaping it.

I agree the dedicated ValidatedInput object gives the feature a cleaner long-term home. My only hesitation is the extra controller ceremony, but I see the tradeoff.

Happy to rework it in that direction if everyone feel the same.

@memleakd memleakd marked this pull request as draft May 5, 2026 14:12
@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 5, 2026

I went ahead and reworked this in the ValidatedInput direction.

FormRequest now exposes validatedInput(), Validation exposes getValidatedInput(), and the typed helpers live on the dedicated ValidatedInput object. The existing array-based APIs stay unchanged.

I also updated the docs, tests, and changelog/interface-change note around this design.

I’ll mark the PR ready for review again.

Quick Note:
I first placed ValidatedInput under Validation, but Deptrac rejected Validation -> I18n and HTTP -> Validation. Moving it under HTTP keeps the dedicated object design while satisfying the existing dependency rules, since Validation may depend on HTTP and HTTP may depend on I18n.

@memleakd memleakd marked this pull request as ready for review May 5, 2026 20:02
- add ValidatedInput for typed access to validated data
- expose ValidatedInput from Validation and FormRequest
- keep FormRequest focused on request validation flow
- update docs and tests for the new typed input API

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd memleakd force-pushed the feat/formrequest-typed-accessors branch from 71b130d to d7067f3 Compare May 5, 2026 20:08
Comment thread system/HTTP/ValidatedInput.php Outdated
Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@neznaika0
Copy link
Copy Markdown
Contributor

I would like to hear what others think before we decide whether this should be reshaped into a dedicated ValidatedInput class.

I would add Superglobals for this. It is very difficult to work with phpstan when Request::getPost(): array|string|... I haven't reviewed the code for this yet, this is my preference.

@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 6, 2026

I would add Superglobals for this. It is very difficult to work with phpstan when Request::getPost(): array|string|... I haven't reviewed the code for this yet, this is my preference.

Superglobals is a low-level/internal abstraction around PHP globals, so I don't think it is the right place for this. Adding typed access to request input is more reasonable, and I agree it could help with PHPStan.

However, I would be against adding methods directly to the request object:

$this->request->integer('page', 1);

That leaves too much ambiguity about the input source, creating the same problem we have with Validation::withRequest(). Something more explicit could work better:

$this->request->getQueryInput()->integer('page', 1);
$this->request->getPostInput()->boolean('active', false);
$this->request->getPayloadInput()->enum('status', Status::class);

To make this reusable, maybe the current ValidatedInput class should become a generic input data container, for example:

namespace CodeIgniter\Input;

class InputData
{
    public function get(string $key, mixed $default = null): mixed {}
    public function has(string $key): bool {}
    public function integer(string $key, ?int $default = null): ?int {}
    public function boolean(string $key, ?bool $default = null): ?bool {}
    public function date(string $key, ...): ?Time {}
    public function enum(string $key, string $enumClass, ?UnitEnum $default = null): ?UnitEnum {}
    public function string(string $key, ?string $default = null): ?string {}
    public function array(string $key, ?array $default = null): ?array {}
}

Then Validation/FormRequest could return this object for validated data, and request-level APIs could reuse the same behavior.

The important part is agreeing on the behavior for missing values, null, and values that cannot be read as the requested type.

One more important thing - I don't think this PR needs to implement request-level input methods, but the class design should not make that reuse difficult.

Thoughts?

@neznaika0
Copy link
Copy Markdown
Contributor

neznaika0 commented May 6, 2026

Sorry. I was thinking about the Request. Superglobals came to mind because this $_POST is also global.

And to shorten the code, Request::getPostInput() may need to return InputData.

$input = $this->request->getPostInput();
$email = $input->string('email', '');
$status = $input->integer('status', 0);

Typed default values should not return another scalar type. There should be no other types for Request (dates, enum). To work this, you need to have an extended ValidatedInput extends InputData class.
Any type is allowed in it. I'm not sure about extensibility, but theoretically ValidatedInput can have many types. For example, I can use ValueObject during validation or check DTO objects. Should I add getDTO() for everyone ?

@neznaika0
Copy link
Copy Markdown
Contributor

#9482 If we go back, I was talking about a similar behavior - separate methods for types. Here we see why it was necessary. Yes, the PR is imperfect, but we just rejected this idea.

@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 6, 2026

I agree that raw request input should probably support only simple types, for example string(), integer(), boolean(), array(), maybe float(). Raw request input has not passed validation yet, so avoiding richer methods like date() or enum() seems reasonable.

Then, as you said, we can still have a ValidatedInput class that extends InputData and adds validation-specific accessors such as date() and enum().

So the structure could be:

CodeIgniter\Input\InputData
CodeIgniter\Validation\ValidatedInput extends InputData

Request::getPostInput() would return InputData, while Validation::getValidatedInput() and FormRequest::validatedInput() would return ValidatedInput.

I would also move the current ValidatedInput class out of the HTTP namespace and into the Validation namespace.

As for extensibility, we could add services for both inputdata and validatedinput, then use them internally. Since these objects wrap a specific data array, they should probably be non-shared by default.

I think DTO/value object mapping is too large for this PR. For now, I'd keep this PR focused on typed access to existing values.

@neznaika0
Copy link
Copy Markdown
Contributor

The DTO is given as an example that the validator works with any data, it does not need to be limited in types. I wrote about it too #9262

@paulbalandan
Copy link
Copy Markdown
Member

Why is the namespace on CodeIgniter\Input? Would it be better if inside HTTP?

@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 6, 2026

Why is the namespace on CodeIgniter\Input? Would it be better if inside HTTP?

I suggested CodeIgniter\Input because this would be a generic class, not something HTTP-specific. It would only wrap keyed input-like data and provide typed accessors.

Input may not be a perfect namespace name, but I think it is less misleading than HTTP here. The same class could be reused by HTTP request input and by validation output, without making the Validation component depend on the HTTP namespace.

- Add generic InputData for typed access to keyed input data
- Move validation-specific accessors to ValidatedInput
- Return ValidatedInput from Validation and FormRequest APIs
- Add non-shared inputdata and validatedinput services
- Update docs, tests, changelog, and architecture rules

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 6, 2026

Thanks everyone for the discussion and feedback!

I reworked this around the latest architecture direction:

CodeIgniter\Input\InputData
CodeIgniter\Validation\ValidatedInput extends InputData

InputData contains the reusable typed accessors, and ValidatedInput adds validation-specific accessors like date() and enum().

Validation::getValidatedInput() and FormRequest::validatedInput() now return ValidatedInput through the non-shared validatedinput service. I also added a non-shared inputdata service for the generic object.

This keeps the PR focused on validated data, while leaving room for future request-source APIs like getPostInput() or getQueryInput() to return InputData.

Docs, tests, changelog, and Deptrac rules were updated too.

- Add InputData::float() for numeric input values
- Cover float strings, integers, defaults, nulls, and invalid values
- Document float access with matching decimal validation examples

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 6, 2026

@memleakd Thank you for the update.

I was thinking about this more, and have some additional thoughts regarding the behavior of InputData vs ValidatedInput.

The idea is to split typed input access by trust level:

  • InputData - raw input, forgiving
  • ValidatedInput - validated input, strict

InputData would be used for raw request data, like query/post/payload input, so throwing here would be unfriendly and would make the lives of developers a nightmare of try/catch blocks. Here typed methods should be convenient and fallback-friendly:

$request->getQueryInput()->integer('page', 1);

If the value is missing or cannot be read as an integer, it can return the default. This is useful for pagination, filters, UI toggles, etc.

ValidatedInput would extend InputData but be stricter (as it is now), because the data already passed validation:

$validation->getValidatedInput()->integer('page', 1);

Here, missing values can still use the default, and null can stay null, but an invalid value should throw.

So: raw input access is forgiving convenience, validated input access is a strict contract.

I was thinking about something like this:

public function integer(string $key, ?int $default = null): ?int
{
    if (! $this->has($key)) {
        return $default;
    }

    $value = $this->get($key);

    if ($value === null || is_int($value)) {
        return $value;
    }

    if (is_string($value)) {
        $integer = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);

        if ($integer !== null) {
            return $integer;
        }
    }

    return $this->invalidValue($key, 'integer', $default);
}

Then InputData::invalidValue() can return the default, while ValidatedInput::invalidValue() throws.

- Return defaults for invalid raw InputData values
- Keep ValidatedInput strict through an override
- Cover fallback and strict behavior in tests
- Document the raw versus validated input trust levels

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 6, 2026

Thanks again for thinking this through and explaining the direction so clearly.

I updated the PR to follow that model. The split feels much cleaner now: InputData can stay comfortable for raw input, and ValidatedInput can keep the stricter contract for data that already passed validation.

I also added tests around both behaviors and a short docs note so the distinction is clear.

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants