Skip to content
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

[RFC] Stateless Validators (Again) #153

Draft
wants to merge 4 commits into
base: 2.26.x
Choose a base branch
from

Conversation

gsteel
Copy link
Member

@gsteel gsteel commented Sep 21, 2022

Q A
BC Break yes
New Feature yes
RFC yes

Description

Is there any appetite for refactoring the validator component to return a validation result, roughly as described in this PR?

The theory here is that error messages/validation failures are carried around by the immutable validation result and translation of the messages would be performed on/by the result using a composed translator (if any).

This would allow validators to behave more like stateless services.

A refactoring like this has been discussed in the past: #48, #38, #49

By using a value object to represent the validation result, error message iteration and translation can be extracted from all the validators.

Signed-off-by: George Steel <[email protected]>
@gsteel gsteel added this to the 3.0.0 milestone Sep 21, 2022
@gsteel
Copy link
Member Author

gsteel commented Sep 21, 2022

Obviously, I've made absolutely no attempt to make CI pass here!

@gsteel gsteel marked this pull request as draft September 21, 2022 13:56
src/StringLength.php Show resolved Hide resolved
Comment on lines 33 to 34
/** @implements IteratorAggregate<string, non-empty-string> */
final class ValidationResult implements IteratorAggregate, Countable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think ValidationResult should be iterable nor countable: from a consumer PoV, it should only be iterable+countable if it failed validation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do you think it would be better to define different types for success and failure states?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would help:

  • while debugging
  • to get better types for us internally
  • to make it crash in case somebody tries to read error messages out of a success scenario

src/StringLength.php Show resolved Hide resolved
src/StringLength.php Show resolved Hide resolved
Comment on lines 69 to 78
public static function new(
array $templates,
array $variables,
mixed $value,
bool $valueObscured = false,
?TranslatorInterface $translator = null,
string $textDomain = 'default',
): self {
return new self($templates, $variables, $value, $valueObscured, $translator, $textDomain);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have two separate named constructors:

  1. failure
  2. success

See http://marcosh.github.io/post/2017/06/16/maybe-in-php.html and http://marcosh.github.io/post/2017/10/27/maybe-in-php-2.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial idea here is that it has a kind of builder interface like (inside a validator):

$result = ValidationResult::new($myTemplates, $substitutions, $value, false, $translator);

if (! is_string($value)) {
   return $result->withError($someKey);
}

if (! $this->constraintA($value)) {
   $result = $result->withError($otherKey);
}

if (! $this->constraintB($value)) {
   $result = $result->withError($anotherKey);
}

return $result;

use function str_replace;
use function strlen;

/** @implements IteratorAggregate<string, non-empty-string> */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @psalm-immutable

Comment on lines 165 to 191
/** @psalm-pure */
private static function stringify(mixed $value): string
{
if (is_float($value) || is_int($value) || is_string($value)) {
return (string) $value;
}

if (is_bool($value)) {
return $value ? 'true' : 'false';
}

if ($value === null) {
return 'null';
}

if (is_array($value)) {
return 'array';
}

if (is_resource($value)) {
return 'resource';
}

assert(is_object($value));

return self::stringifyObject($value);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's the validator's responsibility, or the view helper's 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a major issue for me though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly, this was about being absolutely rigid about the expected value of the list of error messages, i.e. array<string, non-empty-string> and because I put translation inside the result, I had to implement some kind of casting for the possible substitutions. It would be cleaner to make the validators provide strings to the constructor, but that would bloat all the validators wouldn't it?

If translation was external to both validators and the result, i.e. $validatorTranslator->translate($validationResult) it would be even cleaner, but more responsibility is shifted to the view, i.e. where do they get the validator translator from? Also, the result would need to expose everything the translator needs, the message templates, keys, value, substitution keys and values.

@froschdesign
Copy link
Member

class StringLength extends AbstractValidator
/**
* @psalm-type OptionsArray array{
* min: int,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int<0, max> to prevent negative numbers (although that would asserting)

/**
* @psalm-type OptionsArray array{
* min: int,
* max: int|null,
Copy link
Member

@internalsystemerror internalsystemerror Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int<0, max>|null to prevent negative numbers (although that would asserting)

@gsteel gsteel removed this from the 3.0.0 milestone Jun 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants