Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Form helpers for Validation Errors #6384

Merged
merged 18 commits into from
Aug 22, 2022
Merged
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
5 changes: 0 additions & 5 deletions phpstan-baseline.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -655,11 +655,6 @@ parameters:
count: 1
path: system/Throttle/Throttler.php

-
message: "#^Property CodeIgniter\\\\Validation\\\\Validation\\:\\:\\$errors \\(array\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: system/Validation/Validation.php

-
message: "#^Variable \\$error on left side of \\?\\? always exists and is always null\\.$#"
count: 1
Expand Down
12 changes: 4 additions & 8 deletions system/HTTP/RedirectResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ public function back(?int $code = null, string $method = 'auto')
}

/**
* Specifies that the current $_GET and $_POST arrays should be
* packaged up with the response.
* Sets the current $_GET and $_POST arrays in the session.
* This also saves the validation errors.
*
* It will then be available via the 'old()' helper function.
*
Expand All @@ -94,22 +94,18 @@ public function withInput()
'post' => $_POST ?? [],
]);

// @TODO Remove this in the future.
// See https://github.com/codeigniter4/CodeIgniter4/issues/5839#issuecomment-1086624600
$this->withErrors();

return $this;
}

/**
* Set validation errors in the session.
* Sets validation errors in the session.
*
* If the validation has any errors, transmit those back
* so they can be displayed when the validation is handled
* within a method different than displaying the form.
*
* @TODO Make this method public when removing $this->withErrors() in withInput().
*
* @return $this
*/
private function withErrors(): self
Expand All @@ -118,7 +114,7 @@ private function withErrors(): self

if ($validation->getErrors()) {
$session = Services::session();
$session->setFlashdata('_ci_validation_errors', serialize($validation->getErrors()));
$session->setFlashdata('_ci_validation_errors', $validation->getErrors());
}

return $this;
Expand Down
80 changes: 80 additions & 0 deletions system/Helpers/form_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* the LICENSE file that was distributed with this source code.
*/

use CodeIgniter\Validation\Exceptions\ValidationException;
use Config\App;
use Config\Services;

Expand Down Expand Up @@ -680,12 +681,91 @@ function set_radio(string $field, string $value = '', bool $default = false): st
}
}

if (! function_exists('validation_errors')) {
/**
* Returns the validation errors.
*
* First, checks the validation errors that are stored in the session.
* To store the errors in the session, you need to use `withInput()` with `redirect()`.
*
* The returned array should be in the following format:
* [
* 'field1' => 'error message',
* 'field2' => 'error message',
* ]
*
* @return array<string, string>
*/
function validation_errors()
{
session();

// Check the session to see if any were
// passed along from a redirect withErrors() request.
if (isset($_SESSION['_ci_validation_errors']) && (ENVIRONMENT === 'testing' || ! is_cli())) {
return $_SESSION['_ci_validation_errors'];
}

$validation = Services::validation();

return $validation->getErrors();
}
}

