-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduced StructValidator to unpack validation errors for structs
- Loading branch information
Showing
7 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
43 changes: 43 additions & 0 deletions
43
src/contracts/Validation/AbstractValidationStructWrapper.php
This file contains 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,43 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
namespace Ibexa\Contracts\Core\Validation; | ||
|
||
use Symfony\Component\Validator\Constraints as Assert; | ||
|
||
/** | ||
* @template T of object | ||
*/ | ||
abstract class AbstractValidationStructWrapper implements ValidatorStructWrapperInterface | ||
{ | ||
/** | ||
* @phpstan-var T | ||
* | ||
* @Assert\Valid() | ||
*/ | ||
private object $struct; | ||
|
||
/** | ||
* @phpstan-param T $struct | ||
*/ | ||
public function __construct(object $struct) | ||
{ | ||
$this->struct = $struct; | ||
} | ||
|
||
/** | ||
* @phpstan-return T | ||
*/ | ||
final public function getStruct(): object | ||
{ | ||
return $this->struct; | ||
} | ||
|
||
final public function getStructName(): string | ||
{ | ||
return 'struct'; | ||
} | ||
} |
This file contains 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,92 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
namespace Ibexa\Contracts\Core\Validation; | ||
|
||
use Symfony\Component\Validator\ConstraintViolation; | ||
use Symfony\Component\Validator\ConstraintViolationList; | ||
use Symfony\Component\Validator\ConstraintViolationListInterface; | ||
use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||
use Symfony\Component\Validator\Mapping\MetadataInterface; | ||
use Symfony\Component\Validator\Validator\ContextualValidatorInterface; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
|
||
final class StructValidator implements ValidatorInterface | ||
{ | ||
private ValidatorInterface $inner; | ||
|
||
public function __construct(ValidatorInterface $inner) | ||
{ | ||
$this->inner = $inner; | ||
} | ||
|
||
public function getMetadataFor($value): MetadataInterface | ||
{ | ||
return $this->inner->getMetadataFor($value); | ||
} | ||
|
||
public function hasMetadataFor($value): bool | ||
{ | ||
return $this->inner->hasMetadataFor($value); | ||
} | ||
|
||
public function validate($value, $constraints = null, $groups = null): ConstraintViolationListInterface | ||
{ | ||
$result = $this->inner->validate($value, $constraints, $groups); | ||
|
||
if (!$value instanceof ValidatorStructWrapperInterface) { | ||
return $result; | ||
} | ||
|
||
$unwrappedErrors = new ConstraintViolationList(); | ||
|
||
// Skip $ from argument name | ||
$prefix = ltrim($value->getStructName(), '$') . '.'; | ||
foreach ($result as $error) { | ||
$path = $error->getPropertyPath(); | ||
if (str_starts_with($path, $prefix)) { | ||
$path = substr($path, strlen($prefix)); | ||
} | ||
|
||
$unwrappedError = new ConstraintViolation( | ||
$error->getMessage(), | ||
$error->getMessageTemplate(), | ||
$error->getParameters(), | ||
$error->getRoot(), | ||
$path, | ||
$error->getInvalidValue(), | ||
$error->getPlural(), | ||
$error->getCode(), | ||
$error->getConstraint(), | ||
$error->getCause() | ||
); | ||
|
||
$unwrappedErrors->add($unwrappedError); | ||
} | ||
|
||
return $unwrappedErrors; | ||
} | ||
|
||
public function validateProperty(object $object, string $propertyName, $groups = null): ConstraintViolationListInterface | ||
{ | ||
return $this->inner->validatePropertyValue($object, $propertyName, $groups); | ||
} | ||
|
||
public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null): ConstraintViolationListInterface | ||
{ | ||
return $this->inner->validatePropertyValue($objectOrClass, $propertyName, $groups); | ||
} | ||
|
||
public function startContext(): ContextualValidatorInterface | ||
{ | ||
return $this->inner->startContext(); | ||
} | ||
|
||
public function inContext(ExecutionContextInterface $context): ContextualValidatorInterface | ||
{ | ||
return $this->inner->inContext($context); | ||
} | ||
} |
This file contains 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,60 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
declare(strict_types=1); | ||
|
||
namespace Ibexa\Contracts\Core\Validation; | ||
|
||
use Exception; | ||
use Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException; | ||
use Symfony\Component\Validator\ConstraintViolationListInterface; | ||
|
||
final class ValidationFailedException extends InvalidArgumentException | ||
{ | ||
private ConstraintViolationListInterface $errors; | ||
|
||
public function __construct( | ||
string $argumentName, | ||
ConstraintViolationListInterface $errors, | ||
Exception $previous = null | ||
) { | ||
parent::__construct($this->createMessage($argumentName, $errors), 0, $previous); | ||
|
||
$this->errors = $errors; | ||
} | ||
|
||
public function getErrors(): ConstraintViolationListInterface | ||
{ | ||
return $this->errors; | ||
} | ||
|
||
private function createMessage(string $argumentName, ConstraintViolationListInterface $errors): string | ||
{ | ||
if ($errors->count() === 0) { | ||
throw new \InvalidArgumentException(sprintf( | ||
'Cannot create %s with empty validation error list.', | ||
self::class, | ||
)); | ||
} | ||
|
||
if ($errors->count() === 1) { | ||
return sprintf( | ||
"Argument '%s->%s' is invalid: %s", | ||
$argumentName, | ||
$errors->get(0)->getPropertyPath(), | ||
$errors->get(0)->getMessage() | ||
); | ||
} | ||
|
||
return sprintf( | ||
"Argument '%s->%s' is invalid: %s and %d more errors", | ||
$argumentName, | ||
$errors->get(0)->getPropertyPath(), | ||
$errors->get(0)->getMessage(), | ||
$errors->count() - 1 | ||
); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/contracts/Validation/ValidatorStructWrapperInterface.php
This file contains 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,12 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
namespace Ibexa\Contracts\Core\Validation; | ||
|
||
interface ValidatorStructWrapperInterface | ||
{ | ||
public function getStructName(): string; | ||
} |
This file contains 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,123 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
namespace Ibexa\Tests\Core\Validation; | ||
|
||
use Ibexa\Contracts\Core\Validation\StructValidator; | ||
use Ibexa\Contracts\Core\Validation\ValidatorStructWrapperInterface; | ||
use PHPUnit\Framework\TestCase; | ||
use stdClass; | ||
use Symfony\Component\Validator\ConstraintViolation; | ||
use Symfony\Component\Validator\ConstraintViolationInterface; | ||
use Symfony\Component\Validator\ConstraintViolationList; | ||
use Symfony\Component\Validator\ConstraintViolationListInterface; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
|
||
/** | ||
* @covers \Ibexa\Contracts\Core\Validation\StructValidator | ||
*/ | ||
final class StructValidatorTest extends TestCase | ||
{ | ||
/** @var \Symfony\Component\Validator\Validator\ValidatorInterface&\PHPUnit\Framework\MockObject\MockObject */ | ||
private ValidatorInterface $validator; | ||
|
||
private StructValidator $structValidator; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->validator = $this->createMock(ValidatorInterface::class); | ||
$this->structValidator = new StructValidator($this->validator); | ||
} | ||
|
||
public function testAssertValidStructWithValidStruct(): void | ||
{ | ||
$struct = new stdClass(); | ||
$initialErrors = $this->createMock(ConstraintViolationListInterface::class); | ||
$initialErrors->method('count')->willReturn(0); | ||
|
||
$this->validator | ||
->expects(self::once()) | ||
->method('validate') | ||
->with( | ||
$struct, | ||
null, | ||
['Default', 'group'] | ||
)->willReturn($initialErrors); | ||
|
||
$errors = $this->structValidator->validate(new stdClass(), null, ['Default', 'group']); | ||
self::assertSame($initialErrors, $errors); | ||
self::assertCount(0, $errors); | ||
} | ||
|
||
public function testAssertValidStructWithInvalidStruct(): void | ||
{ | ||
$initialError = $this->createExampleConstraintViolation(); | ||
$initialErrors = $this->createExampleConstraintViolationList($initialError); | ||
|
||
$this->validator | ||
->method('validate') | ||
->with( | ||
new stdClass(), | ||
null, | ||
['Default', 'group'] | ||
)->willReturn($initialErrors); | ||
|
||
$errors = $this->structValidator->validate(new stdClass(), null, ['Default', 'group']); | ||
self::assertSame($initialErrors, $errors); | ||
self::assertCount(1, $errors); | ||
|
||
$error = $errors->get(0); | ||
self::assertSame($initialError, $error); | ||
self::assertEquals('validation error', $error->getMessage()); | ||
self::assertEquals('struct.property', $error->getPropertyPath()); | ||
} | ||
|
||
public function testAssertValidStructWithInvalidWrapperStruct(): void | ||
{ | ||
$initialError = $this->createExampleConstraintViolation(); | ||
$initialErrors = $this->createExampleConstraintViolationList($initialError); | ||
|
||
$struct = $this->createMock(ValidatorStructWrapperInterface::class); | ||
$struct->expects(self::once()) | ||
->method('getStructName') | ||
->willReturn('$struct'); | ||
|
||
$this->validator | ||
->method('validate') | ||
->with( | ||
$struct, | ||
null, | ||
['Default', 'group'] | ||
)->willReturn($initialErrors); | ||
|
||
$errors = $this->structValidator->validate($struct, null, ['Default', 'group']); | ||
self::assertNotSame($initialErrors, $errors); | ||
self::assertCount(1, $errors); | ||
|
||
$error = $errors->get(0); | ||
self::assertNotSame($error, $initialError); | ||
self::assertSame('validation error', $error->getMessage()); | ||
self::assertSame('property', $error->getPropertyPath()); | ||
} | ||
|
||
private function createExampleConstraintViolation(): ConstraintViolationInterface | ||
{ | ||
return new ConstraintViolation( | ||
'validation error', | ||
null, | ||
[], | ||
'', | ||
'struct.property', | ||
'example' | ||
); | ||
} | ||
|
||
private function createExampleConstraintViolationList( | ||
ConstraintViolationInterface ...$errors | ||
): ConstraintViolationListInterface { | ||
return new ConstraintViolationList($errors); | ||
} | ||
} |
Oops, something went wrong.