Skip to content

feat: add typed FormRequest accessors#10158

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

feat: add typed FormRequest accessors#10158
memleakd wants to merge 3 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 adding 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. Typed access now lives on an object that represents validated input, so the same feature is also available from the Validation service.

$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();

Why

This keeps controller code clearer after validation succeeds, without turning FormRequest into a growing collection of typed helper methods and without changing the existing array-based APIs.

validated() and getValidated() continue returning arrays as before. The typed input object is an optional convenience for reading common validated values in the expected type.

Behavior

The new ValidatedInput object provides:

  • get()
  • has()
  • integer()
  • boolean()
  • date()
  • enum()

These methods read from validated data only. They do not replace validation rules. If a present value cannot be read as the requested type, an InvalidArgumentException is thrown.

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

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 the concise controller usage, while the Validation docs provide the canonical ValidatedInput reference and behavior details.

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.

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