Skip to content
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
15 changes: 15 additions & 0 deletions src/contracts/src/Validation/ParameterizedRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Hypervel\Contracts\Validation;

use Stringable;

interface ParameterizedRule extends Stringable
{
/**
* Get the rule name followed by its parameters.
*/
public function toArray(): array;
}
2 changes: 1 addition & 1 deletion src/contracts/src/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function failed(): array;
/**
* Add conditions to a given field based on a Closure.
*/
public function sometimes(array|string $attribute, array|string $rules, callable $callback): static;
public function sometimes(array|string $attribute, array|object|string $rules, callable $callback): static;

/**
* Add an after validation callback.
Expand Down
5 changes: 5 additions & 0 deletions src/data/src/Attributes/Validation/AcceptedIf.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ class AcceptedIf extends StringValidationAttribute
{
protected FieldReference $field;

protected array $values;

/**
* Create an accepted-if rule attribute.
*/
public function __construct(
string|FieldReference $field,
protected string|bool|int|float|BackedEnum|ExternalReference $value,
string|bool|int|float|BackedEnum|ExternalReference ...$values,
) {
$this->field = $this->parseFieldReference($field);
$this->values = $values;
}

/**
Expand All @@ -40,6 +44,7 @@ public function parameters(): array
return [
$this->field,
$this->value,
$this->values,
];
}

Expand Down
13 changes: 12 additions & 1 deletion src/data/src/Attributes/Validation/Confirmed.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@
namespace Hypervel\Data\Attributes\Validation;

use Attribute;
use Hypervel\Data\Support\Validation\References\FieldReference;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Confirmed extends StringValidationAttribute
{
protected ?FieldReference $field;

/**
* Create a confirmed rule attribute.
*/
public function __construct(string|FieldReference|null $field = null)
{
$this->field = $field === null ? null : $this->parseFieldReference($field);
}

