-
Notifications
You must be signed in to change notification settings - Fork 774
Make ValidatorBuilder::isValid fail fast implicitly #1633
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
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * SPDX-License-Identifier: MIT | ||
| * SPDX-FileCopyrightText: (c) Respect Project Contributors | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Respect\Validation; | ||
|
|
||
| interface IsValid extends Validator | ||
| { | ||
| public function isValid(mixed $input): bool; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * SPDX-License-Identifier: MIT | ||
| * SPDX-FileCopyrightText: (c) Respect Project Contributors | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Respect\Validation\Benchmarks; | ||
|
|
||
| use Generator; | ||
| use PhpBench\Attributes as Bench; | ||
| use Respect\Validation\Validator; | ||
| use Respect\Validation\ValidatorBuilder; | ||
| use Respect\Validation\Validators\Alnum; | ||
| use Respect\Validation\Validators\Alpha; | ||
| use Respect\Validation\Validators\BoolType; | ||
| use Respect\Validation\Validators\Digit; | ||
| use Respect\Validation\Validators\Even; | ||
| use Respect\Validation\Validators\FloatType; | ||
| use Respect\Validation\Validators\IntType; | ||
| use Respect\Validation\Validators\Negative; | ||
| use Respect\Validation\Validators\Positive; | ||
| use Respect\Validation\Validators\StringType; | ||
|
|
||
| final class CompositeValidatorsBench | ||
| { | ||
| /** @param array{string, array<Validator>} $params */ | ||
| #[Bench\ParamProviders(['provideValidatorBuilder'])] | ||
| #[Bench\Iterations(5)] | ||
| #[Bench\Revs(50)] | ||
| #[Bench\Warmup(1)] | ||
| #[Bench\Subject] | ||
| public function isValid(array $params): void | ||
| { | ||
| ValidatorBuilder::__callStatic(...$params)->isValid(42); | ||
| } | ||
|
|
||
| public function provideValidatorBuilder(): Generator | ||
| { | ||
| yield 'allOf(10)' => ['allOf', $this->buildValidators(10)]; | ||
| yield 'oneOf(10)' => ['oneOf', $this->buildValidators(10)]; | ||
| yield 'anyOf(10)' => ['anyOf', $this->buildValidators(10)]; | ||
| yield 'noneOf(10)' => ['noneOf', $this->buildValidators(10)]; | ||
| yield 'allOf(100)' => ['allOf', $this->buildValidators(100)]; | ||
| yield 'oneOf(100)' => ['oneOf', $this->buildValidators(100)]; | ||
| yield 'anyOf(100)' => ['anyOf', $this->buildValidators(100)]; | ||
| yield 'noneOf(100)' => ['noneOf', $this->buildValidators(100)]; | ||
| } | ||
|
|
||
| /** @return array<Validator> */ | ||
| private function buildValidators(int $count): array | ||
| { | ||
| $validators = []; | ||
| for ($i = 0; $i < $count; $i++) { | ||
| $validators[] = $this->makeValidator($i); | ||
| } | ||
|
|
||
| return $validators; | ||
| } | ||
|
|
||
| private function makeValidator(int $index): Validator | ||
| { | ||
| return match ($index % 10) { | ||
| 0 => new IntType(), | ||
| 1 => new Positive(), | ||
| 2 => new Negative(), | ||
| 3 => new Even(), | ||
| 4 => new FloatType(), | ||
| 5 => new StringType(), | ||
| 6 => new Alpha(), | ||
| 7 => new Alnum(), | ||
| 8 => new Digit(), | ||
| default => new BoolType(), | ||
| }; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * SPDX-License-Identifier: MIT | ||
| * SPDX-FileCopyrightText: (c) Respect Project Contributors | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Respect\Validation; | ||
|
|
||
| use PHPUnit\Framework\Attributes\CoversClass; | ||
| use PHPUnit\Framework\Attributes\Group; | ||
| use PHPUnit\Framework\Attributes\Test; | ||
| use Respect\Validation\Exceptions\ComponentException; | ||
| use Respect\Validation\Test\TestCase; | ||
| use Respect\Validation\Test\Validators\Stub; | ||
| use Respect\Validation\Validators\AllOf; | ||
|
|
||
| #[Group('validator')] | ||
| #[CoversClass(ValidatorBuilder::class)] | ||
| final class ValidatorBuilderTest extends TestCase | ||
| { | ||
| #[Test] | ||
| public function shouldDelegateToIsValidWhenSingleValidatorInBuilder(): void | ||
| { | ||
| $builder = ValidatorBuilder::init(new AllOf(Stub::pass(1), Stub::pass(1))); | ||
|
|
||
| self::assertTrue($builder->isValid([])); | ||
| } | ||
|
|
||
| #[Test] | ||
| public function shouldCallIsValidOnCombinedIsValidWhenMultipleValidatorsExist(): void | ||
| { | ||
| $builder = ValidatorBuilder::init( | ||
| Stub::pass(1), | ||
| Stub::pass(1), | ||
| Stub::pass(1), | ||
| Stub::fail(1), | ||
| ); | ||
|
|
||
| self::assertFalse($builder->isValid([])); | ||
| } | ||
|
|
||
| #[Test] | ||
| public function shouldThrowComponentExceptionWhenNoValidatorsExist(): void | ||
| { | ||
| $this->expectException(ComponentException::class); | ||
|
|
||
| ValidatorBuilder::init()->isValid([]); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the goal is improving the performance (or simplify) of the
isValid()method, we could do this simply by usingCircuitinstead ofAllOfhere.I like what you're doing with
Generative, and I see that with it we would still have performance improvements for other "composite" rules (likeAllOf,AnyOf), but then I would argue that what we're having inGenerative:::isValid()should be something we use inCircuitusesWe could just have a different Rule interface (
FailFastRule) what would have another method (evaluateWithFailFast()) so validators would implement that method andCircuitwould use them. That way we makeCircuita true fail-fast validator.PS.: The names I suggested are just illustrative, I don't think they're good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circuitdoes not apply itself recursively through all its children. If there's an AllOf inside it, thatAllOfwill be fully evaluated.Generativegoes deep.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, that's why I'm suggesting this solution. Something like this:
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The major gain is that
Circuitbecomes a REAL fail fast validator, and it's not so different from what you're doing.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your suggestion returns
Result. What is the goal here? Build a partialResultinstance?I solved this by interfacing to boolean, so I don't have to deal with partials.
Seems like you're only wrapping the first level of chains in this solution. Circuit can see other Circuits, but it's up to the user to wrap every single of the inner ones, or make sure they're at the beginning of the chain.
Consider this:
In this case, the composite is not the first node in the chain. It would be wrapped by
Circuit, but then, how can you fail fast the secondallOfnode?I'm not sure we need an extra wrapping around
Circuitevery time we find aFailFastRule. I just decided to make that mechanism part of the interface itself.This way,
Generativebecomes an inner path for getting booleans fast.Circuitremains as an useful tool for managing individual fast-fail reports with messages. There is no way to combine both (getting true fail fast and full Report instances at the same time).Maybe I misunderstood your suggestion. My main blocker here is dealing with potentially partial
Resultinstances. That is what I'm most curious about your suggestion, how to deal with that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I finished the implementation and tests, and the PR is not an incomplete draft anymore. I added you as a reviewer.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suggesting to use
Circuitas a default only onisValid(), not onassert(), because indeed, it's up to the user to define how they'd like the results to be shown.If we consider the following chain:
when we would use
isValid()(notassert()), it would be translated into something like:BUT, because
CircuitcallsFailFastValidator:: evaluateWithFailFast(),AllOfwould fail-fast if one of its validators fail. Admittedly, that works best withAllOfandNoneOfthan with the other ones, because returning partial results inAnyOfandOneOfcan confusing for the user, because we would indeed return "partial results", and the message might not make sense withAnyOfandOneOf.I meant "partial" because in the context of
AllOfandNoneOfeach child result makes sense on its own, not so much forAnyOfandOneOfYour changes sparkle some ideas, and I thought we could indeed return "partial"
Resultinstances. A better way of saying it is to return the child result directly, not the parent (in some cases)With the changes I'm suggesting
Circuitcan fail-fast not only with the validators we give it, but those validators themselves could also fail-fast (even theEachvalidator). If someone has av::circuit(v::intVal(), v::allOf(v::positive(), v::greaterThan(3)))->assert(-1), theAllOfvalidator would never evaluatev::greaterThan(3), and instead return the result ofPositive(), but only becauseCircuitis the main wrapper.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fair if we don't want to have a
Circuitas a wrapper the whole time, we could callevaluateWithFailFast()directly onisValid()Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I experimented locally with partial
Resultinstances. It's a deep rabbit hole.Ultimately, we could make
Resultcompletely lazy (it already carries the Validator instance it was obtained from), using property hooks for$hasPassedas I mentioned previously. This PR advances the internal engine towards that goal. The immutable list of children is a design issue though. It only goes as far as partials.The truth is that getting a boolean will always be faster, even than partial or lazy
Resultinstances. And it's simpler to implement, so it makes a lot of sense (to me) to start with a small measurable deliverable that does that. No changes to public interface, no complications, clear use case for all sorts of scenarios.That is a separate refactor, improving
FilteredNonEmptyArrayto deal with iterables (basically, making itFilteredNonEmptyIterable). There are challenges with no-rewind style iterables though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@henriquemoody
I have updated the PR to only feature the boolean
IsValidinterface, and not deal with any generators, leaving space for us to decide on how to deal with result streaming later.This still offers a significant performance improvement for obtaining boolean only results,
if (v::foo()->bar()->baz()->validate()is very a common usage pattern seeing in our users across GitHub. Examples:https://github.com/search?q=%22Respect%5CValidation%22+AND+%22v%3A%3A%22+AND+%22validate%28%22&type=code
This also opens up possibilities for modes, such as
--verbose/--quietin (but not limited to) CLI apps, getting both performance in the quiet mode and throroughness in the verbose mode.With this, you are free to implement your ideas around
Circuit, as long as the benchmarks for single boolean do not display any performance regression (I would accept a small performance regression actually).