Validator
in package
CRUDUI form validator — SPEC §2 G5→§3→§2 G1. Port of validator-ts/src/validate/validator.ts (Validator), function-for-function.
This is the THIRD pass of the CRUDUI pipeline. It consumes a CRUDUI field model (the
validate/design/behavior/options role slots) AFTER the compose pass has
expanded $ref/$patch into a single spec (Validate::run runs compose first).
It does NOT re-implement the rule semantics or the expression engine — it CALLS
the rule instances (Polyspec\Crudui\Validator\Rules) and the CRUDUI expression engine
(Polyspec\Crudui\Validator\Expr). The logic here is:
(a) reading the validate slot,
(b) evaluating a rule value that is an expression OR a condition map (G1 —
the condition is the value's expression, never a separate if/when key),
(c) skipping every field hidden by design.show, with everything it contains.
The rule instances return a bool and skip nothing on empty values, so this engine
applies the empty-value skip of the validation rules centrally and builds the error
message itself (DEFAULT_MESSAGES + per-field messages override). Declared rule
parameters, including every literal a condition can select, are checked when the
validator is constructed (DeclarationCheck); a value a ternary takes from the data
is checked when it is selected, before the empty-value skip. Both raise ComposeLoadError at the field's declaration
path.
Table of Contents
Constants
- DEFAULT_MESSAGES : mixed = ['required' => 'This field is required.', 'emai...
- Default error messages per rule (identical in every runtime). Every registered rule has one, so the keys are the registered rule names. The `pattern` and `match` aliases share one message. {0}/{1} are replaced by the parameters of the rules that have them.
- ARRAY_LEVEL_RULES : mixed = ['required', 'unique', 'mincount', 'maxcount']
- Rules that apply to the whole array of a `multiple` field (the rest apply to each element). VALIDATION-RULES §array-level.
- SINGLE_CHOICE_TYPES : mixed = ['select', 'dropdown', 'selectbox', 'choice', '...
- Field types whose control holds one value (validation-rules.md, "Evaluation").
Properties
- $duplicatesByField : array<string, array<string, bool>>
- For each collection field of the validation in progress (container path, field name and filter), the row keys whose `unique` value an earlier row already holds.
- $idsByPath : array<string, Membership>
- For each declaration path that `reference` names, the member set of its ids in the data of the validation in progress (docs/spec/validation-rules.md, "References").
- $properties : array<string, mixed>
- $rules : array<string, RuleInterface>
Methods
- __construct() : mixed
- validate() : ValidationResult
- Validate `data` against the composed CRUDUI spec.
- applyRules() : bool
- Run the rules in order until one fails, record that error under the path of the value and return whether one failed.
- areAllUnique() : bool
- Whether no two canonical keys are identical.
- assertSingleChoiceData() : void
- Reject an array or object as the value of a single-choice field, or of a row of a repeated one (rows in sorted key order).
- buildMessage() : string
- Build the display message: per-field override → DEFAULT_MESSAGES → fallback, with every {0}/{1} of a rule that has those parameters replaced by their canonical texts.
- canonicalKey() : string
- The canonical key of a value for `unique` (docs/spec/validation-rules.md): equal keys mean the same JSON value. Every value carries its type, strings are JSON-escaped, a number of either PHP type is written by its exact value as ECMAScript writes it, lists keep their order, and object members, of a stdClass or an associative array, are sorted by name at every depth.
- childProperties() : array<string, mixed>|null
- The child field map of a group (composed, plain object), or null.
- collectHidden() : void
- Append the data paths of the fields whose design.show resolves to false, in declaration order, each field of a group row under the row's key and the fields inside a hidden field included (validation-rules.md, "Evaluation"). Data of another shape is read as missing.
- fieldMessages() : array<string, string>|null
- Read the per-field custom messages (the `messages` map).
- fieldsOf() : Generator<string, array<string, mixed>>
- The field declarations of a properties map in declaration order, each as an array under its name; a member that is not an object is not a field.
- getFieldName() : string
- getValueByPath() : mixed
- Read the value at a concrete path (JS getValueByPath). Missing → null.
- isConditionExpression() : bool
- Whether a string looks like a condition expression (JS isConditionExpression): starts with `.`, `identifier.`, contains a comparison/logical/in operator, or contains a ternary `?...:`.
- isSingleChoice() : bool
- Whether a field is a single-choice field: a single-choice type without `lang`.
- itemPassesCondition() : bool
- Evaluate a filter condition for an item, as if validating the same field on that item (JS itemPassesCondition).
- makeError() : array<string, mixed>
- Assemble an error record matching the shared fixture shape.
- normalizeValidateSlot() : array<string, mixed>|null
- Normalize a polymorphic `validate` slot to a rule map. false/true/null → no rules; a list/empty array → no rules; an assoc array → the rule map itself.
- pathKey() : string
- A per-field key so the first error per field wins (later errors on the same path are ignored). The dot-path is unique per field instance.
- pathToString() : string
- registerRules() : void
- Register the rule instances; `pattern` and `match` share one implementation.
- resolveRuleValue() : mixed
- Resolve a rule value to the effective param (G1).
- rulesAt() : array<string, mixed>
- The rules that judge the collection of a `multiple` field as a whole (`$arrayLevel`), or the rules that judge each element, in declaration order.
- runGroupSource() : bool
- A field whose `items` is a group source runs an implicit `in` check against the choices that the source gives for the whole form data, after the implicit `number` check and before the declared rules (validation-rules.md, "Values"). Returns true when it pushed an error (caller stops the field).
- runImplicitNumber() : bool
- `type:number` runs an implicit `number` rule before everything else when no explicit `number` rule is declared (VALIDATION-RULES §2 — reported as rule `number`). Returns true when it pushed an error (caller stops the field).
- runRule() : string|null
- Run one rule: evaluate its (possibly conditional) value to an effective param, check a selected param, apply the central empty-skip, then call the rule instance and build the message. Returns the error message or null.
- stringify() : string
- Placeholder text: the canonical text of a scalar; false, null, arrays and objects are empty.
- validateFieldRules() : void
- All rules for a single (scalar or group-as-whole) field, or, with `$elementLevel`, the element-level rules of one element of a `multiple` field (no array-level rule).
- validateMultipleFieldRules() : void
- Collection rules and per-row rules for a repeated scalar field.
- validateProperties() : void
- validateUnique() : bool
- Unique rule (two modes). A string param that is a condition expression acts as a per-item filter (only items passing the condition participate); a non-condition string param is a field name within array items.
Constants
DEFAULT_MESSAGES
Default error messages per rule (identical in every runtime). Every registered rule has one, so the keys are the registered rule names. The `pattern` and `match` aliases share one message. {0}/{1} are replaced by the parameters of the rules that have them.
public
mixed
DEFAULT_MESSAGES
= ['required' => 'This field is required.', 'email' => 'Please enter a valid email address.', 'minlength' => 'Please enter at least {0} characters.', 'maxlength' => 'Please enter no more than {0} characters.', 'min' => 'Please enter a value greater than or equal to {0}.', 'max' => 'Please enter a value less than or equal to {0}.', 'match' => 'Please enter a valid format.', 'pattern' => 'Please enter a valid format.', 'unique' => 'Values must be unique.', 'in' => 'Please select a valid option.', 'range' => 'Please enter a value between {0} and {1}.', 'rangelength' => 'Please enter a value between {0} and {1} characters.', 'number' => 'Please enter a valid number.', 'digits' => 'Please enter only digits.', 'equalTo' => 'Please enter the same value again.', 'containedIn' => 'Please enter a value that the referenced field contains.', 'typeFrom' => 'Please enter a value of the declared type.', 'minFrom' => 'Please enter a value at or above the declared minimum.', 'maxFrom' => 'Please enter a value at or below the declared maximum.', 'notEqual' => 'Please enter a different value.', 'date' => 'Please enter a valid date.', 'dateISO' => 'Please enter a valid date in ISO format (YYYY-MM-DD).', 'enddate' => 'End date must be after the start date.', 'url' => 'Please enter a valid URL.', 'accept' => 'Please upload a file with a valid format.', 'mincount' => 'Please select at least {0} items.', 'maxcount' => 'Please select no more than {0} items.', 'step' => 'Please enter a value that is a multiple of {0}.', 'conditionTree' => 'Please enter a valid condition.', 'reference' => 'Please select an existing value.']
ARRAY_LEVEL_RULES
Rules that apply to the whole array of a `multiple` field (the rest apply to each element). VALIDATION-RULES §array-level.
private
mixed
ARRAY_LEVEL_RULES
= ['required', 'unique', 'mincount', 'maxcount']
SINGLE_CHOICE_TYPES
Field types whose control holds one value (validation-rules.md, "Evaluation").
private
mixed
SINGLE_CHOICE_TYPES
= ['select', 'dropdown', 'selectbox', 'choice', 'radio']
Properties
$duplicatesByField
For each collection field of the validation in progress (container path, field name and filter), the row keys whose `unique` value an earlier row already holds.
private
array<string, array<string, bool>>
$duplicatesByField
= []
$idsByPath
For each declaration path that `reference` names, the member set of its ids in the data of the validation in progress (docs/spec/validation-rules.md, "References").
private
array<string, Membership>
$idsByPath
= []
$properties read-only
private
array<string, mixed>
$properties
the composed root field map (composition-free)
$rules
private
array<string, RuleInterface>
$rules
rule instances by rule name
Methods
__construct()
public
__construct(array<string, mixed> $composedSpec) : mixed
Parameters
- $composedSpec : array<string, mixed>
-
a composed CRUDUI root spec — a group with
properties(already free of $ref/$patch and forbidden keys).
Tags
validate()
Validate `data` against the composed CRUDUI spec.
public
validate(array<string, mixed> $data) : ValidationResult
Parameters
- $data : array<string, mixed>
Return values
ValidationResultapplyRules()
Run the rules in order until one fails, record that error under the path of the value and return whether one failed.
private
applyRules(array<string, mixed> $rules, mixed $value, array<int, string> $path, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, string>|null $messages, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors) : bool
Parameters
- $rules : array<string, mixed>
-
the rules to run, in declaration order, each with its declared value
- $value : mixed
- $path : array<int, string>
-
the path of the value that the rules check and that keys the error
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
-
the path of the declaration in the specification, for the load errors
- $messages : array<string, string>|null
-
the custom messages of the field
- $allData : array<string, mixed>
-
the whole form data, which the conditions of a rule read
- $errors : array<int, array<string, mixed>>
-
the errors found so far, keyed by path; receives the first error
Return values
boolareAllUnique()
Whether no two canonical keys are identical.
private
areAllUnique(array<int, mixed> $values) : bool
Parameters
- $values : array<int, mixed>
Return values
boolassertSingleChoiceData()
Reject an array or object as the value of a single-choice field, or of a row of a repeated one (rows in sorted key order).
private
static assertSingleChoiceData(mixed $value, array<int, string> $path, bool $repeated) : void
Parameters
- $value : mixed
- $path : array<int, string>
- $repeated : bool
Tags
buildMessage()
Build the display message: per-field override → DEFAULT_MESSAGES → fallback, with every {0}/{1} of a rule that has those parameters replaced by their canonical texts.
private
buildMessage(string $ruleName, mixed $param, array<string, mixed>|null $messages) : string
Parameters
- $ruleName : string
- $param : mixed
- $messages : array<string, mixed>|null
Return values
stringcanonicalKey()
The canonical key of a value for `unique` (docs/spec/validation-rules.md): equal keys mean the same JSON value. Every value carries its type, strings are JSON-escaped, a number of either PHP type is written by its exact value as ECMAScript writes it, lists keep their order, and object members, of a stdClass or an associative array, are sorted by name at every depth.
private
canonicalKey(mixed $value) : string
Parameters
- $value : mixed
Return values
stringchildProperties()
The child field map of a group (composed, plain object), or null.
private
childProperties(array<string|int, mixed> $field) : array<string, mixed>|null
Parameters
- $field : array<string|int, mixed>
Return values
array<string, mixed>|nullcollectHidden()
Append the data paths of the fields whose design.show resolves to false, in declaration order, each field of a group row under the row's key and the fields inside a hidden field included (validation-rules.md, "Evaluation"). Data of another shape is read as missing.
private
collectHidden(array<string, mixed> $properties, array<string|int, mixed> $data, array<int, string> $currentPath, array<int, int> $rowKeys, array<string, mixed> $allData, array<int, string> &$hidden) : void
Parameters
- $properties : array<string, mixed>
- $data : array<string|int, mixed>
- $currentPath : array<int, string>
- $rowKeys : array<int, int>
-
positions of the row keys in $currentPath
- $allData : array<string, mixed>
- $hidden : array<int, string>
fieldMessages()
Read the per-field custom messages (the `messages` map).
private
fieldMessages(array<string|int, mixed> $field) : array<string, string>|null
Parameters
- $field : array<string|int, mixed>
Return values
array<string, string>|nullfieldsOf()
The field declarations of a properties map in declaration order, each as an array under its name; a member that is not an object is not a field.
private
fieldsOf(array<string|int, mixed> $properties) : Generator<string, array<string, mixed>>
Parameters
- $properties : array<string|int, mixed>
Return values
Generator<string, array<string, mixed>>getFieldName()
private
getFieldName(array<int, string> $path) : string
Parameters
- $path : array<int, string>
Return values
stringgetValueByPath()
Read the value at a concrete path (JS getValueByPath). Missing → null.
private
getValueByPath(array<string, mixed> $data, array<int, string> $path) : mixed
Parameters
- $data : array<string, mixed>
- $path : array<int, string>
isConditionExpression()
Whether a string looks like a condition expression (JS isConditionExpression): starts with `.`, `identifier.`, contains a comparison/logical/in operator, or contains a ternary `?...:`.
private
isConditionExpression(string $value) : bool
Parameters
- $value : string
Return values
boolisSingleChoice()
Whether a field is a single-choice field: a single-choice type without `lang`.
private
static isSingleChoice(array<string|int, mixed> $field) : bool
Parameters
- $field : array<string|int, mixed>
Return values
boolitemPassesCondition()
Evaluate a filter condition for an item, as if validating the same field on that item (JS itemPassesCondition).
private
itemPassesCondition(string $condition, array<int, string> $itemFieldPath, array<string|int, mixed> $rowKeys, array<string, mixed> $allData) : bool
Parameters
- $condition : string
- $itemFieldPath : array<int, string>
- $rowKeys : array<string|int, mixed>
- $allData : array<string, mixed>
Return values
boolmakeError()
Assemble an error record matching the shared fixture shape.
private
makeError(array<int, string> $path, string $rule, string $message, mixed $value) : array<string, mixed>
Parameters
- $path : array<int, string>
- $rule : string
- $message : string
- $value : mixed
Return values
array<string, mixed>normalizeValidateSlot()
Normalize a polymorphic `validate` slot to a rule map. false/true/null → no rules; a list/empty array → no rules; an assoc array → the rule map itself.
private
normalizeValidateSlot(mixed $slot) : array<string, mixed>|null
Parameters
- $slot : mixed
Return values
array<string, mixed>|nullpathKey()
A per-field key so the first error per field wins (later errors on the same path are ignored). The dot-path is unique per field instance.
private
pathKey(array<int, string> $path) : string
Parameters
- $path : array<int, string>
Return values
stringpathToString()
private
pathToString(array<int, string> $path) : string
Parameters
- $path : array<int, string>
Return values
stringregisterRules()
Register the rule instances; `pattern` and `match` share one implementation.
private
registerRules() : void
resolveRuleValue()
Resolve a rule value to the effective param (G1).
private
resolveRuleValue(string $ruleName, mixed $ruleValue, array<int, string> $path, array<string|int, mixed> $rowKeys, array<string, mixed> $allData) : mixed
Path-reference, literal-param, regex and membership rules keep their parameter verbatim; any other rule value resolves as a conditional parameter (ConditionalValue).
Parameters
- $ruleName : string
- $ruleValue : mixed
- $path : array<int, string>
- $rowKeys : array<string|int, mixed>
- $allData : array<string, mixed>
rulesAt()
The rules that judge the collection of a `multiple` field as a whole (`$arrayLevel`), or the rules that judge each element, in declaration order.
private
static rulesAt(array<string, mixed> $rules, bool $arrayLevel) : array<string, mixed>
Parameters
- $rules : array<string, mixed>
- $arrayLevel : bool
Return values
array<string, mixed>runGroupSource()
A field whose `items` is a group source runs an implicit `in` check against the choices that the source gives for the whole form data, after the implicit `number` check and before the declared rules (validation-rules.md, "Values"). Returns true when it pushed an error (caller stops the field).
private
runGroupSource(array<string, mixed> $field, mixed $value, array<int, string> $path, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, string>|null $messages, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors) : bool
Parameters
- $field : array<string, mixed>
-
the field declaration
- $value : mixed
- $path : array<int, string>
-
the path of the value that is checked
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
-
the path of the declaration of the field in the specification
- $messages : array<string, string>|null
-
the custom messages of the field
- $allData : array<string, mixed>
-
the whole form data, which the choices are read from
- $errors : array<int, array<string, mixed>>
-
the errors found so far, keyed by path; receives the new one
Return values
boolrunImplicitNumber()
`type:number` runs an implicit `number` rule before everything else when no explicit `number` rule is declared (VALIDATION-RULES §2 — reported as rule `number`). Returns true when it pushed an error (caller stops the field).
private
runImplicitNumber(array<string|int, mixed> $field, array<string, mixed>|null $rules, mixed $value, array<int, string> $path, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, string>|null $messages, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors) : bool
Parameters
- $field : array<string|int, mixed>
- $rules : array<string, mixed>|null
-
the validate slot of the field, which says whether
numberis declared - $value : mixed
- $path : array<int, string>
-
the path of the value that is checked
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
-
the path of the declaration of the field in the specification
- $messages : array<string, string>|null
-
the custom messages of the field
- $allData : array<string, mixed>
-
the whole form data, which the conditions of the rule read
- $errors : array<int, array<string, mixed>>
-
the errors found so far, keyed by path; receives the new one
Return values
boolrunRule()
Run one rule: evaluate its (possibly conditional) value to an effective param, check a selected param, apply the central empty-skip, then call the rule instance and build the message. Returns the error message or null.
private
runRule(string $ruleName, mixed $ruleValue, mixed $value, array<int, string> $path, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, string>|null $messages, array<string, mixed> $allData) : string|null
Parameters
- $ruleName : string
- $ruleValue : mixed
- $value : mixed
- $path : array<int, string>
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
- $messages : array<string, string>|null
- $allData : array<string, mixed>
Tags
Return values
string|nullstringify()
Placeholder text: the canonical text of a scalar; false, null, arrays and objects are empty.
private
stringify(mixed $value) : string
Parameters
- $value : mixed
Return values
stringvalidateFieldRules()
All rules for a single (scalar or group-as-whole) field, or, with `$elementLevel`, the element-level rules of one element of a `multiple` field (no array-level rule).
private
validateFieldRules(array<string|int, mixed> $field, mixed $value, array<int, string> $fieldPath, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors[, bool $elementLevel = false ]) : void
Parameters
- $field : array<string|int, mixed>
- $value : mixed
- $fieldPath : array<int, string>
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
- $allData : array<string, mixed>
- $errors : array<int, array<string, mixed>>
- $elementLevel : bool = false
validateMultipleFieldRules()
Collection rules and per-row rules for a repeated scalar field.
private
validateMultipleFieldRules(array<string|int, mixed> $field, array<string, mixed>|stdClass $values, array<int, string> $fieldPath, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors) : void
Parameters
- $field : array<string|int, mixed>
- $values : array<string, mixed>|stdClass
-
keyed rows
- $fieldPath : array<int, string>
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
- $allData : array<string, mixed>
- $errors : array<int, array<string, mixed>>
validateProperties()
private
validateProperties(array<string, mixed> $properties, array<string, mixed> $data, array<int, string> $currentPath, array<string|int, mixed> $rowKeys, array<int, string> $declarationPath, array<string, mixed> $allData, array<int, array<string, mixed>> &$errors[, bool $hidden = false ]) : void
Parameters
- $properties : array<string, mixed>
- $data : array<string, mixed>
- $currentPath : array<int, string>
- $rowKeys : array<string|int, mixed>
- $declarationPath : array<int, string>
-
$currentPath without row keys
- $allData : array<string, mixed>
- $errors : array<int, array<string, mixed>>
- $hidden : bool = false
-
whether an enclosing field is hidden: its descendants' data shape is still checked, their rules are not
validateUnique()
Unique rule (two modes). A string param that is a condition expression acts as a per-item filter (only items passing the condition participate); a non-condition string param is a field name within array items.
private
validateUnique(mixed $value, mixed $ruleParam, array<int, string> $pathSegments, array<string|int, mixed> $rowKeys, array<string, mixed> $allData) : bool
Parameters
- $value : mixed
- $ruleParam : mixed
- $pathSegments : array<int, string>
- $rowKeys : array<string|int, mixed>
- $allData : array<string, mixed>