/**
* Get the Validator rule keyword.
*/
Expand All @@ -22,6 +33,6 @@ public static function keyword(): string
*/
public function parameters(): array
{
return [];
return $this->field === null ? [] : [$this->field];
}
}
3 changes: 2 additions & 1 deletion src/data/src/Attributes/Validation/DateEquals.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
use Attribute;
use DateTimeInterface;
use Hypervel\Data\Support\Validation\References\ExternalReference;
use Hypervel\Data\Support\Validation\References\FieldReference;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class DateEquals extends StringValidationAttribute
{
/**
* Create a date-equals rule attribute.
*/
public function __construct(protected string|DateTimeInterface|ExternalReference $date)
public function __construct(protected string|DateTimeInterface|FieldReference|ExternalReference $date)
{
}

Expand Down
5 changes: 5 additions & 0 deletions src/data/src/Attributes/Validation/DeclinedIf.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ class DeclinedIf extends StringValidationAttribute
{
protected FieldReference $field;

protected array $values;

/**
* Create a declined-if rule attribute.
*/
public function __construct(
string|FieldReference $field,
protected string|bool|int|float|BackedEnum|ExternalReference $value,
string|bool|int|float|BackedEnum|ExternalReference ...$values,
) {
$this->field = $this->parseFieldReference($field);
$this->values = $values;
}

/**
Expand All @@ -40,6 +44,7 @@ public function parameters(): array
return [
$this->field,
$this->value,
$this->values,
];
}

Expand Down
5 changes: 5 additions & 0 deletions src/data/src/Attributes/Validation/ExcludeIf.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ class ExcludeIf extends StringValidationAttribute
{
protected FieldReference $field;

protected array $values;

/**
* Create a new exclude-if validation attribute.
*/
public function __construct(
string|FieldReference $field,
protected string|bool|int|float|BackedEnum|ExternalReference $value,
string|bool|int|float|BackedEnum|ExternalReference ...$values,
) {
$this->field = $this->parseFieldReference($field);
$this->values = $values;
}

/**
Expand All @@ -43,6 +47,7 @@ public function parameters(): array
return [
$this->field,
$this->value,
$this->values,
];
}
}
5 changes: 5 additions & 0 deletions src/data/src/Attributes/Validation/ExcludeUnless.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ class ExcludeUnless extends StringValidationAttribute
{
protected FieldReference $field;

protected array $values;

/**
* Create a new exclude-unless validation attribute.
*/
public function __construct(
string|FieldReference $field,
protected string|bool|int|float|BackedEnum|ExternalReference $value,
string|bool|int|float|BackedEnum|ExternalReference ...$values,
) {
$this->field = $this->parseFieldReference($field);
$this->values = $values;
}

/**
Expand All @@ -40,6 +44,7 @@ public function parameters(): array
return [
$this->field,
$this->value,
$this->values,
];
}
}
6 changes: 6 additions & 0 deletions src/data/src/Attributes/Validation/ExcludeWithout.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ class ExcludeWithout extends StringValidationAttribute
{
protected FieldReference $field;

/** @var array<array-key, FieldReference> */
protected array $fields;

/**
* Create a new exclude-without validation attribute.
*/
public function __construct(
string|FieldReference $field,
string|FieldReference ...$fields,
) {
$this->field = $this->parseFieldReference($field);
$this->fields = array_map($this->parseFieldReference(...), $fields);
}

/**
Expand All @@ -36,6 +41,7 @@ public function parameters(): array
{
return [
$this->field,
$this->fields,
];
}
}
25 changes: 15 additions & 10 deletions src/data/src/Support/Validation/DataValidationCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Closure;
use DateTimeInterface;
use Hypervel\Contracts\Container\Container;
use Hypervel\Contracts\Validation\ParameterizedRule;
use Hypervel\Data\Attributes\Validation\ArrayType;
use Hypervel\Data\Attributes\Validation\BooleanType;
use Hypervel\Data\Attributes\Validation\Enum;
Expand Down Expand Up @@ -1591,9 +1592,7 @@ protected function hasDistinctRule(array $rules): bool
return true;
}

if (is_string($rule)
&& strtolower(trim(explode(':', $rule, 2)[0])) === 'distinct'
) {
if ($this->ruleKeyword($rule) === 'distinct') {
return true;
}
}
Expand Down Expand Up @@ -1633,20 +1632,26 @@ protected function hasPresenceRule(array $rules): bool
return true;
}

if (! is_string($rule)) {
continue;
}

$name = strtolower(trim(explode(':', $rule, 2)[0]));

if (in_array($name, self::PRESENCE_RULES, true)) {
if (in_array($this->ruleKeyword($rule), self::PRESENCE_RULES, true)) {
return true;
}
}

return false;
}

/**
* Get the keyword of a string or structured validation rule.
*/
protected function ruleKeyword(mixed $rule): ?string
{
if ($rule instanceof ParameterizedRule) {
$rule = $rule->toArray()[0];
}

return is_string($rule) ? strtolower(trim(explode(':', $rule, 2)[0])) : null;
}

/**
* Determine if item rules can differ across one collection.
*
Expand Down
5 changes: 5 additions & 0 deletions src/data/src/Support/Validation/RuleDenormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ protected function normalizeRuleParameter(
return null;
}

if (is_float($parameter)) {
// String casts round finite floats, JSON cannot encode INF, and casting NAN warns on PHP 8.5.
return is_nan($parameter) ? 'NAN' : (json_encode($parameter) ?: (string) $parameter);
}

if (is_string($parameter) || is_numeric($parameter)) {
return (string) $parameter;
}
Expand Down
23 changes: 18 additions & 5 deletions src/data/src/Support/Validation/RuleNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Closure;
use Hypervel\Contracts\Validation\CompilableRules;
use Hypervel\Contracts\Validation\ParameterizedRule;
use Hypervel\Contracts\Validation\Rule as RuleContract;
use Hypervel\Contracts\Validation\ValidationRule as ValidationRuleContract;
use Hypervel\Data\Attributes\Validation\Dimensions;
Expand Down Expand Up @@ -64,6 +65,10 @@ public function execute(
return $this->resolveStringRule($rule);
}

if ($rule instanceof ParameterizedRule) {
return [$this->resolveRule($rule)];
}

if ($rule instanceof Rule) {
return $this->execute($rule->get());
}
Expand Down Expand Up @@ -122,13 +127,21 @@ protected function resolveStringRule(string $rule): array
? [$rule]
: explode('|', $rule);
foreach ($subRules as $subRule) {
try {
$rules[] = $this->ruleFactory->create($subRule);
} catch (CouldNotCreateValidationRule|CannotBuildValidationRule|TypeError|ValueError) {
$rules[] = new Rule($subRule);
}
$rules[] = $this->resolveRule($subRule);
}

return $rules;
}

/**
* Convert a rule to its validation attribute, keeping it as written when no attribute can hold its parameters.
*/
protected function resolveRule(string|ParameterizedRule $rule): ValidationRule
{
try {
return $this->ruleFactory->create($rule);
} catch (CouldNotCreateValidationRule|CannotBuildValidationRule|TypeError|ValueError) {
return new Rule($rule);
}
}
}
5 changes: 5 additions & 0 deletions src/data/src/Support/Validation/ValidationAccumulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Hypervel\Data\Support\Validation;

use Hypervel\Contracts\Validation\ParameterizedRule;
use Hypervel\Validation\Rules\Enum;
use Hypervel\Validation\Rules\Exists;
use Hypervel\Validation\Rules\Unique;
Expand Down Expand Up @@ -147,6 +148,10 @@ protected function ruleValueEquals(mixed $rule, mixed $otherRule): bool
&& $this->rulesEqual($rule, $otherRule);
}

