Skip to content

feat: add FormRequest for encapsulating validation and authorization#10087

Open
michalsn wants to merge 3 commits intocodeigniter4:4.8from
michalsn:feat/form-request
Open

feat: add FormRequest for encapsulating validation and authorization#10087
michalsn wants to merge 3 commits intocodeigniter4:4.8from
michalsn:feat/form-request

Conversation

@michalsn
Copy link
Copy Markdown
Member

@michalsn michalsn commented Apr 6, 2026

Description
This PR introduces FormRequest, an abstract base class that lets you move validation rules, custom error messages, and authorization logic out of controller methods and into a dedicated class. You type-hint the class in the controller signature, and the framework resolves, authorizes, and validates the request automatically before the method body runs.

A make:request spark command scaffolds new classes. Injection works in both controller methods and closure routes, with route parameters resolved positionally alongside the FormRequest.

I'd like your opinions on whether this belongs in the framework. Personally, I find it useful - once an app grows, controllers tend to fill up with validation boilerplate. FormRequest moves that out, keeps controllers focused on the "happy path", and makes the rules reusable across endpoints.

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

@michalsn michalsn added new feature PRs for new features 4.8 PRs that target the `4.8` branch. labels Apr 6, 2026
Copy link
Copy Markdown
Member

@paulbalandan paulbalandan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the same named class in Laravel useful before for focused request validation. If the functionality is similar to that, then I think it would be also beneficial here.

Some initial thoughts:

@patel-vansh
Copy link
Copy Markdown
Contributor

Overall, this is a really nice feature to have. Most of the times, I have to create a final class which has static functions which return array of validation rules and call Validation service in controller. But looks like I have easier way to do that from 4.8.0.

Thanks @michalsn

@neznaika0
Copy link
Copy Markdown
Contributor

You've started developing magic (autowire). It would be great not to limit yourself to one form and continue the idea. set the Autowire interface and any available class can be connected as in other frameworks.

I think it can be done a long time ago. But the framework adheres to the rules from the 2000s =) Less obscure magic. Thus, we come to the need for DI/ServiceLocator, which is now replacing Services.

You can view https://github.com/PHP-DI/PHP-DI as a separate package.

@neznaika0
Copy link
Copy Markdown
Contributor

I'm already making a similar object for validation myself, only directly PostService::fromRequest(PostShema::rules())

Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
@michalsn
Copy link
Copy Markdown
Member Author

michalsn commented Apr 6, 2026

@paulbalandan I took the idea for this directly from Laravel.

@michalsn
Copy link
Copy Markdown
Member Author

michalsn commented Apr 6, 2026

@neznaika0 Thanks for the feedback. FormRequest injection is intentionally narrow - it resolves one specific type at one specific point, not a general autowiring mechanism. Extending it into full container autowiring would go against CodeIgniter core philosophy of keeping things explicit and easy to follow. One of CI4 strengths is that you can trace exactly what's happening without implicit magic - and a general DI container trades that away.

Full DI container support is a legitimate feature in frameworks that prioritize that approach, but I don't believe it's the direction CI4 is going. That's not a weakness - it's more a positioning decision.

@neznaika0
Copy link
Copy Markdown
Contributor

That's the philosophy I'm talking about.

I'm worried that subsequent similar improvements will use the same principle, but duplicate the logic. Right now, I can't say what would be useful to dynamically add to the controllers.. Everything is possible - Models, Services, Configs.. in the end, we come to DI =).

I think that's why many people don't look towards CI, because they're used to magic SF, Laravel. Wordpress is an exception - it has a large community and allows non-standard approaches.

@michalsn
Copy link
Copy Markdown
Member Author

michalsn commented Apr 6, 2026

I'm worried that subsequent similar improvements will use the same principle, but duplicate the logic. Right now, I can't say what would be useful to dynamically add to the controllers.. Everything is possible - Models, Services, Configs.. in the end, we come to DI =).

That's a real concern. FormRequest is a one-off - it solves a specific problem with a clear boundary. I don't plan to allow similar injections without a broader design discussion first.

In fact Services are CodeIgniter answer to dependency management - explicit, overridable, and testable. They're not a DI container in the classical sense, but they serve the same purpose without the implicit magic.

*
* @return mixed
*/
public function __get(string $name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function __get(string $name)
public function __get(string $name): mixed

*/
public function __get(string $name)
{
return $this->validatedData[$name] ?? null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw instead if the name is not found in validated data?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic property access usually behaves softly. So if we want strict access, that should be a separate explicit method, not __get().

Maybe we should resign from this magic method and add something like:
validated(?string $key = null, mixed $default = null): mixed?

Or forget about the $default and just throw. I'm not sure what would be the best approach.

*/
public function __isset(string $name): bool
{
return isset($this->validatedData[$name]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use array_key_exists for $name with null value? Though, I'm not sure if such use is allowed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered using array_key_exists() at first, but validatedData currently does not preserve fields whose validated value is null. That data comes from Validation::getValidated(), which is built via DotArrayFilter, and DotArrayFilter currently treats null values like missing keys. So array_key_exists() would not change the behavior here today. The bigger question is whether dropping null from validatedData is an intentional behavior or a bug in the validation layer.

I also could not find any existing tests covering this behavior, so I added a small one in tests/system/Validation/DotArrayFilterTest.php:

public function testRunDropsNullValue(): void
{
    $data = [
        'foo' => null,
    ];

    $result = DotArrayFilter::run(['foo'], $data);

    $this->assertSame([], $result);
}

So at the moment this appears to be the current behavior, but I am not sure whether it is intentional or just an untested edge case in DotArrayFilter. For example, when a field is allowed by rules such as permit_empty, I would expect a null value to pass validation and remain present in validatedData.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalsn I haven't checked, theoretically, there is an if_exist rule.
They work in a similar way:
to pass if_exist validation, it is important to have a key (along with null), permit_empty seems to be the same thing, because it doesn't make sense to check the empty without a key.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neznaika0 Only the Validation::getValidated() is affected, the validation itself is acting as expected.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If #10101 will be merged, I will change this to array_key_exists().

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. new feature PRs for new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants