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
8 changes: 8 additions & 0 deletions system/HTTP/FormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ public function validated(): array
return $this->validatedData;
}

/**
* Returns the validated data as a typed input object.
*/
public function validatedInput(): ValidatedInput
{
return new ValidatedInput($this->validatedData);
}

/**
* Returns a single validated field value by name, or the default value
* if the field is not present in the validated data.
Expand Down
232 changes: 232 additions & 0 deletions system/HTTP/ValidatedInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\HTTP;

use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use DateTimeZone;
use Exception;
use ReflectionEnum;
use UnitEnum;

/**
* @see \CodeIgniter\HTTP\ValidatedInputTest
*/
class ValidatedInput
{
/**
* @param array<string, mixed> $data
*/
public function __construct(private readonly array $data)
{
}

/**
* Returns a single validated field value by name, or the default value
* if the field is not present in the validated data.
*
* Supports dot-array syntax for nested validated data.
*/
public function get(string $key, mixed $default = null): mixed
{
helper('array');

if (! dot_array_has($key, $this->data)) {
return $default;
}

return dot_array_search($key, $this->data);
}

/**
* Returns true when the named field exists in the validated data, even if
* its value is null.
*
* Supports dot-array syntax for nested validated data.
*/
public function has(string $key): bool
{
helper('array');

return dot_array_has($key, $this->data);
}

/**
* Returns a validated field as an integer.
*
* Supports dot-array syntax for nested validated data.
*/
public function integer(string $key, ?int $default = null): ?int
{
$value = $this->get($key, $default);

if ($value === null || is_int($value)) {
return $value;
}

if (is_string($value)) {
$integer = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);

if ($integer !== null) {
return $integer;
}
}

throw $this->invalidType($key, 'integer');
}

/**
* Returns a validated field as a boolean.
*
* Supports dot-array syntax for nested validated data.
*/
public function boolean(string $key, ?bool $default = null): ?bool
{
$value = $this->get($key, $default);

if ($value === null || is_bool($value)) {
return $value;
}

if (is_int($value) || is_string($value)) {
$boolean = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

if ($boolean !== null) {
return $boolean;
}
}

throw $this->invalidType($key, 'boolean');
}

/**
* Returns a validated field as a Time instance.
*
* Supports dot-array syntax for nested validated data.
*/
public function date(
string $key,
?string $format = null,
DateTimeZone|string|null $timezone = null,
): ?Time {
$value = $this->get($key);

if ($value === null) {
return null;
}

if (! is_string($value) || $value === '') {
throw $this->invalidType($key, 'date');
}

try {
if ($format === null) {
return Time::parse($value, $timezone);
}

return Time::createFromFormat($format, $value, $timezone);
} catch (Exception) {
throw $this->invalidType($key, 'date');
}
}

/**
* Returns a validated field as an enum instance.
*
* Supports dot-array syntax for nested validated data.
*
* @template TEnum of UnitEnum
*
* @param class-string<TEnum> $enumClass
* @param TEnum|null $default
*
* @return TEnum|null
*/
public function enum(string $key, string $enumClass, ?UnitEnum $default = null): ?UnitEnum
{
if (! enum_exists($enumClass)) {
throw new InvalidArgumentException('The "' . $enumClass . '" class is not a valid enum.');
}

if ($default instanceof UnitEnum && ! $default instanceof $enumClass) {
throw $this->invalidType($key, $enumClass);
}

$value = $this->get($key, $default);

if ($value === null) {
return null;
}

if ($value instanceof UnitEnum) {
if ($value instanceof $enumClass) {
return $value;
}

throw $this->invalidType($key, $enumClass);
}

$reflection = new ReflectionEnum($enumClass);

if ($reflection->isBacked()) {
return $this->backedEnum($key, $enumClass, $reflection, $value);
}

if (is_string($value)) {
foreach ($enumClass::cases() as $case) {
if ($case->name === $value) {
return $case;
}
}
}

throw $this->invalidType($key, $enumClass);
}

private function backedEnum(string $key, string $enumClass, ReflectionEnum $reflection, mixed $value): UnitEnum
{
$backingType = $reflection->getBackingType()?->getName();

if ($backingType === 'int') {
if (is_string($value)) {
$value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
}

if (! is_int($value)) {
throw $this->invalidType($key, $enumClass);
}
} elseif (! is_int($value) && ! is_string($value)) {
throw $this->invalidType($key, $enumClass);
}

if ($backingType === 'string') {
$value = (string) $value;
}

$enum = $enumClass::tryFrom($value);

if ($enum === null) {
throw $this->invalidType($key, $enumClass);
}

return $enum;
}

private function invalidType(string $key, string $type): InvalidArgumentException
{
return new InvalidArgumentException(
sprintf('The validated "%s" value cannot be read as %s.', $key, $type),
);
}
}
9 changes: 9 additions & 0 deletions system/Validation/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ValidatedInput;
use CodeIgniter\Validation\Exceptions\ValidationException;
use CodeIgniter\View\RendererInterface;

Expand Down Expand Up @@ -271,6 +272,14 @@ public function getValidated(): array
return $this->validated;
}

/**
* Returns the actual validated data as a typed input object.
*/
public function getValidatedInput(): ValidatedInput
{
return new ValidatedInput($this->validated);
}

/**
* Runs all of $rules against $field, until one fails, or
* all of them have been processed. If one fails, it adds
Expand Down
6 changes: 6 additions & 0 deletions system/Validation/ValidationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ValidatedInput;

/**
* Expected behavior of a validator
Expand Down Expand Up @@ -162,4 +163,9 @@ public function showError(string $field, string $template = 'single'): string;
* Returns the actual validated data.
*/
public function getValidated(): array;

/**
* Returns the actual validated data as a typed input object.
*/
public function getValidatedInput(): ValidatedInput;
}
14 changes: 14 additions & 0 deletions tests/system/HTTP/FormRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@ public function rules(): array
$this->assertTrue($formRequest->hasValidated('note'));
}

public function testValidatedInputReturnsValidatedInputObject(): void
{
service('superglobals')->setPost('title', 'Hello World');
service('superglobals')->setPost('body', 'Some body text');

$formRequest = new ValidPostFormRequest($this->makeRequest());
$formRequest->resolveRequest();

$input = $formRequest->validatedInput();

$this->assertInstanceOf(ValidatedInput::class, $input);
$this->assertSame('Hello World', $input->get('title'));
}

// -------------------------------------------------------------------------
// prepareForValidation hook
// -------------------------------------------------------------------------
Expand Down
Loading
Loading