if ($rule instanceof ParameterizedRule) {
return $otherRule instanceof ParameterizedRule && $rule->toArray() === $otherRule->toArray();
}

// Each item compiles its own inferred enum rule, so equal rules compare by their type and case restrictions.
if ($rule instanceof Enum) {
return $otherRule instanceof Enum && $rule == $otherRule;
Expand Down
7 changes: 4 additions & 3 deletions src/data/src/Support/Validation/ValidationRuleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Hypervel\Data\Support\Validation;

use Hypervel\Contracts\Validation\ParameterizedRule;
use Hypervel\Data\Attributes\Validation\Accepted;
use Hypervel\Data\Attributes\Validation\AcceptedIf;
use Hypervel\Data\Attributes\Validation\ActiveUrl;
Expand Down Expand Up @@ -252,18 +253,18 @@ class ValidationRuleFactory
];

/**
* Create the validation attribute for a Validator rule string.
* Create the validation attribute for a Validator rule.
*
* @throws CouldNotCreateValidationRule when no attribute has the rule's keyword
*/
public function create(string $rule): ValidationRule
public function create(string|ParameterizedRule $rule): ValidationRule
{
[$keyword, $parameters] = ValidationRuleParser::parse($rule);

$ruleClass = $this->mapping()[Str::snake($keyword)] ?? null;

if ($ruleClass === null) {
throw CouldNotCreateValidationRule::create($rule);
throw CouldNotCreateValidationRule::create(is_string($rule) ? $rule : $keyword);
}

return $ruleClass::create(...$parameters);
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,7 @@ public static function rules(ValidationContext $context): array

The `ValidationContext` provides the current object's input as `payload`, the complete input as `fullPayload`, and the object's input `path`. A class rule replaces inferred rules for that property. Add `#[MergeValidationRules]` to the class to merge them instead. Property keys use PHP property names; Hypervel translates them to the mapped input paths.

Validation attributes, including `#[Rule]`, may reference another property by its PHP name; Hypervel converts it to the input name. Rule strings returned from `rules()` or rule hooks are passed to the validator as written, so use input names there, or return an attribute such as `new RequiredWith('lastName')`.
Validation attributes, including `#[Rule]`, may reference another property by its PHP name; Hypervel converts it to the input name. Date rules treat a plain string as written, so use `new FieldReference('lastName')` to reference a property. Rule strings and rule builders returned from `rules()` or rule hooks are passed to the validator as written, so use input names there, or return an attribute such as `new RequiredWith('lastName')`.

For references from a nested object to the root input, use `FieldReference`:

Expand Down
Loading
Loading