Skip to content

Commit

Permalink
Merge pull request cakephp#17403 from cakephp/5.x-enum-validation
Browse files Browse the repository at this point in the history
5.x - Pull in Enum validation support from 5.next
  • Loading branch information
markstory authored Nov 4, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents cbb5022 + ee94828 commit b8bc29b
Showing 6 changed files with 185 additions and 1 deletion.
8 changes: 7 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.13.0@a0a9c27630bcf8301ee78cb06741d2907d8c9fef">
<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352">
<file src="src/Cache/Engine/FileEngine.php">
<TooManyTemplateParams>
<code>$iterator</code>
@@ -165,6 +165,12 @@
<code>is_array($_list)</code>
</RedundantCondition>
</file>
<file src="src/Validation/Validation.php">
<RedundantCondition>
<code><![CDATA[$check instanceof $enumClassName &&
$check instanceof BackedEnum]]></code>
</RedundantCondition>
</file>
<file src="src/View/Exception/MissingCellTemplateException.php">
<ImplementedReturnTypeMismatch>
<code><![CDATA[array{name: string, file: string, paths: array<string>}]]></code>
40 changes: 40 additions & 0 deletions src/Validation/Validation.php
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
*/
namespace Cake\Validation;

use BackedEnum;
use Cake\Chronos\ChronosDate;
use Cake\Core\Exception\CakeException;
use Cake\I18n\DateTime;
@@ -25,6 +26,8 @@
use InvalidArgumentException;
use NumberFormatter;
use Psr\Http\Message\UploadedFileInterface;
use ReflectionEnum;
use ReflectionException;
use RuntimeException;
use UnhandledMatchError;

@@ -781,6 +784,43 @@ public static function email(mixed $check, ?bool $deep = false, ?string $regex =
return false;
}

/**
* Checks that the value is a valid backed enum instance or value.
*
* @param mixed $check Value to check
* @param class-string<\BackedEnum> $enumClassName The valid backed enum class name
* @return bool Success
* @since 5.0.3
*/
public static function enum(mixed $check, string $enumClassName): bool
{
if (
$check instanceof $enumClassName &&
$check instanceof BackedEnum
) {
return true;
}

$backingType = null;
try {
$reflectionEnum = new ReflectionEnum($enumClassName);
$backingType = $reflectionEnum->getBackingType();
} catch (ReflectionException) {
}

if ($backingType === null) {
throw new InvalidArgumentException(
'The `$enumClassName` argument must be the classname of a valid backed enum.'
);
}

if (get_debug_type($check) !== (string)$backingType) {
return false;
}

return $enumClassName::tryFrom($check) !== null;
}

/**
* Checks that value is exactly $comparedTo.
*
42 changes: 42 additions & 0 deletions src/Validation/Validator.php
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@

use ArrayAccess;
use ArrayIterator;
use BackedEnum;
use Closure;
use Countable;
use InvalidArgumentException;
@@ -2033,6 +2034,47 @@ public function email(
]);
}

/**
* Add a backed enum validation rule to a field.
*
* @param string $field The field you want to apply the rule to.
* @param class-string<\BackedEnum> $enumClassName The valid backed enum class name.
* @param string|null $message The error message when the rule fails.
* @param \Closure|string|null $when Either 'create' or 'update' or a Closure that returns
* true when the validation rule should be applied.
* @return $this
* @see \Cake\Validation\Validation::enum()
* @since 5.0.3
*/
public function enum(
string $field,
string $enumClassName,
?string $message = null,
Closure|string|null $when = null
) {
if (!in_array(BackedEnum::class, (array)class_implements($enumClassName), true)) {
throw new InvalidArgumentException(
'The `$enumClassName` argument must be the classname of a valid backed enum.'
);
}

if ($message === null) {
$cases = array_map(fn ($case) => $case->value, $enumClassName::cases());
$caseOptions = implode('`, `', $cases);
if (!$this->_useI18n) {
$message = sprintf('The provided value must be one of `%s`', $caseOptions);
} else {
$message = __d('cake', 'The provided value must be one of `{0}`', $caseOptions);
}
}

$extra = array_filter(['on' => $when, 'message' => $message]);

return $this->add($field, 'enum', $extra + [
'rule' => ['enum', $enumClassName],
]);
}

/**
* Add an IP validation rule to a field.
*
36 changes: 36 additions & 0 deletions tests/TestCase/Validation/ValidationTest.php
Original file line number Diff line number Diff line change
@@ -30,6 +30,9 @@
use Laminas\Diactoros\UploadedFile;
use Locale;
use stdClass;
use TestApp\Model\Enum\ArticleStatus;
use TestApp\Model\Enum\NonBacked;
use TestApp\Model\Enum\Priority;

require_once __DIR__ . '/stubs.php';

@@ -2014,6 +2017,39 @@ public function testEmailCustomRegex(): void
$this->assertFalse(Validation::email('[email protected]', null, '/^[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$/i'));
}

public function testEnum(): void
{
$this->assertTrue(Validation::enum(ArticleStatus::PUBLISHED, ArticleStatus::class));
$this->assertTrue(Validation::enum('Y', ArticleStatus::class));

$this->assertTrue(Validation::enum(Priority::LOW, Priority::class));
$this->assertTrue(Validation::enum(1, Priority::class));

$this->assertFalse(Validation::enum(Priority::LOW, ArticleStatus::class));
$this->assertFalse(Validation::enum(1, ArticleStatus::class));
$this->assertFalse(Validation::enum('non-existent', ArticleStatus::class));

$this->assertFalse(Validation::enum(ArticleStatus::PUBLISHED, Priority::class));
$this->assertFalse(Validation::enum('wrong type', Priority::class));
$this->assertFalse(Validation::enum(123, Priority::class));
}

public function testEnumNonBacked(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The `$enumClassName` argument must be the classname of a valid backed enum.');

Validation::enum(NonBacked::Basic, NonBacked::class);
}

public function testEnumNonEnum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The `$enumClassName` argument must be the classname of a valid backed enum.');

Validation::enum('non-enum class', TestCase::class);
}

/**
* testEqualTo method
*/
40 changes: 40 additions & 0 deletions tests/TestCase/Validation/ValidatorTest.php
Original file line number Diff line number Diff line change
@@ -25,6 +25,9 @@
use InvalidArgumentException;
use Laminas\Diactoros\UploadedFile;
use stdClass;
use TestApp\Model\Enum\ArticleStatus;
use TestApp\Model\Enum\NonBacked;
use TestApp\Model\Enum\Priority;
use Traversable;

/**
@@ -2704,6 +2707,43 @@ public function testEmail(): void
$this->assertValidationMessage($fieldName, $rule, $expectedMessage, $checkMx);
}

public function testEnum(): void
{
$validator = new Validator();
$validator->enum('status', ArticleStatus::class);

$this->assertEmpty($validator->validate(['status' => ArticleStatus::PUBLISHED]));
$this->assertEmpty($validator->validate(['status' => 'Y']));

$this->assertNotEmpty($validator->validate(['status' => Priority::LOW]));
$this->assertNotEmpty($validator->validate(['status' => 'wrong type']));
$this->assertNotEmpty($validator->validate(['status' => 123]));
$this->assertNotEmpty($validator->validate(['status' => NonBacked::Basic]));

$fieldName = 'status';
$rule = 'enum';
$expectedMessage = 'The provided value must be one of `Y`, `N`';
$this->assertValidationMessage($fieldName, $rule, $expectedMessage, ArticleStatus::class);
}

public function testEnumNonBacked(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The `$enumClassName` argument must be the classname of a valid backed enum.');

$validator = new Validator();
$validator->enum('status', NonBacked::class);
}

public function testEnumNonEnum(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The `$enumClassName` argument must be the classname of a valid backed enum.');

$validator = new Validator();
$validator->enum('status', TestCase::class);
}

/**
* Tests the integer proxy method
*/
20 changes: 20 additions & 0 deletions tests/test_app/TestApp/Model/Enum/NonBacked.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 5.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace TestApp\Model\Enum;

enum NonBacked
{
case Basic;
}

0 comments on commit b8bc29b

Please sign in to comment.