Skip to content

Commit 874a820

Browse files
authored
feat: expose prepared FormRequest data during validation failure (#10259)
1 parent f33d181 commit 874a820

5 files changed

Lines changed: 175 additions & 19 deletions

File tree

system/HTTP/FormRequest.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,29 @@ protected function prepareForValidation(array $data): array
153153
* returns a 422 JSON response instead.
154154
*
155155
* @param array<string, string> $errors
156+
* @param array<string, mixed> $preparedData
156157
*/
157-
protected function failedValidation(array $errors): ResponseInterface
158+
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
158159
{
159160
if ($this->shouldReturnJsonResponse()) {
160161
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
161162
}
162163

163-
return redirect()->back()->withInput();
164+
$redirect = redirect()->back()->withInput();
165+
166+
$key = in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)
167+
? 'get'
168+
: 'post';
169+
170+
$oldInput = [
171+
'get' => service('superglobals')->getGetArray(),
172+
'post' => service('superglobals')->getPostArray(),
173+
];
174+
$oldInput[$key] = $preparedData;
175+
176+
service('session')->setFlashdata('_ci_old_input', $oldInput);
177+
178+
return $redirect;
164179
}
165180

166181
/**
@@ -249,6 +264,8 @@ protected function validationData(): array
249264
*/
250265
final public function resolveRequest(): ?ResponseInterface
251266
{
267+
$this->validatedData = [];
268+
252269
if (! $this->isAuthorized()) {
253270
return $this->failedAuthorization();
254271
}
@@ -259,7 +276,7 @@ final public function resolveRequest(): ?ResponseInterface
259276
->setRules($this->rules(), $this->messages());
260277

261278
if (! $validation->run($data)) {
262-
return $this->failedValidation($validation->getErrors());
279+
return $this->failedValidation($validation->getErrors(), $data);
263280
}
264281

265282
$this->validatedData = $validation->getValidated();

tests/system/HTTP/FormRequestTest.php

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,142 @@ public function testResolveRequestRedirectsForWildcardAcceptHeader(): void
395395
$this->assertSame(303, $response->getStatusCode());
396396
}
397397

398+
public function testPreparedValidationDataIsPassedToFailedValidationWithoutPreparingAgain(): void
399+
{
400+
service('superglobals')->setPost('title', ' Hello World ');
401+
402+
$formRequest = new class ($this->makeRequest()) extends FormRequest {
403+
public int $prepareCount = 0;
404+
405+
/**
406+
* @var array<string, mixed>
407+
*/
408+
public array $preparedData = [];
409+
410+
public function rules(): array
411+
{
412+
return [
413+
'title' => 'required',
414+
'body' => 'required',
415+
];
416+
}
417+
418+
protected function prepareForValidation(array $data): array
419+
{
420+
$this->prepareCount++;
421+
$data['title'] = trim($data['title'] ?? '');
422+
423+
return $data;
424+
}
425+
426+
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
427+
{
428+
$this->preparedData = $preparedData;
429+
430+
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
431+
}
432+
};
433+
434+
$formRequest->resolveRequest();
435+
436+
$this->assertSame(1, $formRequest->prepareCount);
437+
$this->assertSame(['title' => 'Hello World'], $formRequest->preparedData);
438+
}
439+
440+
#[RunInSeparateProcess]
441+
public function testDefaultFailedValidationFlashesPreparedValidationDataAsOldInput(): void
442+
{
443+
/** @var array<string, mixed> $_SESSION */
444+
$_SESSION = [];
445+
446+
service('superglobals')->setPost('title', ' Hello World ');
447+
448+
$formRequest = new class ($this->makeRequest()) extends FormRequest {
449+
public function rules(): array
450+
{
451+
return [
452+
'title' => 'required',
453+
'body' => 'required',
454+
];
455+
}
456+
457+
protected function prepareForValidation(array $data): array
458+
{
459+
$data['title'] = trim($data['title'] ?? '');
460+
461+
return $data;
462+
}
463+
};
464+
465+
$formRequest->resolveRequest();
466+
467+
$this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['post']);
468+
}
469+
470+
#[RunInSeparateProcess]
471+
public function testDefaultFailedValidationFlashesPreparedGetDataAsOldInput(): void
472+
{
473+
/** @var array<string, mixed> $_SESSION */
474+
$_SESSION = [];
475+
476+
service('superglobals')->setServer('REQUEST_METHOD', 'GET');
477+
service('superglobals')->setGet('title', ' Hello World ');
478+
479+
$formRequest = new class ($this->makeRequest()) extends FormRequest {
480+
public function rules(): array
481+
{
482+
return [
483+
'title' => 'required',
484+
'body' => 'required',
485+
];
486+
}
487+
488+
protected function prepareForValidation(array $data): array
489+
{
490+
$data['title'] = trim($data['title'] ?? '');
491+
492+
return $data;
493+
}
494+
};
495+
496+
$formRequest->resolveRequest();
497+
498+
$this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['get']);
499+
$this->assertSame([], $_SESSION['_ci_old_input']['post']);
500+
}
501+
502+
#[RunInSeparateProcess]
503+
public function testDefaultFailedValidationPreservesGetDataWhenPostDataIsPrepared(): void
504+
{
505+
/** @var array<string, mixed> $_SESSION */
506+
$_SESSION = [];
507+
508+
service('superglobals')->setGet('category', '2');
509+
service('superglobals')->setPost('title', ' Hello World ');
510+
511+
$formRequest = new class ($this->makeRequest()) extends FormRequest {
512+
public function rules(): array
513+
{
514+
return [
515+
'title' => 'required',
516+
'body' => 'required',
517+
];
518+
}
519+
520+
protected function prepareForValidation(array $data): array
521+
{
522+
$data['title'] = trim($data['title'] ?? '');
523+
524+
return $data;
525+
}
526+
};
527+
528+
$formRequest->resolveRequest();
529+
530+
$this->assertSame(['category' => '2'], $_SESSION['_ci_old_input']['get']);
531+
$this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['post']);
532+
}
533+
398534
// -------------------------------------------------------------------------
399535
// Authorization failure
400536
// -------------------------------------------------------------------------
@@ -488,7 +624,7 @@ public function rules(): array
488624
return ['title' => 'required'];
489625
}
490626

