Open mhsdesign opened 1 year ago
The current state of property validation (Neos 8.3)
Neos <= 9.0 offers property validation via validators configured in properties.[$property].validation
.
Those are sorely handled by the Neos Ui. Initially only their javascript implementation was executed inside the Neos Ui javascript Application.
But with https://github.com/neos/neos-ui/pull/2351 validators were also run on server side when properties were changed in the Neos Ui. This feature - as mentioned here - only works for native Neos.Neos
validators. Custom validators will be skipped (see code) and also if no Flow validator can be found it will also be skipped.
Custom validators can only be introduced via a javascript plugin for the Neos Ui. Implementing them as php validator will have no effect. (Though it might be interesting to implement ajax handling validator that will run an arbitrary php validator)
A node type with validation would look like:
'Neos.TestNodeTypes:Content.Headline':
properties:
title:
type: string
# otherwise the node would be inconsistent as there is no node creation dialog configured
defaultValue: '<h1>Enter headline here</h1>'
validation:
'My.Custom/NeosUi/Javascript/Validator': {}
'Neos.Neos/Validation/NotEmptyValidator': {}
'Neos.Neos/Validation/StringLengthValidator':
minimum: 1
maximum: 255
Where Neos.Neos/Validation/StringLengthValidator
is both a javascript validator provided by the neos ui as well as a flow server validator see Neos\Flow\Validation\Validator\StringLengthValidator
.
Problems arise if the validator specifications differ and javascript allows a value where php doesnt.
The custom validator My.Custom/NeosUi/Javascript/Validator
is not run on the server because it doesnt start with Neos.Neos
.
The content repository package doesnt handle the validators at all. So its possible to set any value to the property regardless of validation. Infact previously to neos 9, the content repository doesnt validate if the node property exists in the schema and if the assigned type matches.
Here's a suggestion that came up in todays Neos 9.0 Weekly:
TL;DR:
Step 1: Adjust the node type schema, deprecate (and eventually remove) validation
and add constraints
:
'Some.Node:Type':
properties:
'someProp':
type: string
required: true
constraints:
minLength: 3
maxLength: 20
'someOtherProp':
type: array
constraints:
minItems: 3
maxItems: 5
those constraints can be evaluated on the client and on the server side.
Step 2: Provide some API for custom validations that will be triggered via AJAX
I'm convinced, that for the mast majority of cases a couple of constraints would be enough (inspired from JSON Schema and graphql constraints directive):
minLength: int
maxLength: int
pattern: string
(regex)format: enum (email,ipv4,ipv6,uri,uuid,...)
(see JSON Schema)minimum: int
maximum: int
minItems: int
maxItems: int
uniqueItems: bool
It should be relatively simple (tm) to map todays validation
configuration to the above vice versa.
For custom validation that can't be implemented with the constraints
mentioned above, we should not try to implement that on the server- and client-side (as we do today) but provide some API that can be used from the client-side via AJAX and on the server side at command handling time.
I could imagine a new interface like
interface PropertyValidationBla {
public function validate(mixed $input, ValidationContext $context): ValidationResult
}
that could be registered via Settings for a specific type
Note: The ValidationResult
could contain more details why a validation failed
Note: I added the ValidationContext
as parameter, because we might need to add "parametrized" validation. For example "image of certain extension, filesize or dimensions" – for this we could consider some unstructured validationOptions
in the node type schema
Note: In addition, we could execute the validation if the type itself implements that interface
final readonly class PhoneNumber {
private function __construct(public string $value) {
if (!self::isValid($value)) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid phone number', $value));
}
}
public static function fromString(string $value): self {
return new self($value);
}
public static function isValid(string $value): bool
{
// todo implement
}
}
final class PhoneNumberValidator implements PropertyValidator {
public function validate(mixed $input, ValidationContext $context): ValidationResult {
if (PhoneNumber::isValid($input)) {
return ValidationResult::succeeded();
}
return ValidationResult::failed('Not a valid phone number');
}
}
Note: The PhoneNumberValidator
would have to be registered as custom validator for the type on the CR in this case. Alternatively the PhoneNumber
VO could implement the interface itself
I discussed this matter again with @nezaniel and @grebaldi. At first server side validation with the goal of producing well guiding error messages seemed a little misplaced as its usually seen in javascript. But the idea is that the VO itself stays clear of the logic to produce a good error message and only does the constraint check.
We also had the idea to not trigger the validation via the registered PropertyValidator
, but structure the validation endpoint differently:
ProgressiveErrorEnhancer
which will take care of giving the user a nice error message.
For example it will tell the user what it takes to put the input into the correct format (eg which properties are missing or of invalid type).Not a valid ${type}
will be shown.This contradicts with your proposal for the validationOptions
, which seems more like the legacy validation
option which we try to banish.
Suggestion for a generic interface for the server-side part:
<?php
// In Cr Core:
final readonly class ConstraintValidationResult {
private function __construct(
public bool $succeeded,
public ?string $errorMessage,
) {}
public static function succeed(): self
{
return new self(true, null);
}
public static function fail(string $errorMessage, ...$args): self
{
return new self(false, vsprintf($errorMessage, ...$args));
}
}
interface ConstraintFoo {
public static function fromOptions(array $options): self;
public function validate(mixed $value): ConstraintValidationResult;
}
enum StringBasedFormat {
case email;
case ipv4;
// ...
}
final readonly class StringBasedFoo implements ConstraintFoo {
private function __construct(
public ?int $minLength,
public ?int $maxLength,
public ?string $pattern,
public ?StringBasedFormat $format,
) {}
public static function fromOptions(array $options): self
{
// TODO verify $options
return new self(
minLength: $options['minLength'] ?? null,
maxLength: $options['maxLength'] ?? null,
pattern: $options['pattern'] ?? null,
format: $options['format'] ?? null,
);
}
public function validate(mixed $value): ConstraintValidationResult
{
if (!is_string($value) && !$value instanceof \Stringable) {
return ConstraintValidationResult::fail('%s is not a string', get_debug_type($value));
}
if ($this->minLength !== null && strlen($value) < $this->minLength) {
return ConstraintValidationResult::fail('%s does not satisfy min length constraint of', $this->minLength);
}
// todo more checks
return ConstraintValidationResult::succeed();
}
}
// in Neos.Neos
final readonly class AssetConstraintFoo implements ConstraintFoo {
private function __construct(
public string $mediaTypePattern, // e.g. "image/*"
) {}
public static function fromOptions(array $options): self
{
// TODO verify $options
return new self(
mediaTypePattern: $options['mediaTypePattern'] ?? null,
);
}
public function validate(mixed $value): ConstraintValidationResult
{
if (!$value instanceof AssetInterface) {
return ConstraintValidationResult::fail('%s is not an instance of %s', get_debug_type($value), AssetInterface::class);
}
// todo more checks
return ConstraintValidationResult::succeed();
}
}
I think we can agree on this basic set on constraints for primitive types. Anything further can be implemented at a later point.
Right now i stumbled upon the fact that the javascript validators might never be async. Me and @Sebobo are thinking about fixing this and this seems to play well together with our neos 9 idea.
As discussed with @kitsunet and @pKallert in todays weekly:
NodePropertyConverterService
correctly values HAVE to match. Though the null case is always permitted and send from the uirequired: true
will make it set in stone (as its false
by default for b/c) that one can create a node without any properties though having some declaredint
string
or boolean
the integration part in fusion might not handle this well. And if we ever want to get away from having to many null values in fusion around we should not start increasing themdefaultValue: false
configuration in Neos.Neos.dataTypes that is respected in 8.3 and in 9.0 only by the Neos Ui, but not by the content repository core
defaultValue: null
should die probablyDefaultPropertyEditorPostprocessor
lost its ability to use the defaultValue
from the datatype (readd?)null
is this smart? https://github.com/neos/neos-ui/blob/28fb9c5e8e92a88fe8d79666b454b19c5533cff3/Classes/Domain/Service/NodePropertyConverterService.php#L151
related: https://github.com/neos/neos-development-collection/issues/968
i talked with @bwaidelich about this and the general ideas were:
to use the following syntax:
the behavior would be, that you cannot set the property to null and the property must either be defined at creation time (via creation dialog) or must have an default value.
we thought at first to make
required: true
an alias to the not empty validator, but introducing flow's validators into the new cr is not what were are looking forward to. Validators are neither in 8.3 nor 9.0 run on the php side but only triggered by the neos ui, if they have a js-validator implementation.A better approach to validators seem to be value objects like
type: Neos\Neos\Types\Email
which validate the constrains in the constructor. Those value objects could be created via ajax (maybe via an own requestHandler to not boot flow? and use plain php?) and the ui could then determine in near real time if the field is valid.Even better would be to actually trigger the cr logic directly to also validate type conflicts and also handle
required
on the server.@bwaidelich added:
IMO we should do both. The UI could interpret that
required
flag and do that "not empty" logic that it does today already. And for other (non-simple) types it could use that AJAX validation, MH described above. In any case,required
flag and type conversion should be tested at command handling time, before theNodePropertiesWereSet
event is createdNote: We also briefly discussed support for special syntax
?string
,!string
(orstring!
) but IMO it's much more explicit (or rather self-explanatory) to use a separate flag for thatOriginally posted by @mhsdesign in https://github.com/neos/neos-development-collection/issues/4304#issuecomment-1578965994