From e92187e27336678f81e8bb5ac4229452571c6df0 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Sun, 31 May 2026 20:41:55 +0200 Subject: [PATCH] feat(http): expose prepared FormRequest data during validation failure - Store prepared validation data before running FormRequest validation - Allow failedValidation() overrides to reuse the exact prepared payload - Avoid calling prepareForValidation() a second time in custom failure responses - Document normalized old-input handling Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/HTTP/FormRequest.php | 29 ++++++++++++- tests/system/HTTP/FormRequestTest.php | 42 +++++++++++++++++++ .../source/incoming/form_requests.rst | 13 ++++-- .../source/incoming/form_requests/013.php | 8 ++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/system/HTTP/FormRequest.php b/system/HTTP/FormRequest.php index 624114060cda..5e5e431db9d4 100644 --- a/system/HTTP/FormRequest.php +++ b/system/HTTP/FormRequest.php @@ -35,6 +35,13 @@ abstract class FormRequest */ private array $validatedData = []; + /** + * Data after prepareForValidation() and before validation rules run. + * + * @var array + */ + private array $preparedValidationData = []; + /** * When called by the framework, the current IncomingRequest is injected * explicitly. When instantiated manually (e.g. in tests), the constructor @@ -143,6 +150,21 @@ protected function prepareForValidation(array $data): array return $data; } + /** + * Returns the data after prepareForValidation() has run. + * + * This is useful in failedValidation() when a custom failure response needs + * the same prepared data that was passed to validation. This data has not + * passed validation; use getValidated() or getValidatedInput() after + * successful validation for trusted values. + * + * @return array + */ + protected function getPreparedValidationData(): array + { + return $this->preparedValidationData; + } + /** * Called when validation fails. Override to customize the failure response. * @@ -249,16 +271,19 @@ protected function validationData(): array */ final public function resolveRequest(): ?ResponseInterface { + $this->validatedData = []; + $this->preparedValidationData = []; + if (! $this->isAuthorized()) { return $this->failedAuthorization(); } - $data = $this->prepareForValidation($this->validationData()); + $this->preparedValidationData = $this->prepareForValidation($this->validationData()); $validation = service('validation') ->setRules($this->rules(), $this->messages()); - if (! $validation->run($data)) { + if (! $validation->run($this->preparedValidationData)) { return $this->failedValidation($validation->getErrors()); } diff --git a/tests/system/HTTP/FormRequestTest.php b/tests/system/HTTP/FormRequestTest.php index fe811fb3ee6a..030e5d737de4 100644 --- a/tests/system/HTTP/FormRequestTest.php +++ b/tests/system/HTTP/FormRequestTest.php @@ -395,6 +395,48 @@ public function testResolveRequestRedirectsForWildcardAcceptHeader(): void $this->assertSame(303, $response->getStatusCode()); } + public function testPreparedValidationDataIsAvailableDuringFailedValidationWithoutPreparingAgain(): void + { + service('superglobals')->setPost('title', ' Hello World '); + + $formRequest = new class ($this->makeRequest()) extends FormRequest { + public int $prepareCount = 0; + + /** + * @var array + */ + public array $preparedData = []; + + public function rules(): array + { + return [ + 'title' => 'required', + 'body' => 'required', + ]; + } + + protected function prepareForValidation(array $data): array + { + $this->prepareCount++; + $data['title'] = trim($data['title'] ?? ''); + + return $data; + } + + protected function failedValidation(array $errors): ResponseInterface + { + $this->preparedData = $this->getPreparedValidationData(); + + return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); + } + }; + + $formRequest->resolveRequest(); + + $this->assertSame(1, $formRequest->prepareCount); + $this->assertSame(['title' => 'Hello World'], $formRequest->preparedData); + } + // ------------------------------------------------------------------------- // Authorization failure // ------------------------------------------------------------------------- diff --git a/user_guide_src/source/incoming/form_requests.rst b/user_guide_src/source/incoming/form_requests.rst index ebcd6ff28bd3..cdfc594838df 100644 --- a/user_guide_src/source/incoming/form_requests.rst +++ b/user_guide_src/source/incoming/form_requests.rst @@ -226,13 +226,20 @@ Flashing Normalized Input If your ``prepareForValidation()`` transforms visible form fields (for example, trimming strings or canonicalizing values), ``old()`` will return the original -submitted input because the redirect flashes the raw superglobals. To make -``old()`` reflect the normalized values instead, override ``failedValidation()`` -and flash the normalized payload manually: +submitted input because the redirect flashes the raw superglobals. + +To make ``old()`` reflect the normalized values instead, override +``failedValidation()`` and flash the data returned from +``prepareForValidation()``: .. literalinclude:: form_requests/013.php :lines: 2- +Use ``getPreparedValidationData()`` inside ``failedValidation()`` to read that +prepared data without running ``prepareForValidation()`` again. The prepared +data has not passed validation. After successful validation, use +``getValidated()`` or ``getValidatedInput()`` for trusted values. + ***************************************** How the Framework Resolves Form Requests ***************************************** diff --git a/user_guide_src/source/incoming/form_requests/013.php b/user_guide_src/source/incoming/form_requests/013.php index 92511ff3903b..c742347d9033 100644 --- a/user_guide_src/source/incoming/form_requests/013.php +++ b/user_guide_src/source/incoming/form_requests/013.php @@ -32,14 +32,14 @@ protected function failedValidation(array $errors): ResponseInterface return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); } - // withInput() flashes the original superglobals and the validation - // errors. We then overwrite old input with the normalized payload so - // that old() returns the same values that were validated. + // withInput() flashes the original superglobals and validation errors. + // Then we overwrite old input with the prepared data so old() returns + // the same values that were passed to validation. $redirect = redirect()->back()->withInput(); service('session')->setFlashdata('_ci_old_input', [ 'get' => [], - 'post' => $this->prepareForValidation($this->validationData()), + 'post' => $this->getPreparedValidationData(), ]); return $redirect;