diff --git a/composer.json b/composer.json index b8cc1465a56..e6578f2aff0 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "api-platform/core", "type": "library", - "description": "Build a fully-featured hypermedia or GraphQL API in minutes", - "keywords": ["REST", "GraphQL", "API", "JSON-LD", "Hydra", "JSONAPI", "OpenAPI", "HAL", "Swagger"], + "description": "The ultimate solution to create web APIs.", + "keywords": ["REST", "API", "JSON", "JSON-LD", "Hydra", "Swagger", "HAL"], "homepage": "https://api-platform.com", "license": "MIT", "authors": [ @@ -17,10 +17,10 @@ "doctrine/inflector": "^1.0", "psr/cache": "^1.0", "psr/container": "^1.0", - "symfony/http-foundation": "^3.4 || ^4.0", - "symfony/http-kernel": "^3.4 || ^4.0", - "symfony/property-access": "^3.4 || ^4.0", - "symfony/property-info": "^3.4 || ^4.0", + "symfony/http-foundation": "^3.1 || ^4.0", + "symfony/http-kernel": "^2.7 || ^3.0 || ^4.0", + "symfony/property-access": "^2.7 || ^3.0 || ^4.0", + "symfony/property-info": "^3.3.11 || ^4.0", "symfony/serializer": "^4.1", "symfony/web-link": "^4.1", "willdurand/negotiation": "^2.0.3" @@ -34,54 +34,46 @@ "behatch/contexts": "3.1.0", "doctrine/annotations": "^1.2", "doctrine/doctrine-bundle": "^1.8", - "doctrine/doctrine-cache-bundle": "^1.3.5", - "doctrine/mongodb-odm": "^2.0.0@beta", - "doctrine/mongodb-odm-bundle": "^4.0.0@beta", - "doctrine/orm": "^2.6.3", - "elasticsearch/elasticsearch": "^6.0", - "friendsofsymfony/user-bundle": "dev-fix-mongodb-mapping", + "doctrine/orm": "^2.5.2", + "friendsofsymfony/user-bundle": "^2.1", "guzzlehttp/guzzle": "^6.0", "justinrainbow/json-schema": "^5.0", "nelmio/api-doc-bundle": "^2.13.3", "php-mock/php-mock-phpunit": "^2.0", "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", - "phpdocumentor/type-resolver": "^0.3 || ^0.4", - "phpspec/prophecy": "^1.8", - "phpunit/phpunit": "^7.5.2", + "phpdocumentor/type-resolver": "^0.2.1 || ^0.3 || 0.4", + "phpunit/phpunit": "^6.1", "psr/log": "^1.0", "ramsey/uuid": "^3.7", "ramsey/uuid-doctrine": "^1.4", - "symfony/asset": "^3.4 || ^4.0", - "symfony/cache": "^3.4 || ^4.0", + "symfony/asset": "^3.3 || ^4.0", + "symfony/cache": "^3.3 || ^4.0", "symfony/config": "^3.4 || ^4.0", "symfony/console": "^3.4 || ^4.0", - "symfony/debug": "^3.4 || ^4.0", + "symfony/debug": "^2.8 || ^3.0 || ^4.0", "symfony/dependency-injection": "^3.4 || ^4.0", - "symfony/doctrine-bridge": "^3.4 || ^4.0", - "symfony/event-dispatcher": "^3.4 || ^4.0", - "symfony/expression-language": "^3.4 || ^4.0", - "symfony/finder": "^3.4 || ^4.0", - "symfony/form": "^3.4 || ^4.0", - "symfony/framework-bundle": "^4.2", + "symfony/doctrine-bridge": "^2.8.12 || ^3.0 || ^4.0", + "symfony/expression-language": "^2.8 || ^3.0 || ^4.0", + "symfony/finder": "^3.3 || ^4.0", + "symfony/form": "^3.3 || ^4.0", + "symfony/framework-bundle": "^3.3 || ^4.0", + "symfony/mercure": "*", "symfony/mercure-bundle": "*", - "symfony/messenger": "^4.2", - "symfony/phpunit-bridge": "^3.4.5 || ^4.0.5", - "symfony/routing": "^3.4 || ^4.0", - "symfony/security": "^3.4 || ^4.0", + "symfony/messenger": "^4.1", + "symfony/phpunit-bridge": "^3.3 || ^4.0", + "symfony/routing": "^3.3 || ^4.0", + "symfony/security": "^3.0 || ^4.0", "symfony/security-bundle": "^3.4 || ^4.0", "symfony/twig-bundle": "^3.4 || ^4.0", - "symfony/validator": "^3.4 || ^4.0", - "symfony/web-profiler-bundle": "^3.4 || ^4.0", - "symfony/yaml": "^3.4 || ^4.0", - "webonyx/graphql-php": ">=0.13 <1.0" + "symfony/validator": "^3.3 || ^4.0", + "symfony/web-profiler-bundle": "^3.3 || ^4.0", + "symfony/yaml": "^3.3 || ^4.0", + "webonyx/graphql-php": ">=0.12 <1.0" }, "conflict": { - "doctrine/common": "<2.7", - "doctrine/mongodb-odm": "<2.0" + "symfony/dependency-injection": "<3.4" }, "suggest": { - "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", - "elasticsearch/elasticsearch": "To support Elasticsearch.", "friendsofsymfony/user-bundle": "To use the FOSUserBundle bridge.", "guzzlehttp/guzzle": "To use the HTTP cache invalidation system.", "phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.", @@ -89,6 +81,7 @@ "ramsey/uuid": "To support Ramsey's UUID identifiers.", "symfony/cache": "To have metadata caching when using Symfony integration.", "symfony/config": "To load XML configuration files.", + "symfony/event-dispatcher": "To handle events", "symfony/expression-language": "To use authorization features.", "symfony/security": "To use authorization features.", "symfony/twig-bundle": "To use the Swagger UI integration.", @@ -103,17 +96,10 @@ }, "extra": { "branch-alias": { - "dev-master": "2.5.x-dev" + "dev-master": "2.4.x-dev" } }, "config": { "sort-packages": true - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/alanpoulain/FOSUserBundle", - "no-api": true - } - ] + } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 7a23930994f..ebc4221cb66 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -139,6 +139,7 @@ + @@ -149,6 +150,7 @@ + @@ -156,6 +158,7 @@ + @@ -164,6 +167,7 @@ + @@ -171,12 +175,15 @@ + + + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index bdc491d1b38..d9dd6e70b61 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -18,6 +18,7 @@ %api_platform.collection.pagination.enabled% + @@ -27,6 +28,7 @@ + @@ -34,6 +36,7 @@ + diff --git a/src/Event/PostDeserializeEvent.php b/src/Event/PostDeserializeEvent.php new file mode 100644 index 00000000000..9a85bbdb635 --- /dev/null +++ b/src/Event/PostDeserializeEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PostDeserializeEvent extends Event +{ + private $data; + + public function __construct($controllerResult) + { + $this->data = $controllerResult; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PostReadEvent.php b/src/Event/PostReadEvent.php new file mode 100644 index 00000000000..cf987d308f6 --- /dev/null +++ b/src/Event/PostReadEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PostReadEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PostSerializeEvent.php b/src/Event/PostSerializeEvent.php new file mode 100644 index 00000000000..354a68557b4 --- /dev/null +++ b/src/Event/PostSerializeEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PostSerializeEvent extends Event +{ + private $data; + + public function __construct($object) + { + $this->data = $object; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PostValidateEvent.php b/src/Event/PostValidateEvent.php new file mode 100644 index 00000000000..7a9a274d375 --- /dev/null +++ b/src/Event/PostValidateEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +class PostValidateEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PostWriteEvent.php b/src/Event/PostWriteEvent.php new file mode 100644 index 00000000000..914fafb5f9c --- /dev/null +++ b/src/Event/PostWriteEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PostWriteEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PreDeserializeEvent.php b/src/Event/PreDeserializeEvent.php new file mode 100644 index 00000000000..a1df5494264 --- /dev/null +++ b/src/Event/PreDeserializeEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PreDeserializeEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PreReadEvent.php b/src/Event/PreReadEvent.php new file mode 100644 index 00000000000..2c61685e97a --- /dev/null +++ b/src/Event/PreReadEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PreReadEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PreSerializeEvent.php b/src/Event/PreSerializeEvent.php new file mode 100644 index 00000000000..f2fcc91e3b9 --- /dev/null +++ b/src/Event/PreSerializeEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PreSerializeEvent extends Event +{ + private $data; + + public function __construct($object) + { + $this->data = $object; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PreValidateEvent.php b/src/Event/PreValidateEvent.php new file mode 100644 index 00000000000..91a4be15baf --- /dev/null +++ b/src/Event/PreValidateEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +class PreValidateEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Event/PreWriteEvent.php b/src/Event/PreWriteEvent.php new file mode 100644 index 00000000000..ad68047a0ae --- /dev/null +++ b/src/Event/PreWriteEvent.php @@ -0,0 +1,36 @@ + + * + * 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\Event; + +use Symfony\Component\EventDispatcher\Event; + +final class PreWriteEvent extends Event +{ + private $data; + + public function __construct($data) + { + $this->data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/EventListener/AddFormatListener.php b/src/EventListener/AddFormatListener.php index 72e53713dda..69b512258f7 100644 --- a/src/EventListener/AddFormatListener.php +++ b/src/EventListener/AddFormatListener.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Core\EventListener; -use ApiPlatform\Core\Api\FormatMatcher; use ApiPlatform\Core\Api\FormatsProviderInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Util\RequestAttributesExtractor; @@ -24,7 +23,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * Chooses the format to use according to the Accept header and supported formats. + * Chooses the format to user according to the Accept header and supported formats. * * @author Kévin Dunglas */ @@ -34,7 +33,6 @@ final class AddFormatListener private $formats = []; private $mimeTypes; private $formatsProvider; - private $formatMatcher; /** * @throws InvalidArgumentException @@ -45,18 +43,20 @@ public function __construct(Negotiator $negotiator, /* FormatsProviderInterface if (\is_array($formatsProvider)) { @trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED); $this->formats = $formatsProvider; - } else { - if (!$formatsProvider instanceof FormatsProviderInterface) { - throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class)); - } - $this->formatsProvider = $formatsProvider; + return; + } + if (!$formatsProvider instanceof FormatsProviderInterface) { + throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class)); } + + $this->formatsProvider = $formatsProvider; } /** * Sets the applicable format to the HttpFoundation Request. * + * * @throws NotFoundHttpException * @throws NotAcceptableHttpException */ @@ -70,9 +70,9 @@ public function onKernelRequest(GetResponseEvent $event) if (null !== $this->formatsProvider) { $this->formats = $this->formatsProvider->getFormatsFromAttributes(RequestAttributesExtractor::extractAttributes($request)); } - $this->formatMatcher = new FormatMatcher($this->formats); $this->populateMimeTypes(); + $this->addRequestFormats($request, $this->formats); // Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format) @@ -88,11 +88,11 @@ public function onKernelRequest(GetResponseEvent $event) /** @var string|null $accept */ $accept = $request->headers->get('Accept'); if (null !== $accept) { - if (null === $mediaType = $this->negotiator->getBest($accept, $mimeTypes)) { + if (null === $acceptHeader = $this->negotiator->getBest($accept, $mimeTypes)) { throw $this->getNotAcceptableHttpException($accept, $mimeTypes); } - $request->setRequestFormat($this->formatMatcher->getFormat($mediaType->getType())); + $request->setRequestFormat($request->getFormat($acceptHeader->getType())); return; } @@ -118,14 +118,12 @@ public function onKernelRequest(GetResponseEvent $event) } /** - * Adds the supported formats to the request. - * - * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work. + * Adds API formats to the HttpFoundation Request. */ private function addRequestFormats(Request $request, array $formats) { foreach ($formats as $format => $mimeTypes) { - $request->setFormat($format, (array) $mimeTypes); + $request->setFormat($format, $mimeTypes); } } diff --git a/src/EventListener/DeserializeListener.php b/src/EventListener/DeserializeListener.php index 1fff5848178..d890b03db7d 100644 --- a/src/EventListener/DeserializeListener.php +++ b/src/EventListener/DeserializeListener.php @@ -13,11 +13,14 @@ namespace ApiPlatform\Core\EventListener; -use ApiPlatform\Core\Api\FormatMatcher; use ApiPlatform\Core\Api\FormatsProviderInterface; +use ApiPlatform\Core\Event\PostDeserializeEvent; +use ApiPlatform\Core\Event\PreDeserializeEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; @@ -35,25 +38,27 @@ final class DeserializeListener private $serializerContextBuilder; private $formats = []; private $formatsProvider; - private $formatMatcher; + private $dispatcher; /** * @throws InvalidArgumentException */ - public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */ $formatsProvider) + public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */ $formatsProvider, EventDispatcherInterface $dispatcher = null) { $this->serializer = $serializer; $this->serializerContextBuilder = $serializerContextBuilder; if (\is_array($formatsProvider)) { @trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED); $this->formats = $formatsProvider; - } else { - if (!$formatsProvider instanceof FormatsProviderInterface) { - throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class)); - } - $this->formatsProvider = $formatsProvider; + return; + } + if (!$formatsProvider instanceof FormatsProviderInterface) { + throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class)); } + + $this->formatsProvider = $formatsProvider; + $this->dispatcher = $dispatcher; } /** @@ -64,10 +69,9 @@ public function onKernelRequest(GetResponseEvent $event) $request = $event->getRequest(); $method = $request->getMethod(); if ( - 'DELETE' === $method - || $request->isMethodSafe(false) + $request->isMethodSafe(false) + || 'DELETE' === $method || !($attributes = RequestAttributesExtractor::extractAttributes($request)) - || false === ($attributes['input_class'] ?? null) || !$attributes['receive'] || ( '' === ($requestContent = $request->getContent()) @@ -80,7 +84,6 @@ public function onKernelRequest(GetResponseEvent $event) if (null !== $this->formatsProvider) { $this->formats = $this->formatsProvider->getFormatsFromAttributes($attributes); } - $this->formatMatcher = new FormatMatcher($this->formats); $format = $this->getFormat($request); $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); @@ -93,17 +96,26 @@ public function onKernelRequest(GetResponseEvent $event) $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $data; } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::PRE_DESERIALIZE, new PreDeserializeEvent($requestContent)); + } + $request->attributes->set( 'data', $this->serializer->deserialize( $requestContent, $attributes['input_class'], $format, $context ) ); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::POST_DESERIALIZE, new PostDeserializeEvent($requestContent)); + } } /** * Extracts the format from the Content-Type header and check that it is supported. * + * * @throws NotAcceptableHttpException */ private function getFormat(Request $request): string @@ -116,7 +128,7 @@ private function getFormat(Request $request): string throw new NotAcceptableHttpException('The "Content-Type" header must exist.'); } - $format = $this->formatMatcher->getFormat($contentType); + $format = $request->getFormat($contentType); if (null === $format || !isset($this->formats[$format])) { $supportedMimeTypes = []; foreach ($this->formats as $mimeTypes) { diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php index 80fb62d439f..c685cb616e5 100644 --- a/src/EventListener/ReadListener.php +++ b/src/EventListener/ReadListener.php @@ -17,13 +17,16 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\OperationDataProviderTrait; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; +use ApiPlatform\Core\Event\PostReadEvent; +use ApiPlatform\Core\Event\PreReadEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; use ApiPlatform\Core\Util\RequestParser; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -37,14 +40,17 @@ final class ReadListener use OperationDataProviderTrait; private $serializerContextBuilder; + /** @var EventDispatcherInterface */ + private $dispatcher; - public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null) + public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, EventDispatcherInterface $dispatcher = null) { $this->collectionDataProvider = $collectionDataProvider; $this->itemDataProvider = $itemDataProvider; $this->subresourceDataProvider = $subresourceDataProvider; $this->serializerContextBuilder = $serializerContextBuilder; $this->identifierConverter = $identifierConverter; + $this->dispatcher = $dispatcher; } /** @@ -89,6 +95,10 @@ public function onKernelRequest(GetResponseEvent $event) try { $identifiers = $this->extractIdentifiers($request->attributes->all(), $attributes); + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::PRE_READ, new PreReadEvent($data)); + } + if (isset($attributes['item_operation_name'])) { $data = $this->getItemData($identifiers, $attributes, $context); } elseif (isset($attributes['subresource_operation_name'])) { @@ -103,6 +113,10 @@ public function onKernelRequest(GetResponseEvent $event) throw new NotFoundHttpException('Not found, because of an invalid identifier configuration', $e); } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::POST_READ, new PostReadEvent($data)); + } + if (null === $data) { throw new NotFoundHttpException('Not Found'); } diff --git a/src/EventListener/SerializeListener.php b/src/EventListener/SerializeListener.php index 803c25296cc..28be933f852 100644 --- a/src/EventListener/SerializeListener.php +++ b/src/EventListener/SerializeListener.php @@ -13,12 +13,14 @@ namespace ApiPlatform\Core\EventListener; +use ApiPlatform\Core\Event\PostSerializeEvent; +use ApiPlatform\Core\Event\PreSerializeEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Serializer\ResourceList; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; @@ -35,10 +37,14 @@ final class SerializeListener private $serializer; private $serializerContextBuilder; - public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder) + /** @var EventDispatcherInterface */ + private $dispatcher; + + public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, EventDispatcherInterface $dispatcher = null) { $this->serializer = $serializer; $this->serializerContextBuilder = $serializerContextBuilder; + $this->dispatcher = $dispatcher; } /** @@ -49,7 +55,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $controllerResult = $event->getControllerResult(); $request = $event->getRequest(); - if ($controllerResult instanceof Response || !$request->attributes->getBoolean('_api_respond', true)) { + if ($controllerResult instanceof Response) { return; } @@ -59,43 +65,32 @@ public function onKernelView(GetResponseForControllerResultEvent $event) return; } - $request->attributes->set('_api_respond', true); $context = $this->serializerContextBuilder->createFromRequest($request, true, $attributes); - - if (isset($context['output_class'])) { - if (false === $context['output_class']) { - // If the output class is explicitly set to false, the response must be empty - $event->setControllerResult(''); - - return; - } - - $context['resource_class'] = $context['output_class']; - } - if ($included = $request->attributes->get('_api_included')) { $context['api_included'] = $included; } $resources = new ResourceList(); $context['resources'] = &$resources; - - $resourcesToPush = new ResourceList(); - $context['resources_to_push'] = &$resourcesToPush; + if (isset($context['output_class'])) { + $context['resource_class'] = $context['output_class']; + } $request->attributes->set('_api_normalization_context', $context); - $event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context)); - - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); - if (!\count($resourcesToPush)) { - return; + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::PRE_SERIALIZE, new PreSerializeEvent($controllerResult)); } - $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); - foreach ($resourcesToPush as $resourceToPush) { - $linkProvider = $linkProvider->withLink(new Link('preload', $resourceToPush)); + $serializedResult = $this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::POST_SERIALIZE, new PostSerializeEvent($serializedResult)); } - $request->attributes->set('_links', $linkProvider); + + $event->setControllerResult($serializedResult); + + $request->attributes->set('_api_respond', true); + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); } /** @@ -107,6 +102,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event) */ private function serializeRawData(GetResponseForControllerResultEvent $event, Request $request, $controllerResult) { + if (!$request->attributes->get('_api_respond')) { + return; + } + if (\is_object($controllerResult)) { $event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $request->attributes->get('_api_normalization_context', []))); diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index 7b222971e0a..8e536036d0d 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -15,11 +15,15 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\Event\PostWriteEvent; +use ApiPlatform\Core\Event\PreWriteEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Util\RequestAttributesExtractor; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; /** - * Bridges persistence and the API system. + * Bridges persistense and the API system. * * @author Kévin Dunglas * @author Baptiste Meyer @@ -28,11 +32,13 @@ final class WriteListener { private $dataPersister; private $iriConverter; + private $dispatcher; - public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null) + public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, EventDispatcherInterface $dispatcher = null) { $this->dataPersister = $dataPersister; $this->iriConverter = $iriConverter; + $this->dispatcher = $dispatcher; } /** @@ -50,12 +56,20 @@ public function onKernelView(GetResponseForControllerResultEvent $event) return; } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::PRE_WRITE, new PreWriteEvent($controllerResult)); + } + switch ($request->getMethod()) { case 'PUT': case 'PATCH': case 'POST': $persistResult = $this->dataPersister->persist($controllerResult); + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(Events::POST_WRITE, new PostWriteEvent($controllerResult)); + } + if (null === $persistResult) { @trigger_error(sprintf('Returning void from %s::persist() is deprecated since API Platform 2.3 and will not be supported in API Platform 3, an object should always be returned.', DataPersisterInterface::class), E_USER_DEPRECATED); } @@ -65,12 +79,17 @@ public function onKernelView(GetResponseForControllerResultEvent $event) // Controller result must be immutable for _api_write_item_iri // if it's class changed compared to the base class let's avoid calling the IriConverter // especially that the Output class could be a DTO that's not referencing any route - if (null !== $this->iriConverter && (false !== $attributes['output_class'] ?? null) && $attributes['resource_class'] === ($class = \get_class($controllerResult)) && $class === \get_class($event->getControllerResult())) { + if (null !== $this->iriConverter && (false !== $attributes['output_class'] ?? null) && \get_class($controllerResult) === \get_class($event->getControllerResult())) { $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult)); } break; case 'DELETE': $this->dataPersister->remove($controllerResult); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostWriteEvent::NAME, new PostWriteEvent($controllerResult)); + } + $event->setControllerResult(null); break; } diff --git a/src/Events.php b/src/Events.php new file mode 100644 index 00000000000..ff90019e914 --- /dev/null +++ b/src/Events.php @@ -0,0 +1,32 @@ + + * + * 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; + +final class Events +{ + const PRE_READ = 'api_platform.pre_read'; + const POST_READ = 'api_platform.post_read'; + + const PRE_DESERIALIZE = 'api_platform.pre_deserialize'; + const POST_DESERIALIZE = 'api_platform.post_deserialize'; + + const PRE_VALIDATE = 'api_platform.pre_validate'; + const POST_VALIDATE = 'api_platform.post_validate'; + + const PRE_WRITE = 'api_platform.pre_write'; + const POST_WRITE = 'api_platform.post_write'; + + const PRE_SERIALIZE = 'api_platform.pre_serialize'; + const POST_SERIALIZE = 'api_platform.post_serialize'; +} diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php index d1a6bcc7ea1..b9646c25eed 100644 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php @@ -17,6 +17,10 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\PaginatorInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; +use ApiPlatform\Core\Event\PostReadEvent; +use ApiPlatform\Core\Event\PostSerializeEvent; +use ApiPlatform\Core\Event\PreReadEvent; +use ApiPlatform\Core\Event\PreSerializeEvent; use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; use ApiPlatform\Core\GraphQl\Resolver\FieldsToAttributesTrait; use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait; @@ -25,6 +29,7 @@ use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use GraphQL\Error\Error; use GraphQL\Type\Definition\ResolveInfo; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -49,8 +54,9 @@ final class CollectionResolverFactory implements ResolverFactoryInterface private $requestStack; private $paginationEnabled; private $resourceMetadataFactory; + private $dispatcher; - public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false) + public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false, EventDispatcherInterface $dispatcher = null) { $this->subresourceDataProvider = $subresourceDataProvider; $this->collectionDataProvider = $collectionDataProvider; @@ -60,6 +66,7 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi $this->requestStack = $requestStack; $this->paginationEnabled = $paginationEnabled; $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->dispatcher = $dispatcher; } public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable @@ -84,20 +91,42 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, if (isset($rootClass, $source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_KEY])) { $rootResolvedFields = $this->identifiersExtractor->getIdentifiersFromItem(unserialize($source[ItemNormalizer::ITEM_KEY])); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreReadEvent::NAME, new PreReadEvent($rootResolvedFields)); + } + $subresource = $this->getSubresource($rootClass, $rootResolvedFields, array_keys($rootResolvedFields), $rootProperty, $resourceClass, true, $dataProviderContext); $collection = $subresource ?? []; } else { + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreReadEvent::NAME, new PreReadEvent([])); + } + $collection = $this->collectionDataProvider->getCollection($resourceClass, null, $dataProviderContext); } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostReadEvent::NAME, new PostReadEvent($collection)); + } + $this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, $operationName ?? 'query'); if (!$this->paginationEnabled) { $data = []; + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreSerializeEvent::NAME, new PreSerializeEvent($collection)); + } + foreach ($collection as $index => $object) { $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext); } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostSerializeEvent::NAME, new PostSerializeEvent($data)); + } + return $data; } @@ -117,6 +146,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, $data['totalCount'] = $totalItems; } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreSerializeEvent::NAME, new PreSerializeEvent($collection)); + } + foreach ($collection as $index => $object) { $data['edges'][$index] = [ 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext), @@ -124,6 +157,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, ]; } + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostSerializeEvent::NAME, new PostSerializeEvent($data['edges'])); + } + return $data; }; } diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php index 46f77aa08b5..8c6b68a8fa3 100644 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php @@ -15,6 +15,9 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\Event\PostWriteEvent; +use ApiPlatform\Core\Event\PreValidateEvent; +use ApiPlatform\Core\Event\PreWriteEvent; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\GraphQl\Resolver\FieldsToAttributesTrait; @@ -28,6 +31,7 @@ use ApiPlatform\Core\Validator\ValidatorInterface; use GraphQL\Error\Error; use GraphQL\Type\Definition\ResolveInfo; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -50,8 +54,9 @@ final class ItemMutationResolverFactory implements ResolverFactoryInterface private $resourceMetadataFactory; private $resourceAccessChecker; private $validator; + private $dispatcher; - public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, ValidatorInterface $validator = null) + public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, ValidatorInterface $validator = null, EventDispatcherInterface $dispatcher = null) { if (!$normalizer instanceof DenormalizerInterface) { throw new InvalidArgumentException(sprintf('The normalizer must implements the "%s" interface', DenormalizerInterface::class)); @@ -63,6 +68,7 @@ public function __construct(IriConverterInterface $iriConverter, DataPersisterIn $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceAccessChecker = $resourceAccessChecker; $this->validator = $validator; + $this->dispatcher = $dispatcher; } public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable @@ -103,21 +109,39 @@ public function __invoke(string $resourceClass = null, string $rootClass = null, $context = null === $item ? ['resource_class' => $resourceClass] : ['resource_class' => $resourceClass, 'object_to_populate' => $item]; $context += $resourceMetadata->getGraphqlAttribute($operationName, 'denormalization_context', [], true); $item = $this->normalizer->denormalize($args['input'], $resourceClass, ItemNormalizer::FORMAT, $context); + $this->dispatcher->dispatch(PreValidateEvent::class, new PreValidateEvent($item)); $this->validate($item, $info, $resourceMetadata, $operationName); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreWriteEvent::NAME, new PreWriteEvent($item)); + } + $persistResult = $this->dataPersister->persist($item); + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostWriteEvent::NAME, new PostWriteEvent($item)); + } + if (null === $persistResult) { @trigger_error(sprintf('Returning void from %s::persist() is deprecated since API Platform 2.3 and will not be supported in API Platform 3, an object should always be returned.', DataPersisterInterface::class), E_USER_DEPRECATED); } return $this->normalizer->normalize($persistResult ?? $item, ItemNormalizer::FORMAT, $normalizationContext) + $data; case 'delete': + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreWriteEvent::NAME, new PreWriteEvent($item)); + } + if ($item) { $this->dataPersister->remove($item); $data['id'] = $args['input']['id']; } else { $data['id'] = null; } + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostWriteEvent::NAME, new PostWriteEvent($item)); + } } return $data; diff --git a/src/GraphQl/Resolver/ItemResolver.php b/src/GraphQl/Resolver/ItemResolver.php index a06dbb05192..1cabf8d44ed 100644 --- a/src/GraphQl/Resolver/ItemResolver.php +++ b/src/GraphQl/Resolver/ItemResolver.php @@ -14,12 +14,17 @@ namespace ApiPlatform\Core\GraphQl\Resolver; use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Core\Event\PostReadEvent; +use ApiPlatform\Core\Event\PostSerializeEvent; +use ApiPlatform\Core\Event\PreReadEvent; +use ApiPlatform\Core\Event\PreSerializeEvent; use ApiPlatform\Core\Exception\ItemNotFoundException; use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use ApiPlatform\Core\Util\ClassInfoTrait; use GraphQL\Type\Definition\ResolveInfo; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** @@ -40,13 +45,15 @@ final class ItemResolver private $resourceAccessChecker; private $normalizer; private $resourceMetadataFactory; + private $dispatcher; - public function __construct(IriConverterInterface $iriConverter, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null) + public function __construct(IriConverterInterface $iriConverter, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, EventDispatcherInterface $dispatcher = null) { $this->iriConverter = $iriConverter; $this->normalizer = $normalizer; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceAccessChecker = $resourceAccessChecker; + $this->dispatcher = $dispatcher; } public function __invoke($source, $args, $context, ResolveInfo $info) @@ -62,7 +69,15 @@ public function __invoke($source, $args, $context, ResolveInfo $info) $baseNormalizationContext = ['attributes' => $this->fieldsToAttributes($info)]; try { + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreReadEvent::NAME, new PreReadEvent(null)); + } + $item = $this->iriConverter->getItemFromIri($args['id'], $baseNormalizationContext); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostReadEvent::NAME, new PostReadEvent($item)); + } } catch (ItemNotFoundException $e) { return null; } @@ -73,6 +88,16 @@ public function __invoke($source, $args, $context, ResolveInfo $info) $normalizationContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true); - return $this->normalizer->normalize($item, ItemNormalizer::FORMAT, $normalizationContext + $baseNormalizationContext); + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PreSerializeEvent::NAME, new PreSerializeEvent($item)); + } + + $normalizedObject = $this->normalizer->normalize($item, ItemNormalizer::FORMAT, $normalizationContext + $baseNormalizationContext); + + if (null !== $this->dispatcher) { + $this->dispatcher->dispatch(PostSerializeEvent::NAME, new PostSerializeEvent($normalizedObject)); + } + + return $normalizedObject; } } diff --git a/src/Validator/EventListener/ValidateListener.php b/src/Validator/EventListener/ValidateListener.php index 5bf3afd3485..f6705fdd8dc 100644 --- a/src/Validator/EventListener/ValidateListener.php +++ b/src/Validator/EventListener/ValidateListener.php @@ -14,9 +14,13 @@ namespace ApiPlatform\Core\Validator\EventListener; use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Core\Event\PostValidateEvent; +use ApiPlatform\Core\Event\PreValidateEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Util\RequestAttributesExtractor; use ApiPlatform\Core\Validator\ValidatorInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; /** @@ -28,11 +32,13 @@ final class ValidateListener { private $validator; private $resourceMetadataFactory; + private $eventDispatcher; - public function __construct(ValidatorInterface $validator, ResourceMetadataFactoryInterface $resourceMetadataFactory) + public function __construct(ValidatorInterface $validator, ResourceMetadataFactoryInterface $resourceMetadataFactory, EventDispatcherInterface $eventDispatcher = null) { $this->validator = $validator; $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->eventDispatcher = $eventDispatcher; } /** @@ -56,6 +62,14 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']); $validationGroups = $resourceMetadata->getOperationAttribute($attributes, 'validation_groups', null, true); + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(Events::PRE_VALIDATE, new PreValidateEvent($data)); + } + $this->validator->validate($data, ['groups' => $validationGroups]); + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(Events::POST_VALIDATE, new PostValidateEvent($data)); + } } } diff --git a/tests/EventListener/AddFormatListenerTest.php b/tests/EventListener/AddFormatListenerTest.php index 61da5fa819d..b7b6cdafc0c 100644 --- a/tests/EventListener/AddFormatListenerTest.php +++ b/tests/EventListener/AddFormatListenerTest.php @@ -14,10 +14,13 @@ namespace ApiPlatform\Core\Tests\EventListener; use ApiPlatform\Core\Api\FormatsProviderInterface; +use ApiPlatform\Core\Event\PostAddFormatEvent; +use ApiPlatform\Core\Event\PreAddFormatEvent; use ApiPlatform\Core\EventListener\AddFormatListener; +use ApiPlatform\Core\Events; use Negotiation\Negotiator; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; @@ -33,28 +36,29 @@ public function testNoResourceClass() $request = new Request(); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); + $formatsProviderProphecy->getFormatsFromAttributes()->shouldNotBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); - $this->assertNull($request->getRequestFormat(null)); + $this->assertNull($request->getFormat('application/vnd.notexist')); } public function testSupportedRequestFormat() { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->setRequestFormat('xml'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['xml' => ['text/xml']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['xml' => ['text/xml']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -69,11 +73,11 @@ public function testRespondFlag() $request->setRequestFormat('xml'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['xml' => ['text/xml']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['xml' => ['text/xml']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -87,15 +91,14 @@ public function testUnsupportedRequestFormat() $this->expectException(NotAcceptableHttpException::class); $this->expectExceptionMessage('Requested format "text/xml" is not supported. Supported MIME types are "application/json".'); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->setRequestFormat('xml'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); - $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -105,15 +108,15 @@ public function testUnsupportedRequestFormat() public function testSupportedAcceptHeader() { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml, application/json;q=0.9, */*;q=0.8'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -121,35 +124,17 @@ public function testSupportedAcceptHeader() $this->assertSame('json', $request->getRequestFormat()); } - public function testSupportedAcceptHeaderSymfonyBuiltInFormat() - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); - $request->headers->set('Accept', 'application/json'); - - $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['jsonld' => ['application/ld+json', 'application/json']]); - - $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('jsonld', $request->getRequestFormat()); - } - public function testAcceptAllHeader() { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -163,34 +148,15 @@ public function testUnsupportedAcceptHeader() $this->expectException(NotAcceptableHttpException::class); $this->expectExceptionMessage('Requested format "text/html, application/xhtml+xml, application/xml;q=0.9" is not supported. Supported MIME types are "application/octet-stream", "application/json".'); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']]); - - $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testUnsupportedAcceptHeaderSymfonyBuiltInFormat() - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "text/xml" is not supported. Supported MIME types are "application/json".'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); - $request->headers->set('Accept', 'text/xml'); - - $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['binary' => ['application/octet-stream'], 'json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -201,15 +167,15 @@ public function testInvalidAcceptHeader() $this->expectException(NotAcceptableHttpException::class); $this->expectExceptionMessage('Requested format "invalid" is not supported. Supported MIME types are "application/json".'); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->headers->set('Accept', 'invalid'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -217,16 +183,16 @@ public function testInvalidAcceptHeader() public function testAcceptHeaderTakePrecedenceOverRequestFormat() { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); + $request = new Request([], [], ['_api_resource_class' => 'Foo']); $request->headers->set('Accept', 'application/json'); $request->setRequestFormat('xml'); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['xml' => ['application/xml'], 'json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['xml' => ['application/xml'], 'json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); @@ -239,14 +205,14 @@ public function testInvalidRouteFormat() $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Format "invalid" is not supported'); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', '_format' => 'invalid']); + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_format' => 'invalid']); $eventProphecy = $this->prophesize(GetResponseEvent::class); - $eventProphecy->getRequest()->willReturn($request); + $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(Argument::any())->willReturn(['json' => ['application/json']]); + $formatsProviderProphecy->getFormatsFromAttributes([])->willReturn(['json' => ['application/json']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php index de32a3a6991..91e02d13b36 100644 --- a/tests/EventListener/DeserializeListenerTest.php +++ b/tests/EventListener/DeserializeListenerTest.php @@ -14,10 +14,14 @@ namespace ApiPlatform\Core\Tests\EventListener; use ApiPlatform\Core\Api\FormatsProviderInterface; +use ApiPlatform\Core\Event\PostDeserializeEvent; +use ApiPlatform\Core\Event\PreDeserializeEvent; use ApiPlatform\Core\EventListener\DeserializeListener; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; @@ -168,7 +172,11 @@ public function testDeserialize(string $method, bool $populateObject) $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled(); - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Events::PRE_DESERIALIZE, new PreDeserializeEvent($request->getContent()))->shouldBeCalled(self::once()); + $eventDispatcher->dispatch(Events::POST_DESERIALIZE, new PostDeserializeEvent($request->getContent()))->shouldBeCalled(self::once()); + + $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal(), $eventDispatcher->reveal()); $listener->onKernelRequest($eventProphecy->reveal()); } diff --git a/tests/EventListener/ReadListenerTest.php b/tests/EventListener/ReadListenerTest.php index e259bf70010..f0767583469 100644 --- a/tests/EventListener/ReadListenerTest.php +++ b/tests/EventListener/ReadListenerTest.php @@ -16,12 +16,16 @@ use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; +use ApiPlatform\Core\Event\PostReadEvent; +use ApiPlatform\Core\Event\PreReadEvent; use ApiPlatform\Core\EventListener\ReadListener; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Exception\InvalidIdentifierException; use ApiPlatform\Core\Exception\RuntimeException; use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -167,7 +171,11 @@ public function testRetrieveItem() $event = $this->prophesize(GetResponseEvent::class); $event->getRequest()->willReturn($request)->shouldBeCalled(); - $listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal(), $subresourceDataProvider->reveal(), null, $identifierConverter->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Events::PRE_READ, new PreReadEvent([]))->shouldBeCalled(self::once()); + $eventDispatcher->dispatch(Events::POST_READ, new PostReadEvent($data))->shouldBeCalled(self::once()); + + $listener = new ReadListener($collectionDataProvider->reveal(), $itemDataProvider->reveal(), $subresourceDataProvider->reveal(), null, $identifierConverter->reveal(), $eventDispatcher->reveal()); $listener->onKernelRequest($event->reveal()); $this->assertSame($data, $request->attributes->get('data')); diff --git a/tests/EventListener/SerializeListenerTest.php b/tests/EventListener/SerializeListenerTest.php index b6f43acadd7..ae876ebfe7a 100644 --- a/tests/EventListener/SerializeListenerTest.php +++ b/tests/EventListener/SerializeListenerTest.php @@ -13,11 +13,15 @@ namespace ApiPlatform\Core\Tests\EventListener; +use ApiPlatform\Core\Event\PostSerializeEvent; +use ApiPlatform\Core\Event\PreSerializeEvent; use ApiPlatform\Core\EventListener\SerializeListener; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Serializer\ResourceList; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; @@ -90,15 +94,21 @@ public function testSerializeCollectionOperation() $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); $request->setRequestFormat('xml'); + $data = new \stdClass(); + $eventProphecy = $this->prophesize(GetResponseForControllerResultEvent::class); - $eventProphecy->getControllerResult()->willReturn(new \stdClass())->shouldBeCalled(); + $eventProphecy->getControllerResult()->willReturn($data)->shouldBeCalled(); $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); $eventProphecy->setControllerResult('bar')->shouldBeCalled(); $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), true, Argument::type('array'))->willReturn($expectedContext)->shouldBeCalled(); - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Events::PRE_SERIALIZE, new PreSerializeEvent($data))->shouldBeCalled(self::once()); + $eventDispatcher->dispatch(Events::POST_SERIALIZE, new PostSerializeEvent('bar'))->shouldBeCalled(self::once()); + + $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $eventDispatcher->reveal()); $listener->onKernelView($eventProphecy->reveal()); } diff --git a/tests/EventListener/WriteListenerTest.php b/tests/EventListener/WriteListenerTest.php index 49fde689b04..d8affb8c3e6 100644 --- a/tests/EventListener/WriteListenerTest.php +++ b/tests/EventListener/WriteListenerTest.php @@ -15,11 +15,15 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\Event\PostWriteEvent; +use ApiPlatform\Core\Event\PreWriteEvent; use ApiPlatform\Core\EventListener\WriteListener; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -127,7 +131,11 @@ public function testOnKernelViewWithControllerResultAndPersistWithImmutableResou $request->setMethod($httpMethod); $request->attributes->set(sprintf('_api_%s_operation_name', 'POST' === $httpMethod ? 'collection' : 'item'), strtolower($httpMethod)); - (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Events::PRE_WRITE, new PreWriteEvent($event->getControllerResult()))->shouldBeCalled(self::once()); + $eventDispatcher->dispatch(Events::POST_WRITE, new PostWriteEvent($event->getControllerResult()))->shouldBeCalled(self::once()); + + (new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal(), $eventDispatcher->reveal()))->onKernelView($event); $this->assertSame($dummy2, $event->getControllerResult()); $this->assertEquals('/dummy/1', $request->attributes->get('_api_write_item_iri')); diff --git a/tests/Validator/EventListener/ValidateListenerTest.php b/tests/Validator/EventListener/ValidateListenerTest.php index 6ed24118185..1b1f5ccd640 100644 --- a/tests/Validator/EventListener/ValidateListenerTest.php +++ b/tests/Validator/EventListener/ValidateListenerTest.php @@ -13,6 +13,9 @@ namespace ApiPlatform\Core\Tests\Validator\EventListener; +use ApiPlatform\Core\Event\PostValidateEvent; +use ApiPlatform\Core\Event\PreValidateEvent; +use ApiPlatform\Core\Events; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Tests\Fixtures\DummyEntity; @@ -20,6 +23,7 @@ use ApiPlatform\Core\Validator\Exception\ValidationException; use ApiPlatform\Core\Validator\ValidatorInterface; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -63,7 +67,11 @@ public function testValidatorIsCalled() list($resourceMetadataFactory, $event) = $this->createEventObject($expectedValidationGroups, $data); - $validationViewListener = new ValidateListener($validator, $resourceMetadataFactory); + $eventDispatcher = $this->prophesize(EventDispatcher::class); + $eventDispatcher->dispatch(Events::PRE_VALIDATE, new PreValidateEvent($data))->shouldBeCalled(self::once()); + $eventDispatcher->dispatch(Events::POST_VALIDATE, new PostValidateEvent($data))->shouldBeCalled(self::once()); + + $validationViewListener = new ValidateListener($validator, $resourceMetadataFactory, $eventDispatcher->reveal()); $validationViewListener->onKernelView($event); }