Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions system/HTTP/FormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,29 @@ protected function prepareForValidation(array $data): array
* returns a 422 JSON response instead.
*
* @param array<string, string> $errors
* @param array<string, mixed> $preparedData
*/
protected function failedValidation(array $errors): ResponseInterface
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
{
if ($this->shouldReturnJsonResponse()) {
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
}

return redirect()->back()->withInput();
$redirect = redirect()->back()->withInput();

$key = in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true)
? 'get'
: 'post';

$oldInput = [
'get' => [],
'post' => [],
];
$oldInput[$key] = $preparedData;

service('session')->setFlashdata('_ci_old_input', $oldInput);

return $redirect;
}

/**
Expand Down Expand Up @@ -249,6 +264,8 @@ protected function validationData(): array
*/
final public function resolveRequest(): ?ResponseInterface
{
$this->validatedData = [];

if (! $this->isAuthorized()) {
return $this->failedAuthorization();
}
Expand All @@ -259,7 +276,7 @@ final public function resolveRequest(): ?ResponseInterface
->setRules($this->rules(), $this->messages());

if (! $validation->run($data)) {
return $this->failedValidation($validation->getErrors());
return $this->failedValidation($validation->getErrors(), $data);
}

$this->validatedData = $validation->getValidated();
Expand Down
106 changes: 105 additions & 1 deletion tests/system/HTTP/FormRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,110 @@ public function testResolveRequestRedirectsForWildcardAcceptHeader(): void
$this->assertSame(303, $response->getStatusCode());
}

public function testPreparedValidationDataIsPassedToFailedValidationWithoutPreparingAgain(): void
{
service('superglobals')->setPost('title', ' Hello World ');

$formRequest = new class ($this->makeRequest()) extends FormRequest {
public int $prepareCount = 0;

/**
* @var array<string, mixed>
*/
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, array $preparedData): ResponseInterface
{
$this->preparedData = $preparedData;

return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
}
};

$formRequest->resolveRequest();

$this->assertSame(1, $formRequest->prepareCount);
$this->assertSame(['title' => 'Hello World'], $formRequest->preparedData);
}

#[RunInSeparateProcess]
public function testDefaultFailedValidationFlashesPreparedValidationDataAsOldInput(): void
{
/** @var array<string, mixed> $_SESSION */
$_SESSION = [];

service('superglobals')->setPost('title', ' Hello World ');

$formRequest = new class ($this->makeRequest()) extends FormRequest {
public function rules(): array
{
return [
'title' => 'required',
'body' => 'required',
];
}

protected function prepareForValidation(array $data): array
{
$data['title'] = trim($data['title'] ?? '');

return $data;
}
};

$formRequest->resolveRequest();

$this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['post']);
}

#[RunInSeparateProcess]
public function testDefaultFailedValidationFlashesPreparedGetDataAsOldInput(): void
{
/** @var array<string, mixed> $_SESSION */
$_SESSION = [];

service('superglobals')->setServer('REQUEST_METHOD', 'GET');
service('superglobals')->setGet('title', ' Hello World ');

$formRequest = new class ($this->makeRequest()) extends FormRequest {
public function rules(): array
{
return [
'title' => 'required',
'body' => 'required',
];
}

protected function prepareForValidation(array $data): array
{
$data['title'] = trim($data['title'] ?? '');

return $data;
}
};

$formRequest->resolveRequest();

$this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['get']);
$this->assertSame([], $_SESSION['_ci_old_input']['post']);
}

// -------------------------------------------------------------------------
// Authorization failure
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -488,7 +592,7 @@ public function rules(): array
return ['title' => 'required'];
}

protected function failedValidation(array $errors): ResponseInterface
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
{
self::$called = true;

Expand Down
20 changes: 12 additions & 8 deletions user_guide_src/source/incoming/form_requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,9 @@ normalized phone numbers, or trimmed strings.
.. literalinclude:: form_requests/006.php
:lines: 2-

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

.. _form-request-validation-data:

Expand Down Expand Up @@ -225,14 +224,19 @@ 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:
trimming strings or canonicalizing values), the default redirect response flashes
the prepared validation data as old input.

If you override ``failedValidation()`` and still need to flash normalized input,
use the second ``$preparedData`` argument. It contains the same data that was
passed to validation:

.. literalinclude:: form_requests/013.php
:lines: 2-

The prepared data has not passed validation. After successful validation, use
``getValidated()`` or ``getValidatedInput()`` for trusted values.

*****************************************
How the Framework Resolves Form Requests
*****************************************
Expand Down
2 changes: 1 addition & 1 deletion user_guide_src/source/incoming/form_requests/008.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function rules(): array
}

// Always respond with JSON, regardless of the request type.
protected function failedValidation(array $errors): ResponseInterface
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
{
return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
}
Expand Down
11 changes: 5 additions & 6 deletions user_guide_src/source/incoming/form_requests/013.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ protected function prepareForValidation(array $data): array
return $data;
}

// Override so that old() reflects the normalized values on redirect.
protected function failedValidation(array $errors): ResponseInterface
// Override while still flashing the prepared values on redirect.
protected function failedValidation(array $errors, array $preparedData): ResponseInterface
{
if (
$this->request->is('json')
Expand All @@ -32,14 +32,13 @@ 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 validation errors. Then we replace old input with
// the same prepared values that were passed to validation.
$redirect = redirect()->back()->withInput();

service('session')->setFlashdata('_ci_old_input', [
'get' => [],
'post' => $this->prepareForValidation($this->validationData()),
'post' => $preparedData,
]);

return $redirect;
Expand Down
Loading