diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature
index c4c0e66e766..162d9285365 100644
--- a/features/graphql/mutation.feature
+++ b/features/graphql/mutation.feature
@@ -114,7 +114,6 @@ Feature: GraphQL mutation support
And the JSON node "data.updateFoo.clientMutationId" should be equal to "myId"
@createSchema
- @dropSchema
Scenario: Modify an item with composite identifiers through a mutation
Given there are Composite identifier objects
When I send the following GraphQL request:
@@ -133,3 +132,18 @@ Feature: GraphQL mutation support
And the JSON node "data.updateCompositeRelation.id" should be equal to "/composite_relations/compositeItem=1;compositeLabel=2"
And the JSON node "data.updateCompositeRelation.value" should be equal to "Modified value."
And the JSON node "data.updateCompositeRelation.clientMutationId" should be equal to "myId"
+
+ @dropSchema
+ Scenario: Trigger a validation error
+ When I send the following GraphQL request:
+ """
+ mutation {
+ createDummy(input: {_id: 12, name: "", clientMutationId: "myId"}) {
+ clientMutationId
+ }
+ }
+ """
+ Then the response status code should be 400
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
index e14b9709f05..3447a31acef 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
@@ -25,7 +25,8 @@
-
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
index c45ff9c4b5e..8fcace8dc56 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
@@ -5,15 +5,20 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+
+
+
+
+
-
-
+
+
-
diff --git a/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php b/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php
index dcd453c706b..32f1273dcd7 100644
--- a/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php
+++ b/src/Bridge/Symfony/Validator/EventListener/ValidateListener.php
@@ -16,6 +16,7 @@
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
+use ApiPlatform\Core\Validator\EventListener\ValidateListener as MainValidateListener;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
@@ -24,6 +25,8 @@
/**
* Validates data.
*
+ * @deprecated
+ *
* @author Kévin Dunglas
*/
final class ValidateListener
@@ -34,6 +37,8 @@ final class ValidateListener
public function __construct(ValidatorInterface $validator, ResourceMetadataFactoryInterface $resourceMetadataFactory, ContainerInterface $container = null)
{
+ @trigger_error(sprintf('Using "%s" is deprecated since API Platform 2.2 and will not be possible anymore in API Platform 3. Use "%s" instead.', __CLASS__, MainValidateListener::class), E_USER_DEPRECATED);
+
$this->validator = $validator;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->container = $container;
diff --git a/src/Bridge/Symfony/Validator/Exception/ValidationException.php b/src/Bridge/Symfony/Validator/Exception/ValidationException.php
index c0819e16f7a..8d7d4ee0b4b 100644
--- a/src/Bridge/Symfony/Validator/Exception/ValidationException.php
+++ b/src/Bridge/Symfony/Validator/Exception/ValidationException.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Core\Bridge\Symfony\Validator\Exception;
+use ApiPlatform\Core\Validator\Exception\ValidationException as BaseValidationException;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
@@ -20,15 +21,15 @@
*
* @author Kévin Dunglas
*/
-final class ValidationException extends \RuntimeException
+final class ValidationException extends BaseValidationException
{
private $constraintViolationList;
- public function __construct(ConstraintViolationListInterface $constraintViolationList, $message = '', $code = 0, \Exception $previous = null)
+ public function __construct(ConstraintViolationListInterface $constraintViolationList, string $message = '', int $code = 0, \Exception $previous = null)
{
- parent::__construct($message, $code, $previous);
-
$this->constraintViolationList = $constraintViolationList;
+
+ parent::__construct($message ?: $this->__toString(), $code, $previous);
}
/**
@@ -40,4 +41,21 @@ public function getConstraintViolationList()
{
return $this->constraintViolationList;
}
+
+ public function __toString(): string
+ {
+ $message = '';
+ foreach ($this->constraintViolationList as $violation) {
+ if ('' !== $message) {
+ $message .= "\n";
+ }
+ if ($propertyPath = $violation->getPropertyPath()) {
+ $message .= "$propertyPath: ";
+ }
+
+ $message .= $violation->getMessage();
+ }
+
+ return $message;
+ }
}
diff --git a/src/Bridge/Symfony/Validator/Validator.php b/src/Bridge/Symfony/Validator/Validator.php
new file mode 100644
index 00000000000..405d7727101
--- /dev/null
+++ b/src/Bridge/Symfony/Validator/Validator.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Bridge\Symfony\Validator;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
+use ApiPlatform\Core\Validator\ValidatorInterface;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface as SymfonyValidatorInterface;
+
+/**
+ * Validates an item using the Symfony validator component.
+ *
+ * @author Kévin Dunglas
+ */
+class Validator implements ValidatorInterface
+{
+ private $validator;
+ private $container;
+
+ public function __construct(SymfonyValidatorInterface $validator, ContainerInterface $container = null)
+ {
+ $this->validator = $validator;
+ $this->container = $container;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($data, array $context = [])
+ {
+ if (null !== $validationGroups = $context['groups'] ?? null) {
+ if (
+ $this->container &&
+ \is_string($validationGroups) &&
+ $this->container->has($validationGroups) &&
+ ($service = $this->container->get($validationGroups)) &&
+ \is_callable($service)
+ ) {
+ $validationGroups = $service($data);
+ } elseif (\is_callable($validationGroups)) {
+ $validationGroups = $validationGroups($data);
+ }
+
+ $validationGroups = (array) $validationGroups;
+ }
+
+ $violations = $this->validator->validate($data, null, $validationGroups);
+ if (0 !== \count($violations)) {
+ throw new ValidationException($violations);
+ }
+ }
+}
diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
index b5ee66c9293..982f5e6d940 100644
--- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
+++ b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
@@ -20,7 +20,10 @@
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
+use ApiPlatform\Core\Validator\Exception\ValidationException;
+use ApiPlatform\Core\Validator\ValidatorInterface;
use GraphQL\Error\Error;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -42,8 +45,9 @@ final class ItemMutationResolverFactory implements ResolverFactoryInterface
private $normalizer;
private $resourceMetadataFactory;
private $resourceAccessChecker;
+ private $validator;
- public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null)
+ public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, ValidatorInterface $validator = null)
{
if (!$normalizer instanceof DenormalizerInterface) {
throw new InvalidArgumentException(sprintf('The normalizer must implements the "%s" interface', DenormalizerInterface::class));
@@ -54,6 +58,7 @@ public function __construct(IriConverterInterface $iriConverter, DataPersisterIn
$this->normalizer = $normalizer;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->resourceAccessChecker = $resourceAccessChecker;
+ $this->validator = $validator;
}
public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
@@ -78,6 +83,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
case 'update':
$context = null === $item ? ['resource_class' => $resourceClass] : ['resource_class' => $resourceClass, 'object_to_populate' => $item];
$item = $this->normalizer->denormalize($args['input'], $resourceClass, ItemNormalizer::FORMAT, $context);
+ $this->validate($item, $info, $resourceMetadata, $operationName);
$this->dataPersister->persist($item);
return $this->normalizer->normalize(
@@ -98,4 +104,24 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
return $data;
};
}
+
+ /**
+ * @param object $item
+ *
+ * @throws Error
+ */
+ private function validate($item, ResolveInfo $info, ResourceMetadata $resourceMetadata, string $operationName = null)
+ {
+ if (null === $this->validator) {
+ return;
+ }
+
+ $validationGroups = $resourceMetadata->getGraphqlAttribute($operationName, 'validation_groups', null, true);
+
+ try {
+ $this->validator->validate($item, ['groups' => $validationGroups]);
+ } catch (ValidationException $e) {
+ throw Error::createLocatedError($e->getMessage(), $info->fieldNodes, $info->path);
+ }
+ }
}
diff --git a/src/Serializer/AbstractConstraintViolationListNormalizer.php b/src/Serializer/AbstractConstraintViolationListNormalizer.php
index 6253e78f6bd..9f8549366d7 100644
--- a/src/Serializer/AbstractConstraintViolationListNormalizer.php
+++ b/src/Serializer/AbstractConstraintViolationListNormalizer.php
@@ -57,10 +57,7 @@ protected function getMessagesAndViolations(ConstraintViolationListInterface $co
}
$violations[] = $violationData;
-
- $propertyPath = $violation->getPropertyPath();
- $prefix = $propertyPath ? sprintf('%s: ', $propertyPath) : '';
- $messages[] = "$prefix{$violation->getMessage()}";
+ $messages[] = ($violationData['propertyPath'] ? "{$violationData['propertyPath']}: " : '').$violationData['message'];
}
return [$messages, $violations];
diff --git a/src/Validator/EventListener/ValidateListener.php b/src/Validator/EventListener/ValidateListener.php
new file mode 100644
index 00000000000..a1d7accc46c
--- /dev/null
+++ b/src/Validator/EventListener/ValidateListener.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Validator\EventListener;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Util\RequestAttributesExtractor;
+use ApiPlatform\Core\Validator\ValidatorInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+
+/**
+ * Validates data.
+ *
+ * @author Kévin Dunglas
+ */
+final class ValidateListener
+{
+ private $validator;
+ private $resourceMetadataFactory;
+
+ public function __construct(ValidatorInterface $validator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
+ {
+ $this->validator = $validator;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ }
+
+ /**
+ * Validates data returned by the controller if applicable.
+ *
+ * @throws ValidationException
+ */
+ public function onKernelView(GetResponseForControllerResultEvent $event)
+ {
+ $request = $event->getRequest();
+ if (
+ $request->isMethodSafe(false)
+ || $request->isMethod(Request::METHOD_DELETE)
+ || !($attributes = RequestAttributesExtractor::extractAttributes($request))
+ || !$attributes['receive']
+ ) {
+ return;
+ }
+
+ $data = $event->getControllerResult();
+ $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
+
+ if (isset($attributes['collection_operation_name'])) {
+ $validationGroups = $resourceMetadata->getCollectionOperationAttribute($attributes['collection_operation_name'], 'validation_groups', null, true);
+ } else {
+ $validationGroups = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], 'validation_groups', null, true);
+ }
+
+ $this->validator->validate($data, ['groups' => $validationGroups]);
+ }
+}
diff --git a/src/Validator/Exception/ValidationException.php b/src/Validator/Exception/ValidationException.php
new file mode 100644
index 00000000000..13c4777eaf4
--- /dev/null
+++ b/src/Validator/Exception/ValidationException.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Validator\Exception;
+
+use ApiPlatform\Core\Exception\RuntimeException;
+
+/**
+ * Thrown when a validation error occurs.
+ *
+ * @author Kévin Dunglas
+ */
+class ValidationException extends RuntimeException
+{
+}
diff --git a/src/Validator/ValidatorInterface.php b/src/Validator/ValidatorInterface.php
new file mode 100644
index 00000000000..0bba8ff649b
--- /dev/null
+++ b/src/Validator/ValidatorInterface.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Validator;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
+
+/**
+ * Validates an item using the Symfony validator component.
+ *
+ * @author Kévin Dunglas
+ */
+interface ValidatorInterface
+{
+ /**
+ * Validates an item.
+ *
+ * @param object $data
+ *
+ * @throws ValidationException
+ */
+ public function validate($data, array $context = []);
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 84f1db10117..72b061a0b37 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -35,6 +35,7 @@
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\TestBundle;
+use ApiPlatform\Core\Validator\ValidatorInterface;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use FOS\UserBundle\FOSUserBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
@@ -480,6 +481,7 @@ private function getPartialContainerBuilderProphecy($test = false)
'api_platform.subresource_operation_factory',
'api_platform.subresource_operation_factory.cached',
'api_platform.serializer_locator',
+ 'api_platform.validator',
];
foreach ($definitions as $definition) {
@@ -514,6 +516,7 @@ private function getPartialContainerBuilderProphecy($test = false)
ResourceMetadataFactoryInterface::class => 'api_platform.metadata.resource.metadata_factory',
PropertyNameCollectionFactoryInterface::class => 'api_platform.metadata.property.name_collection_factory',
PropertyMetadataFactoryInterface::class => 'api_platform.metadata.property.metadata_factory',
+ ValidatorInterface::class => 'api_platform.validator',
];
foreach ($aliases as $alias => $service) {
@@ -640,6 +643,7 @@ private function getBaseContainerBuilderProphecy()
'api_platform.http_cache.purger.varnish',
'api_platform.http_cache.purger.varnish_client',
'api_platform.http_cache.listener.response.add_tags',
+ 'api_platform.validator',
];
foreach ($definitions as $definition) {
@@ -653,6 +657,7 @@ private function getBaseContainerBuilderProphecy()
FilterEagerLoadingExtension::class => 'api_platform.doctrine.orm.query_extension.filter_eager_loading',
PaginationExtension::class => 'api_platform.doctrine.orm.query_extension.pagination',
OrderExtension::class => 'api_platform.doctrine.orm.query_extension.order',
+ ValidatorInterface::class => 'api_platform.validator',
];
foreach ($aliases as $alias => $service) {
diff --git a/tests/Bridge/Symfony/Validator/EventListener/ValidateListenerTest.php b/tests/Bridge/Symfony/Validator/EventListener/ValidateListenerTest.php
index 19d46ff347d..e94f3e28c95 100644
--- a/tests/Bridge/Symfony/Validator/EventListener/ValidateListenerTest.php
+++ b/tests/Bridge/Symfony/Validator/EventListener/ValidateListenerTest.php
@@ -28,6 +28,8 @@
/**
* @author Samuel ROZE
+ *
+ * @group legacy
*/
class ValidateListenerTest extends TestCase
{
@@ -157,6 +159,8 @@ public function testThrowsValidationExceptionWithViolationsFound()
$expectedValidationGroups = ['a', 'b', 'c'];
$violationsProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $violationsProphecy->rewind()->shouldBeCalled();
+ $violationsProphecy->valid()->shouldBeCalled();
$violationsProphecy->count()->willReturn(1)->shouldBeCalled();
$violations = $violationsProphecy->reveal();
diff --git a/tests/Bridge/Symfony/Validator/Exception/ValidationExceptionTest.php b/tests/Bridge/Symfony/Validator/Exception/ValidationExceptionTest.php
new file mode 100644
index 00000000000..44436e5d16d
--- /dev/null
+++ b/tests/Bridge/Symfony/Validator/Exception/ValidationExceptionTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace ApiPlatform\Core\Tests\Bridge\Symfony\Validator\Exception;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
+use ApiPlatform\Core\Exception\RuntimeException;
+use ApiPlatform\Core\Validator\Exception\ValidationException as MainValidationException;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationList;
+
+/**
+ * @author Kévin Dunglas
+ */
+class ValidationExceptionTest extends TestCase
+{
+ public function testToString()
+ {
+ $e = new ValidationException(new ConstraintViolationList([
+ new ConstraintViolation('message 1', '', [], '', '', 'invalid'),
+ new ConstraintViolation('message 2', '', [], '', 'foo', 'invalid'),
+ ]));
+ $this->assertInstanceOf(MainValidationException::class, $e);
+ $this->assertInstanceOf(RuntimeException::class, $e);
+ $this->assertInstanceOf(\RuntimeException::class, $e);
+
+ $this->assertEquals(<<__toString());
+ }
+}
diff --git a/tests/Bridge/Symfony/Validator/ValidatorTest.php b/tests/Bridge/Symfony/Validator/ValidatorTest.php
new file mode 100644
index 00000000000..6c034a4a33c
--- /dev/null
+++ b/tests/Bridge/Symfony/Validator/ValidatorTest.php
@@ -0,0 +1,116 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Bridge\Symfony\Validator;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Validator;
+use ApiPlatform\Core\Tests\Fixtures\DummyEntity;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface as SymfonyValidatorInterface;
+
+/**
+ * @author Kévin Dunglas
+ */
+class ValidatorTest extends TestCase
+{
+ public function testValid()
+ {
+ $data = new DummyEntity();
+
+ $constraintViolationListProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class);
+ $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled();
+ $symfonyValidator = $symfonyValidatorProphecy->reveal();
+
+ $validator = new Validator($symfonyValidator);
+ $validator->validate(new DummyEntity());
+ }
+
+ /**
+ * @expectedException \ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException
+ */
+ public function testInvalid()
+ {
+ $data = new DummyEntity();
+
+ $constraintViolationListProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $constraintViolationListProphecy->rewind()->shouldBeCalled();
+ $constraintViolationListProphecy->valid()->shouldBeCalled();
+ $constraintViolationListProphecy->count()->willReturn(2)->shouldBeCalled();
+
+ $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class);
+ $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled();
+ $symfonyValidator = $symfonyValidatorProphecy->reveal();
+
+ $validator = new Validator($symfonyValidator);
+ $validator->validate(new DummyEntity());
+ }
+
+ public function testGetGroupsFromCallable()
+ {
+ $data = new DummyEntity();
+ $expectedValidationGroups = ['a', 'b', 'c'];
+
+ $constraintViolationListProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class);
+ $symfonyValidatorProphecy->validate($data, null, $expectedValidationGroups)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled();
+ $symfonyValidator = $symfonyValidatorProphecy->reveal();
+
+ $validator = new Validator($symfonyValidator);
+ $validator->validate(new DummyEntity(), ['groups' => function ($data) use ($expectedValidationGroups): array {
+ return $data instanceof DummyEntity ? $expectedValidationGroups : [];
+ }]);
+ }
+
+ public function testGetGroupsFromService()
+ {
+ $data = new DummyEntity();
+
+ $constraintViolationListProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class);
+ $symfonyValidatorProphecy->validate($data, null, ['a', 'b', 'c'])->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled();
+ $symfonyValidator = $symfonyValidatorProphecy->reveal();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('groups_builder')->willReturn(true)->shouldBeCalled();
+ $containerProphecy->get('groups_builder')->willReturn(new class() {
+ public function __invoke($data): array
+ {
+ return $data instanceof DummyEntity ? ['a', 'b', 'c'] : [];
+ }
+ }
+ )->shouldBeCalled();
+
+ $validator = new Validator($symfonyValidator, $containerProphecy->reveal());
+ $validator->validate(new DummyEntity(), ['groups' => 'groups_builder']);
+ }
+
+ public function testValidatorWithScalarGroup()
+ {
+ $data = new DummyEntity();
+ $expectedValidationGroups = ['foo'];
+
+ $constraintViolationListProphecy = $this->prophesize(ConstraintViolationListInterface::class);
+ $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class);
+ $symfonyValidatorProphecy->validate($data, null, $expectedValidationGroups)->willreturn($constraintViolationListProphecy->reveal())->shouldBeCalled();
+ $symfonyValidator = $symfonyValidatorProphecy->reveal();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('foo')->willReturn(false)->shouldBeCalled();
+
+ $validator = new Validator($symfonyValidator, $containerProphecy->reveal());
+ $validator->validate(new DummyEntity(), ['groups' => 'foo']);
+ }
+}
diff --git a/tests/Validator/EventListener/ValidateListenerTest.php b/tests/Validator/EventListener/ValidateListenerTest.php
new file mode 100644
index 00000000000..dc27e068a2e
--- /dev/null
+++ b/tests/Validator/EventListener/ValidateListenerTest.php
@@ -0,0 +1,129 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Validator\EventListener;
+
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\DummyEntity;
+use ApiPlatform\Core\Validator\EventListener\ValidateListener;
+use ApiPlatform\Core\Validator\Exception\ValidationException;
+use ApiPlatform\Core\Validator\ValidatorInterface;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * @author Samuel ROZE
+ * @author Kévin Dunglas
+ *
+ * @group legacy
+ */
+class ValidateListenerTest extends TestCase
+{
+ public function testNotAnApiPlatformRequest()
+ {
+ $validatorProphecy = $this->prophesize(ValidatorInterface::class);
+ $validatorProphecy->validate()->shouldNotBeCalled();
+ $validator = $validatorProphecy->reveal();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create()->shouldNotBeCalled();
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+
+ $request = new Request();
+ $request->setMethod('POST');
+
+ $event = $this->prophesize(GetResponseForControllerResultEvent::class);
+ $event->getRequest()->willReturn($request)->shouldBeCalled();
+
+ $listener = new ValidateListener($validator, $resourceMetadataFactory);
+ $listener->onKernelView($event->reveal());
+ }
+
+ public function testValidatorIsCalled()
+ {
+ $data = new DummyEntity();
+ $expectedValidationGroups = ['a', 'b', 'c'];
+
+ $validatorProphecy = $this->prophesize(ValidatorInterface::class);
+ $validatorProphecy->validate($data, ['groups' => $expectedValidationGroups])->shouldBeCalled();
+ $validator = $validatorProphecy->reveal();
+
+ list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data);
+
+ $validationViewListener = new ValidateListener($validator, $resourceMetadataFactory);
+ $validationViewListener->onKernelView($event);
+ }
+
+ public function testDoNotCallWhenReceiveFlagIsFalse()
+ {
+ $data = new DummyEntity();
+ $expectedValidationGroups = ['a', 'b', 'c'];
+
+ $validatorProphecy = $this->prophesize(ValidatorInterface::class);
+ $validatorProphecy->validate($data, ['groups' => $expectedValidationGroups])->shouldNotBeCalled();
+ $validator = $validatorProphecy->reveal();
+
+ list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data, false);
+
+ $validationViewListener = new ValidateListener($validator, $resourceMetadataFactory);
+ $validationViewListener->onKernelView($event);
+ }
+
+ /**
+ * @expectedException \ApiPlatform\Core\Validator\Exception\ValidationException
+ */
+ public function testThrowsValidationExceptionWithViolationsFound()
+ {
+ $data = new DummyEntity();
+ $expectedValidationGroups = ['a', 'b', 'c'];
+
+ $validatorProphecy = $this->prophesize(ValidatorInterface::class);
+ $validatorProphecy->validate($data, ['groups' => $expectedValidationGroups])->willThrow(new ValidationException())->shouldBeCalled();
+ $validator = $validatorProphecy->reveal();
+
+ list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data);
+
+ $validationViewListener = new ValidateListener($validator, $resourceMetadataFactory);
+ $validationViewListener->onKernelView($event);
+ }
+
+ private function createEventObject($expectedValidationGroups, $data, bool $receive = true): array
+ {
+ $resourceMetadata = new ResourceMetadata(null, null, null, [
+ 'create' => ['validation_groups' => $expectedValidationGroups],
+ ]);
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ if ($receive) {
+ $resourceMetadataFactoryProphecy->create(DummyEntity::class)->willReturn($resourceMetadata)->shouldBeCalled();
+ }
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+
+ $kernel = $this->prophesize(HttpKernelInterface::class)->reveal();
+ $request = new Request([], [], [
+ '_api_resource_class' => DummyEntity::class,
+ '_api_item_operation_name' => 'create',
+ '_api_format' => 'json',
+ '_api_mime_type' => 'application/json',
+ '_api_receive' => $receive,
+ ]);
+
+ $request->setMethod(Request::METHOD_POST);
+ $event = new GetResponseForControllerResultEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $data);
+
+ return [$resourceMetadataFactory, $event];
+ }
+}