Improved json schema validation handling#7
Conversation
| { | ||
| private array $errors; | ||
| public function __construct( | ||
| private readonly ValidationResult $validationResult, |
There was a problem hiding this comment.
Seems that this variable is only used inside of the constructor itself. If that's the case, we don't need it as a class property.
| private readonly ValidationResult $validationResult, | |
| ValidationResult $validationResult, |
src/Validation/Validator.php
Outdated
| if (!$result->isValid()) { | ||
| throw new ValidationException($result); | ||
| } |
There was a problem hiding this comment.
I would return the result of the validator here instead and use evaluate the result and handle accordingly in the class that uses the validator instead.
The responsibility of this class should be to validate the incoming data against a schema and then return a result. Whether to actually stop the execution of the code and throw an exception is not the concern of the Validator itself, because context is important, which the Validator class is lacking.
tests/Form/FormTest.php
Outdated
| ), | ||
| ), | ||
| ); | ||
| dump($this->form->toJsonSchema()); |
There was a problem hiding this comment.
| dump($this->form->toJsonSchema()); |
| /** | ||
| * @throws FormValidationException | ||
| */ | ||
| public function test_it_validates(): void |
There was a problem hiding this comment.
These tests related to validation have been removed, likely because the validation logic has shifted. But still, I would expect there to be at least some tests related to validation, since the validate method still exists.
If you're struggling with testing the static method, then you might consider injecting the validator, either through a constructor or a method parameter. That way the validator object can be mocked and its failed and success flows can be tested here.
tests/Validation/ValidatorTest.php
Outdated
| protected array $schema; | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); // TODO: Change the autogenerated stub |
There was a problem hiding this comment.
| parent::setUp(); // TODO: Change the autogenerated stub | |
| parent::setUp(); |
tests/Validation/ValidatorTest.php
Outdated
| public function test_it_fails_to_validate_required_missing(): void | ||
| { | ||
| $this->expectException(ValidationException::class); | ||
| Validator::validate([ | ||
| 'name_1' => 'value', | ||
| 'name_2' => false, | ||
| 'name_4' => [ | ||
| 'name_1' => 'value', | ||
| 'name_3' => 'a', | ||
| ], | ||
| ], $this->schema); | ||
| } | ||
|
|
||
| public function test_it_fails_to_validate_wrong_property_type(): void | ||
| { | ||
| $this->expectException(ValidationException::class); | ||
| Validator::validate([ | ||
| 'name_1' => 5, | ||
| 'name_2' => 'hello', | ||
| 'name_3' => 'a', | ||
| 'name_4' => [ | ||
| 'name_1' => 'value', | ||
| 'name_3' => 'a', | ||
| ], | ||
| ], $this->schema); | ||
| } | ||
|
|
||
| public function test_it_fails_to_validate_invalid_enum_value(): void | ||
| { | ||
| $this->expectException(ValidationException::class); | ||
| Validator::validate([ | ||
| 'name_1' => 'value', | ||
| 'name_2' => true, | ||
| 'name_3' => 'x', | ||
| 'name_4' => [ | ||
| 'name_1' => 'value', | ||
| 'name_2' => true, | ||
| 'name_3' => 'y', | ||
| ], | ||
| ], $this->schema); | ||
| } |
There was a problem hiding this comment.
If you decide to test different use cases where validation can fail, I expect an assertion that that specific error is what failed the validation as well. For example, the first test expects validation to fail on missing required properties. If you decide to test that specifically (which you might argue is not even the validator's responsibility, because we didn't write that logic) I expect you to also have an assertion on the thrown Exception that reflects that specific problem.
There was a problem hiding this comment.
I added assertions to the error values, if you think it makes more sense to drop these tests altogether I'm not all too attached to it.
There was a problem hiding this comment.
In my previous comment I mentioned that I don't think the specific cases are the responsibility of this validator. I think a better test would be to make sure the external validator class is called and that our Validator class handles according to its return value.
There was a problem hiding this comment.
Ah right now I get you. I will make the change
There was a problem hiding this comment.
I replaced the testing in Validator with mock so that we only assert the third party library code is called as expected. I hope that I didn't miss something in your explanation definitly got a little confused with this one 🫠
NickVries
left a comment
There was a problem hiding this comment.
Whoops, forgot to click "Request changes" with my review 🙃
src/Form/Form.php
Outdated
| * @throws FormValidationException | ||
| */ | ||
| public function validate(array $values): void | ||
| public function validate(array $values, ?Validator $validator = null, ?ErrorFormatter $errorFormatter = null): void |
There was a problem hiding this comment.
I'm not gonna say I'm the biggest fan of this approach but since the $errorFormatter behaviour started requiring a lot of mocking I opted for also passing it in the validate method.
Lmk what you think.
There was a problem hiding this comment.
Yikes, I'm absolutely not a fan of this haha. The caller of $form->validate() now has to pass an $errorFormatter so that the Form then can pass it to the Validator, who can potentially pass it to the FormValidationException in case validation fails. That means that 4 classes are now aware of this formatter that has logic that should really only concern one of them and I don't think it's the Exception class.
Our own Validator class is already tightly coupled with the package, so it wouldn't add any coupling to have the Validator also instantiate the ErrorFormatter. Furthermore, I would personally make the Exception as dumb as possible with as little dependencies as possible. The validator can get a getErrors method that uses the ErrorFormatter to produce the errors in the required format so that the exception can just be dumb and act as an Exception only and not contain logic to format errors. Exceptions can be thrown anywhere with whatever context is available at that time. Making one accept a dependency also tightly couples the context of the Exception to the ErrorFormatter because it needs to accept the expected data to pass into the ErrorFormatter in order to produce a result. That's way too tightly coupled for my taste.
NickVries
left a comment
There was a problem hiding this comment.
Whoops, forgot to click "Request changes" with my review 🙃
src/Form/Form.php
Outdated
| * @throws FormValidationException | ||
| */ | ||
| public function validate(array $values): void | ||
| public function validate(array $values, ?Validator $validator = null, ?ErrorFormatter $errorFormatter = null): void |
There was a problem hiding this comment.
Yikes, I'm absolutely not a fan of this haha. The caller of $form->validate() now has to pass an $errorFormatter so that the Form then can pass it to the Validator, who can potentially pass it to the FormValidationException in case validation fails. That means that 4 classes are now aware of this formatter that has logic that should really only concern one of them and I don't think it's the Exception class.
Our own Validator class is already tightly coupled with the package, so it wouldn't add any coupling to have the Validator also instantiate the ErrorFormatter. Furthermore, I would personally make the Exception as dumb as possible with as little dependencies as possible. The validator can get a getErrors method that uses the ErrorFormatter to produce the errors in the required format so that the exception can just be dumb and act as an Exception only and not contain logic to format errors. Exceptions can be thrown anywhere with whatever context is available at that time. Making one accept a dependency also tightly couples the context of the Exception to the ErrorFormatter because it needs to accept the expected data to pass into the ErrorFormatter in order to produce a result. That's way too tightly coupled for my taste.
…ception directly in the Form class
0e72386 to
d19b539
Compare
| * @throws FormValidationException | ||
| */ | ||
| public function validate(array $values): void | ||
| public function validate(array $values, ?Validator $validator = null): void |
There was a problem hiding this comment.
Nice! I really like this entire method and how it reads now! Good job.
src/Validation/Validator.php
Outdated
| : []; | ||
| } | ||
|
|
||
| private function getValidationResult(): ValidationResult |
There was a problem hiding this comment.
This method is called multiple times, but relies on the read-only class properties, and will therefore not yield different results. Let's cache the result to remove this duplicated step. We can cache the result on the class after the first time it's performed and check if that property is set in each call (and set it otherwise of course).
There was a problem hiding this comment.
Good point!
I took a slightly different approach: init an instance of ValidationResult in the constructor and then use it to get both the isValid and getErrors. Since it never changes this will work exactly the same 👍
| public function test_it_validates(): void | ||
| public function test_it_validates_success(): void | ||
| { | ||
| $this->expectNotToPerformAssertions(); |
There was a problem hiding this comment.
Not super important, but maybe there is a expectNotToThrowExceptions or similar? Since that is the only thing the method does apart from invoking other classes. Besides, the mocked expectations are assertions, so I don't think we need this statement at all.
There was a problem hiding this comment.
There doesn't seem to be such an assertion. Copilot seems to agree:
If you want to explicitly check that no exception is thrown, simply run the code without setting any exception expectations. If an exception is thrown, the test will fail automatically.
| self::assertTrue($validator->isValid()); | ||
| self::assertEquals([], $validator->getErrors()); | ||
|
|
||
| $values = [ |
There was a problem hiding this comment.
Let's put this in a separate test, so that we isolate one type of test per method and dont have to figure out what part of this test fails in case it does.
…r to avoid redundantly reinstantiating that class
✨ Moved to a more up-to-date Json schema validation library
♻️ Refactored validation to be under its own module (
Validation\Validator)ForminstanceJira Ticket