491-
protected function failedValidation(array $errors): ResponseInterface
627+
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
492628
{
493629
self::$called = true;
494630

user_guide_src/source/incoming/form_requests.rst

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,9 @@ normalized phone numbers, or trimmed strings.
171171
.. literalinclude:: form_requests/006.php
172172
:lines: 2-
173173

174-
.. note:: ``old()`` returns the original submitted input, not the normalized
175-
values. Use ``getValidated()`` to access the processed data after a successful
176-
request. If you need ``old()`` to reflect normalized values, see
177-
:ref:`form-request-flash-normalized`.
174+
.. note:: When validation fails and the default redirect response is used,
175+
``old()`` returns the prepared validation data. Use ``getValidated()`` to
176+
access the processed data after a successful request.
178177

179178
.. _form-request-validation-data:
180179

@@ -225,14 +224,19 @@ Flashing Normalized Input
225224
=========================
226225

227226
If your ``prepareForValidation()`` transforms visible form fields (for example,
228-
trimming strings or canonicalizing values), ``old()`` will return the original
229-
submitted input because the redirect flashes the raw superglobals. To make
230-
``old()`` reflect the normalized values instead, override ``failedValidation()``
231-
and flash the normalized payload manually:
227+
trimming strings or canonicalizing values), the default redirect response flashes
228+
the prepared validation data as old input.
229+
230+
If you override ``failedValidation()`` and still need to flash normalized input,
231+
use the second ``$preparedData`` argument. It contains the same data that was
232+
passed to validation:
232233

233234
.. literalinclude:: form_requests/013.php
234235
:lines: 2-
235236

237+
The prepared data has not passed validation. After successful validation, use
238+
``getValidated()`` or ``getValidatedInput()`` for trusted values.
239+
236240
*****************************************
237241
How the Framework Resolves Form Requests
238242
*****************************************

user_guide_src/source/incoming/form_requests/008.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function rules(): array
1616
}
1717

1818
// Always respond with JSON, regardless of the request type.
19-
protected function failedValidation(array $errors): ResponseInterface
19+
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
2020
{
2121
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
2222
}

user_guide_src/source/incoming/form_requests/013.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ protected function prepareForValidation(array $data): array
2222
return $data;
2323
}
2424

25-
// Override so that old() reflects the normalized values on redirect.
26-
protected function failedValidation(array $errors): ResponseInterface
25+
// Override while still flashing the prepared values on redirect.
26+
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
2727
{
2828
if (
2929
$this->request->is('json')
@@ -32,14 +32,13 @@ protected function failedValidation(array $errors): ResponseInterface
3232
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
3333
}
3434

35-
// withInput() flashes the original superglobals and the validation
36-
// errors. We then overwrite old input with the normalized payload so
37-
// that old() returns the same values that were validated.
35+
// withInput() flashes validation errors. Then we replace old input with
36+
// the same prepared values that were passed to validation.
3837
$redirect = redirect()->back()->withInput();
3938

4039
service('session')->setFlashdata('_ci_old_input', [
4140
'get' => [],
42-
'post' => $this->prepareForValidation($this->validationData()),
41+
'post' => $preparedData,
4342
]);
4443

4544
return $redirect;

0 commit comments

Comments
 (0)