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
29 changes: 27 additions & 2 deletions system/HTTP/FormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ abstract class FormRequest
*/
private array $validatedData = [];

/**
* Data after prepareForValidation() and before validation rules run.
*
* @var array<string, mixed>
*/
private array $preparedValidationData = [];

/**
* When called by the framework, the current IncomingRequest is injected
* explicitly. When instantiated manually (e.g. in tests), the constructor
Expand Down Expand Up @@ -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<string, mixed>
*/
protected function getPreparedValidationData(): array
{
return $this->preparedValidationData;
}

/**
* Called when validation fails. Override to customize the failure response.
*
Expand Down Expand Up @@ -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());
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.

Instead of all these changes, I would simply modify the failedValidation() method:

protected function failedValidation(array $errors, array $preparedData): ResponseInterface 
{
    if ($this->shouldReturnJsonResponse()) {
        return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]);
    }

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

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

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

    return $redirect;
}

}

Expand Down
42 changes: 42 additions & 0 deletions tests/system/HTTP/FormRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<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): 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
// -------------------------------------------------------------------------
Expand Down
13 changes: 10 additions & 3 deletions user_guide_src/source/incoming/form_requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
*****************************************
Expand Down
8 changes: 4 additions & 4 deletions user_guide_src/source/incoming/form_requests/013.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading