Skip to content

Feature: add optional and nullable rules #5

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

Merged
merged 7 commits into from
Feb 7, 2023
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
14 changes: 14 additions & 0 deletions src/Commands/SkipValidationRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace StellarWP\Validation\Commands;

/**
* Returning this command from ValidationRule::__invoke() tells the Validator to skip all subsequent rules.
*
* @unreleased
*/
class SkipValidationRules
{
}
3 changes: 2 additions & 1 deletion src/Contracts/ValidationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace StellarWP\Validation\Contracts;

use Closure;
use StellarWP\Validation\Commands\SkipValidationRules;

interface ValidationRule
{
Expand All @@ -31,7 +32,7 @@ public static function fromString(string $options = null): ValidationRule;
*
* @since 1.0.0
*
* @return void
* @return void|SkipValidationRules
*/
public function __invoke($value, Closure $fail, string $key, array $values);
}
55 changes: 55 additions & 0 deletions src/Rules/Nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace StellarWP\Validation\Rules;

use Closure;
use StellarWP\Validation\Commands\SkipValidationRules;
use StellarWP\Validation\Contracts\ValidatesOnFrontEnd;
use StellarWP\Validation\Contracts\ValidationRule;

/**
* This rule skips further validation if the field is null. It is similar to Optional, but the only allowed value is
* null.
*
* @unreleased
*/
class Nullable implements ValidationRule, ValidatesOnFrontEnd
{
/**
* @unreleased
*/
public static function id(): string
{
return 'nullable';
}

/**
* @unreleased
*/
public static function fromString(string $options = null): ValidationRule
{
return new self();
}

/**
* @unreleased
*
* @return SkipValidationRules|void
*/
public function __invoke($value, Closure $fail, string $key, array $values)
{
if ($value === null) {
return new SkipValidationRules();
}
}

/**
* @unreleased
*/
public function serializeOption()
{
return null;
}
}
54 changes: 54 additions & 0 deletions src/Rules/Optional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace StellarWP\Validation\Rules;

use Closure;
use StellarWP\Validation\Commands\SkipValidationRules;
use StellarWP\Validation\Contracts\ValidatesOnFrontEnd;
use StellarWP\Validation\Contracts\ValidationRule;

/**
* This rule marks a field as optional and skips further validation if the rule is either null or an empty string.
*
* @unreleased
*/
class Optional implements ValidationRule, ValidatesOnFrontEnd
{
/**
* @unreleased
*/
public static function id(): string
{
return 'optional';
}

/**
* @unreleased
*/
public static function fromString(string $options = null): ValidationRule
{
return new self();
}

/**
* @unreleased
*
* @return SkipValidationRules|void
*/
public function __invoke($value, Closure $fail, string $key, array $values)
{
if ($value === null || $value === '') {
return new SkipValidationRules();
}
}

/**
* @unreleased
*/
public function serializeOption()
{
return null;
}
}
4 changes: 4 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use StellarWP\Validation\Rules\Integer;
use StellarWP\Validation\Rules\Max;
use StellarWP\Validation\Rules\Min;
use StellarWP\Validation\Rules\Nullable;
use StellarWP\Validation\Rules\Numeric;
use StellarWP\Validation\Rules\Optional;
use StellarWP\Validation\Rules\Required;
use StellarWP\Validation\Rules\Size;

Expand All @@ -24,6 +26,8 @@ class ServiceProvider
Integer::class,
Email::class,
Currency::class,
Nullable::class,
Optional::class,
];

/**
Expand Down
27 changes: 6 additions & 21 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace StellarWP\Validation;

use StellarWP\Validation\Commands\SkipValidationRules;
use StellarWP\Validation\Contracts\Sanitizer;

/**
Expand Down Expand Up @@ -51,8 +52,6 @@ class Validator
*/
public function __construct(array $ruleSets, array $values, array $labels = [])
{
$this->validateRulesAndValues($ruleSets, $values);

$validatedRules = [];
foreach ($ruleSets as $key => $rule) {
if (is_array($rule)) {
Expand All @@ -71,24 +70,6 @@ public function __construct(array $ruleSets, array $values, array $labels = [])
$this->labels = $labels;
}

/**
* Validates that all rules have a corresponding value with the same key.
*
* @since 1.0.0
*
* @return void
*/
private function validateRulesAndValues(array $rules, array $values)
{
$missingKeys = array_diff_key($rules, $values);

if (!empty($missingKeys)) {
Config::throwInvalidArgumentException(
"Missing values for rules: " . implode(', ', array_keys($missingKeys))
);
}
}

/**
* Returns whether the values failed validation or not.
*
Expand Down Expand Up @@ -134,7 +115,11 @@ private function runValidationRules()
};

foreach ($ruleSet as $rule) {
$rule($value, $fail, $key, $this->values);
$command = $rule($value, $fail, $key, $this->values);

if ($command instanceof SkipValidationRules) {
break;
}

if ($rule instanceof Sanitizer) {
$value = $rule->sanitize($value);
Expand Down
30 changes: 30 additions & 0 deletions tests/_support/Helper/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,36 @@ public static function assertValidationRuleFailed(
self::assertValidationRulePassed($rule, $value, $key, $values, false);
}

public static function assertValidationRuleDoesReturnCommandInstance(
ValidationRule $rule,
string $commandClass,
$value = null,
string $key = '',
array $values = []
) {
$fail = static function () {
};

$command = $rule($value, $fail, $key, $values);

self::assertInstanceOf($commandClass, $command);
}

public static function assertValidationRuleDoesNotReturnCommandInstance(
ValidationRule $rule,
string $commandClass,
$value = null,
string $key = '',
array $values = []
) {
$fail = static function () {
};

$command = $rule($value, $fail, $key, $values);

self::assertNotInstanceOf($commandClass, $command);
}

public static function assertIsIterable($actual, $message = '')
{
if (\function_exists('is_iterable') === true) {
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/Rules/NullableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace StellarWP\Validation\Tests\Unit\Rules;

use StellarWP\Validation\Commands\SkipValidationRules;
use StellarWP\Validation\Rules\Nullable;
use StellarWP\Validation\Tests\TestCase;

class NullableTest extends TestCase
{
/**
* @unreleased
*/
public function testNullableValidation()
{
$rule = new Nullable();

// Passes when value is null and skips remaining tests
self::assertValidationRulePassed($rule, null);
self::assertValidationRuleDoesReturnCommandInstance($rule, SkipValidationRules::class, null);

// Passes on any other value but does not skip remaining tests
self::assertValidationRulePassed($rule, 'bar');
self::assertValidationRuleDoesNotReturnCommandInstance($rule, SkipValidationRules::class, 'bar');
}
}
32 changes: 32 additions & 0 deletions tests/unit/Rules/OptionalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace StellarWP\Validation\Tests\Unit\Rules;

use StellarWP\Validation\Commands\SkipValidationRules;
use StellarWP\Validation\Rules\Optional;
use StellarWP\Validation\Tests\TestCase;

class OptionalTest extends TestCase
{
/**
* @unreleased
*/
public function testNullableValidation()
{
$rule = new Optional();

// Passes when value is null and skips remaining tests
self::assertValidationRulePassed($rule, null);
self::assertValidationRuleDoesReturnCommandInstance($rule, SkipValidationRules::class, null);

// Passes when value is empty string and skips remaining tests
self::assertValidationRulePassed($rule, '');
self::assertValidationRuleDoesReturnCommandInstance($rule, SkipValidationRules::class, '');

// Passes on any other value but does not skip remaining tests
self::assertValidationRulePassed($rule, 'bar');
self::assertValidationRuleDoesNotReturnCommandInstance($rule, SkipValidationRules::class, 'bar');
}
}
Loading