Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
55c6822
composer dependencies
daniel-myparcel Nov 21, 2025
c3edbc6
:sparkles: replaced our validation logic to use new lib + fixed faili…
daniel-myparcel Nov 21, 2025
502d692
Moved validation logic to dedicated module
daniel-myparcel Nov 21, 2025
d468a31
updated ->validate()
daniel-myparcel Nov 24, 2025
422c4a6
drop redundant class property
daniel-myparcel Nov 26, 2025
b980650
return $result instead of throwing exception in validator + handle ex…
daniel-myparcel Nov 26, 2025
c83b0d6
delete dump
daniel-myparcel Nov 26, 2025
b9fd233
Brought back tests for the Form class validation
daniel-myparcel Nov 26, 2025
346ee33
remove TODO
daniel-myparcel Nov 26, 2025
8fd7dea
undo new line change
daniel-myparcel Nov 26, 2025
736a70a
Added error message assertions
daniel-myparcel Nov 26, 2025
ddc87be
Opis json schema now gets mocked instead of called directly
daniel-myparcel Nov 26, 2025
28020d9
added assertion that returned result is the same as the mocked one
daniel-myparcel Nov 26, 2025
4c147c3
improved tests
daniel-myparcel Nov 27, 2025
2eca39f
Refactor code for better testability
daniel-myparcel Dec 1, 2025
dc8e9e8
Fixed validator test
daniel-myparcel Dec 1, 2025
af7f849
typo
daniel-myparcel Dec 1, 2025
1490c79
typo
daniel-myparcel Dec 1, 2025
d19b539
Fix import mistake after rebase
daniel-myparcel Dec 3, 2025
b630463
initialize `ValidationResult` instance in the constructor of Validato…
daniel-myparcel Dec 3, 2025
75c30aa
divide ValidatorTest to 2 separate test cases - success and failure
daniel-myparcel Dec 3, 2025
89b9d08
slightly cleaner mocking in FormTest
daniel-myparcel Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"php": ">=8.4",
"laravel/framework": ">=12.0",
"ramsey/uuid": "^4.7",
"justinrainbow/json-schema": "^6.4"
"opis/json-schema": "^2.6"
},
"require-dev": {
"fakerphp/faker": "^1.24",
Expand Down
19 changes: 7 additions & 12 deletions src/Form/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
namespace MyParcelCom\JsonSchema\FormBuilder\Form;

use ArrayObject;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Validator;
use MyParcelCom\JsonSchema\FormBuilder\Form\Exceptions\FormValidationException;
use MyParcelCom\JsonSchema\FormBuilder\Validation\Validator;
use MyParcelCom\JsonSchema\FormBuilder\Validation\Exceptions\FormValidationException;

/**
* @extends ArrayObject<array-key, FormElement>
Expand All @@ -18,6 +17,7 @@ public function toJsonSchema(): array
{
return [
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'type' => 'object',
'additionalProperties' => false,
'required' => $this->getRequired(),
'properties' => $this->getProperties()->toArray(),
Expand All @@ -29,16 +29,11 @@ public function toJsonSchema(): array
* @param array<string, mixed> $values a key value array of form values
* @throws FormValidationException
*/
public function validate(array $values): void
public function validate(array $values, ?Validator $validator = null): void
Copy link
Member

Choose a reason for hiding this comment

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

Nice! I really like this entire method and how it reads now! Good job.

{
/**
* CHECK_MODE_TYPE_CAST: Enable fuzzy type checking for associative arrays and objects
* src: https://github.com/jsonrainbow/json-schema?tab=readme-ov-file#configuration-options
**/
$validator = new Validator;
$validator->validate($values, $this->toJsonSchema(), Constraint::CHECK_MODE_TYPE_CAST);
if (!$validator->isValid()) {
throw new FormValidationException("Form validation failed", $validator->getErrors());
$validator ??= new Validator($values, $this->toJsonSchema());
if(!$validator->isValid()) {
throw new FormValidationException(errors: $validator->getErrors());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

declare(strict_types=1);

namespace MyParcelCom\JsonSchema\FormBuilder\Form\Exceptions;
namespace MyParcelCom\JsonSchema\FormBuilder\Validation\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Arr;
use Throwable;
use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\ValidationResult;

/**
* Exception thrown when form validation fails.
*/
class FormValidationException extends Exception
{
public function __construct(
string $message = 'Form validation failed',
private readonly array $errors = [],
?Throwable $previous = null,
private readonly ?array $errors = null,
) {
parent::__construct(message: $message, previous: $previous);
parent::__construct($message);
}

/**
Expand All @@ -29,16 +31,10 @@ public function report(): bool

public function render(): JsonResponse
{
$errorMapping = Arr::collapse(array_map(
fn ($error) => [
$error['property'] => $error['message'],
],
$this->errors,
));
return new JsonResponse([
return new JsonResponse(array_filter([
'message' => $this->getMessage(),
'errors' => $errorMapping,
], 422);
'errors' => $this->errors,
]), 422);
}

/**
Expand Down
36 changes: 36 additions & 0 deletions src/Validation/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace MyParcelCom\JsonSchema\FormBuilder\Validation;

use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Helper;
use Opis\JsonSchema\ValidationResult;
use Opis\JsonSchema\Validator as JsonSchemaValidator;

class Validator
{
private ValidationResult $validationResult;
public function __construct(
private readonly array $values,
private readonly array $schema,
) {
$jsonSchemaValidator = new JsonSchemaValidator();
$this->validationResult = $jsonSchemaValidator->validate(Helper::toJSON($this->values), Helper::toJSON($this->schema));
}

/**
* Validates the form values against the JSON Schema.
*/
public function isValid(): bool
{
return $this->validationResult->isValid();
}
public function getErrors(): array
{
return $this->validationResult->hasError()
? new ErrorFormatter()->format($this->validationResult->error())
: [];
}
}
41 changes: 0 additions & 41 deletions tests/Form/Exceptions/FormValidationExceptionTest.php

This file was deleted.

59 changes: 20 additions & 39 deletions tests/Form/FormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@

namespace Tests\Form;

use Mockery;
use MyParcelCom\JsonSchema\FormBuilder\Form\Checkbox;
use MyParcelCom\JsonSchema\FormBuilder\Form\Exceptions\FormValidationException;
use MyParcelCom\JsonSchema\FormBuilder\Form\Form;
use MyParcelCom\JsonSchema\FormBuilder\Form\FormElementCollection;
use MyParcelCom\JsonSchema\FormBuilder\Form\Group;
use MyParcelCom\JsonSchema\FormBuilder\Form\Option;
use MyParcelCom\JsonSchema\FormBuilder\Form\OptionCollection;
use MyParcelCom\JsonSchema\FormBuilder\Form\RadioButtons;
use MyParcelCom\JsonSchema\FormBuilder\Form\Text;
use MyParcelCom\JsonSchema\FormBuilder\Validation\Exceptions\FormValidationException;
use MyParcelCom\JsonSchema\FormBuilder\Validation\Validator;
use PHPUnit\Framework\TestCase;

use function PHPUnit\Framework\assertEquals;
Expand Down Expand Up @@ -136,6 +138,7 @@ public function test_it_converts_to_json_schema(): void
{
assertEquals([
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'type' => 'object',
'additionalProperties' => false,
'required' => ['name_2', 'name_3'],
'properties' => [
Expand Down Expand Up @@ -250,9 +253,15 @@ public function test_it_gets_values(): void
/**
* @throws FormValidationException
*/
public function test_it_validates(): void
public function test_it_validates_success(): void
{
$this->expectNotToPerformAssertions();
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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.


$validator = Mockery::mock(Validator::class);

$validator->expects('isValid')->andReturnTrue();
$validator->expects('getErrors')->never();

$this->form->validate([
'name_1' => 'value',
'name_2' => false,
Expand All @@ -262,54 +271,26 @@ public function test_it_validates(): void
'name_2' => false,
'name_3' => 'a',
],
]);

$this->form->validate([
'name_1' => 'value',
'name_2' => false,
'name_3' => 'a',
]);
], $validator);
}

public function test_it_fails_to_validate_required_missing(): void
public function test_it_validates_failure(): void
{
$this->expectException(FormValidationException::class);

$validator = Mockery::mock(Validator::class);
$validator->expects('isValid')->andReturnFalse();
$validator->expects('getErrors');

$this->form->validate([
'name_1' => 'value',
'name_2' => false,
'name_4' => [
'name_1' => 'value',
'name_3' => 'a',
],
]);
}

public function test_it_fails_to_validate_wrong_property_type(): void
{
$this->expectException(FormValidationException::class);
$this->form->validate([
'name_1' => 5,
'name_2' => 'hello',
'name_3' => 'a',
'name_4' => [
'name_1' => 'value',
'name_2' => false,
'name_3' => 'a',
],
]);
}

public function test_it_fails_to_validate_invalid_enum_value(): void
{
$this->expectException(FormValidationException::class);
$this->form->validate([
'name_1' => 'value',
'name_2' => true,
'name_3' => 'x',
'name_4' => [
'name_1' => 'value',
'name_2' => true,
'name_3' => 'y',
],
]);
], $validator);
}
}
36 changes: 36 additions & 0 deletions tests/Validation/Exceptions/FormValidationExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Tests\Validation\Exceptions;

use Mockery;
use MyParcelCom\JsonSchema\FormBuilder\Validation\Exceptions\FormValidationException;
use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Errors\ValidationError;
use Opis\JsonSchema\ValidationResult;
use PHPUnit\Framework\TestCase;

use function PHPUnit\Framework\assertEquals;

class FormValidationExceptionTest extends TestCase
{
public function test_it_renders(): void
{
$exception = new FormValidationException('What a failure', [
'foo.bar' => 'Foo is required',
'bar.foo' => 'Bar is required',
]);

$response = $exception->render();

assertEquals(422, $response->getStatusCode());
assertEquals($response->getData(true), [
'message' => 'What a failure',
'errors' => [
'foo.bar' => 'Foo is required',
'bar.foo' => 'Bar is required',
],
]);
}
}
Loading