if (! function_exists('validation_list_errors')) {
/**
* Returns the rendered HTML of the validation errors.
*
* See Validation::listErrors()
*/
function validation_list_errors(string $template = 'list'): string
{
$config = config('Validation');
$view = Services::renderer();

if (! array_key_exists($template, $config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}

return $view->setVar('errors', validation_errors())
->render($config->templates[$template]);
}
}

if (! function_exists('validation_show_error')) {
/**
* Returns a single error for the specified field in formatted HTML.
*
* See Validation::showError()
*/
function validation_show_error(string $field, string $template = 'single'): string
{
$config = config('Validation');
$view = Services::renderer();

$errors = validation_errors();

if (! array_key_exists($field, $errors)) {
return '';
}

if (! array_key_exists($template, $config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}

return $view->setVar('error', $errors[$field])
->render($config->templates[$template]);
}
}

if (! function_exists('parse_form_attributes')) {
/**
* Parse the form attributes
*
* Helper function used by some of the form helpers
*
* @internal
*
* @param array|string $attributes List of attributes
* @param array $default Default values
*/
Expand Down
19 changes: 5 additions & 14 deletions system/Validation/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,6 @@ public function __construct($config, RendererInterface $view)
*/
public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool
{
// If there are still validation errors for redirect_with_input request, remove them.
// See `getErrors()` method.
if (isset($_SESSION, $_SESSION['_ci_validation_errors'])) {
unset($_SESSION['_ci_validation_errors']);
}

$data ??= $this->data;

// i.e. is_unique
Expand Down Expand Up @@ -506,6 +500,8 @@ public function setRuleGroup(string $group)

/**
* Returns the rendered HTML of the errors as defined in $template.
*
* You can also use validation_list_errors() in Form helper.
Copy link
Member

Choose a reason for hiding this comment

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

Should we deprecate this function and require the use of the helper? It's always been rather weird that our Validation class has a dependency on View.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is certainly possible to deprecate this method since it duplicates functionality.
If so, what would we do with the error view files and templates setting?
See https://codeigniter4.github.io/CodeIgniter4/libraries/validation.html#configuration

Copy link
Member

Choose a reason for hiding this comment

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

It looks to me like your functions still use those components. What else would you do with them?

Copy link
Member Author

Choose a reason for hiding this comment

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

My question is: If we decouple View from the Validation class, where do we put the validation error templates and its configuration?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Counterquestion. Why are error patterns needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

What's error pattern?

*/
public function listErrors(string $template = 'list'): string
{
Expand All @@ -520,6 +516,8 @@ public function listErrors(string $template = 'list'): string

/**
* Displays a single error in formatted HTML as defined in the $template view.
*
* You can also use validation_show_error() in Form helper.
*/
public function showError(string $field, string $template = 'single'): string
{
Expand Down Expand Up @@ -681,14 +679,7 @@ public function getError(?string $field = null): string
*/
public function getErrors(): array
{
// If we already have errors, we'll use those.
// If we don't, check the session to see if any were
// passed along from a redirect_with_input request.
if (empty($this->errors) && ! is_cli() && isset($_SESSION, $_SESSION['_ci_validation_errors'])) {
$this->errors = unserialize($_SESSION['_ci_validation_errors']);
}

return $this->errors ?? [];
return $this->errors;
}

/**
Expand Down
37 changes: 37 additions & 0 deletions tests/system/Helpers/FormHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,43 @@ public function testSetRadioDefault()
$this->assertSame('', set_radio('code', 'beta', false));
}

public function testValidationErrorsFromSession()
{
$_SESSION = ['_ci_validation_errors' => ['foo' => 'bar']];

$this->assertSame(['foo' => 'bar'], validation_errors());

$_SESSION = [];
}

public function testValidationErrorsFromValidation()
{
$validation = Services::validation();
$validation->setRule('id', 'ID', 'required')->run([]);

$this->assertSame(['id' => 'The ID field is required.'], validation_errors());
}

public function testValidationListErrors()
{
$validation = Services::validation();
$validation->setRule('id', 'ID', 'required')->run([]);

$html = validation_list_errors();

$this->assertStringContainsString('<li>The ID field is required.</li>', $html);
}

public function testValidationShowError()
{
$validation = Services::validation();
$validation->setRule('id', 'ID', 'required')->run([]);

$html = validation_show_error('id');

$this->assertSame('<span class="help-block">The ID field is required.</span>' . "\n", $html);
}

public function testFormParseFormAttributesTrue()
{
$expected = 'readonly ';
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Others
- Now ``spark routes`` command shows route names. See :ref:`URI Routing <routing-spark-routes>`.
- Added new :ref:`entities-property-casting` class ``IntBoolCast`` for Entity.
- Help information for a spark command can now be accessed using the ``--help`` option (e.g. ``php spark serve --help``)
- Added new Form helper function :php:func:`validation_errors()`, :php:func:`validation_list_errors()` and :php:func:`validation_show_error()` to display Validation Errors.

Changes
*******
Expand Down
55 changes: 55 additions & 0 deletions user_guide_src/source/helpers/form_helper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,58 @@ The following functions are available:
<input type="radio" name="myradio" value="1" <?= set_radio('myradio', '1', true) ?> />
<input type="radio" name="myradio" value="2" <?= set_radio('myradio', '2') ?> />

.. php:function:: validation_errors()

:returns: The validation errors
:rtype: array

This function was introduced in v4.3.0.
MGatner marked this conversation as resolved.
Show resolved Hide resolved

Returns the validation errors. First, this function checks the validation errors
that are stored in the session. To store the errors in the session, you need to use ``withInput()`` with :php:func:`redirect() <redirect>`.

The returned array is the same as ``Validation::getErrors()``.
See :ref:`Validation <validation-getting-all-errors>` for details.

Example::

<?php $errors = validation_errors(); ?>

.. php:function:: validation_list_errors($template = 'list')

:param string $template: Validation template name
:returns: Rendered HTML of the validation errors
:rtype: string

This function was introduced in v4.3.0.

Returns the rendered HTML of the validation errors.

The parameter ``$template`` is a Validation template name.
See :ref:`validation-customizing-error-display` for details.

This function uses :php:func:`validation_errors()` internally.

Example::

<?= validation_list_errors() ?>

.. php:function:: validation_show_error($field, $template = 'single')

:param string $field: Field name
:param string $template: Validation template name
:returns: Rendered HTML of the validation error
:rtype: string

This function was introduced in v4.3.0.

Returns a single error for the specified field in formatted HTML.

The parameter ``$template`` is a Validation template name.
See :ref:`validation-customizing-error-display` for details.

This function uses :php:func:`validation_errors()` internally.

Example::

<?= validation_show_error('username') ?>
2 changes: 1 addition & 1 deletion user_guide_src/source/incoming/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ Routes are registered in the routing table in the order in which they are define

.. note:: If a route (the URI path) is defined more than once with different handlers, only the first defined route is registered.

You can check registered routes in the routing table by running the :ref:`spark routes <spark-routes>` command.
You can check registered routes in the routing table by running the :ref:`spark routes <routing-spark-routes>` command.

Changing Route Priority
=======================
Expand Down
22 changes: 22 additions & 0 deletions user_guide_src/source/installation/upgrade_430.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ HTTP Status Code and Exit Code of Uncaught Exceptions
- If you expect *Exit code* based on *Exception code*, the Exit code will be changed.
In that case, you need to implement ``HasExitCodeInterface`` in the Exception. See :ref:`error-specify-exit-code`.

redirect()->withInput() and Validation Errors
=============================================

``redirect()->withInput()`` and Validation errors had an undocumented behavior.
If you redirect with ``withInput()``, CodeIgniter stores the validation errors
in the session, and you can get the errors in the redirected page from
a validation object *before a new validation is run*::

// In the controller
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}

// In the view of the redirected page
<?= service('Validation')->listErrors() ?>

This behavior was a bug and fixed in v4.3.0.

If you have code that depends on the bug, you need to change the code.
Use new Form helpers, :php:func:`validation_errors()`, :php:func:`validation_list_errors()` and :php:func:`validation_show_error()` to display Validation Errors,
instead of the Validation object.

Others
======

Expand Down
5 changes: 3 additions & 2 deletions user_guide_src/source/installation/upgrade_validations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ What has been changed
- CI4 validation has no Callbacks nor Callable in CI3.
- CI4 validation format rules do not permit empty string.
- CI4 validation never changes your data.
- Since v4.3.0, :php:func:`validation_errors()` has been introduced, but the API is different from CI3's.

Upgrade Guide
=============
1. Within the view which contains the form you have to change:

- ``<?php echo validation_errors(); ?>`` to ``<?= $validation->listErrors() ?>``
- ``<?php echo validation_errors(); ?>`` to ``<?= validation_list_errors() ?>``

2. Within the controller you have to change the following:

Expand Down Expand Up @@ -85,7 +86,7 @@ Path: **app/Views**::
</head>
<body>

<?= $validation->listErrors() ?>
<?= validation_list_errors() ?>

<?= form_open('form') ?>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ public function index()
if (! $this->validate([
// Validation rules
])) {
echo view('myform', [
'validation' => $this->validator,
]);
echo view('myform');
} else {
echo view('formsuccess');
}
Expand Down
Loading