From 1c01a8bceb175ba7c14093f9caa3de7213531f82 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 19 Aug 2025 08:31:32 +0200 Subject: [PATCH 1/2] feat(symfony): stop watch system provider/processor --- .../Processor/AddLinkHeaderProcessor.php | 8 ++++- src/State/Processor/RespondProcessor.php | 9 +++++- src/State/Processor/SerializeProcessor.php | 12 ++++++- src/State/Processor/WriteProcessor.php | 7 ++++- .../Provider/ContentNegotiationProvider.php | 7 ++++- src/State/Provider/DeserializeProvider.php | 12 +++++-- src/State/Provider/ParameterProvider.php | 7 ++++- src/State/Provider/ReadProvider.php | 9 +++++- src/State/StopwatchAwareInterface.php | 31 +++++++++++++++++++ src/State/StopwatchAwareTrait.php | 29 +++++++++++++++++ .../ApiPlatformExtension.php | 27 ++++++++++++++++ 11 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 src/State/StopwatchAwareInterface.php create mode 100644 src/State/StopwatchAwareTrait.php diff --git a/src/State/Processor/AddLinkHeaderProcessor.php b/src/State/Processor/AddLinkHeaderProcessor.php index 2f58e922351..036213374e9 100644 --- a/src/State/Processor/AddLinkHeaderProcessor.php +++ b/src/State/Processor/AddLinkHeaderProcessor.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\WebLink\HttpHeaderSerializer; @@ -24,8 +26,10 @@ * * @implements ProcessorInterface */ -final class AddLinkHeaderProcessor implements ProcessorInterface +final class AddLinkHeaderProcessor implements ProcessorInterface, StopwatchAwareInterface { + use StopwatchAwareTrait; + /** * @param ProcessorInterface $decorated */ @@ -44,11 +48,13 @@ public function process(mixed $data, Operation $operation, array $uriVariables = return $response; } + $this->stopwatch?->start('api_platform.processor.add_link_header'); // We add our header here as Symfony does it only for the main Request and we want it to be done on errors (sub-request) as well $linksProvider = $request->attributes->get('_api_platform_links'); if ($this->serializer && ($links = $linksProvider?->getLinks())) { $response->headers->set('Link', $this->serializer->serialize($links)); } + $this->stopwatch?->stop('api_platform.processor.add_link_header'); return $response; } diff --git a/src/State/Processor/RespondProcessor.php b/src/State/Processor/RespondProcessor.php index 58941e437c3..f716484b456 100644 --- a/src/State/Processor/RespondProcessor.php +++ b/src/State/Processor/RespondProcessor.php @@ -27,6 +27,8 @@ use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; @@ -35,10 +37,11 @@ * * @author Kévin Dunglas */ -final class RespondProcessor implements ProcessorInterface +final class RespondProcessor implements ProcessorInterface, StopwatchAwareInterface { use ClassInfoTrait; use CloneTrait; + use StopwatchAwareTrait; public const METHOD_TO_CODE = [ 'POST' => Response::HTTP_CREATED, @@ -62,6 +65,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = return $data; } + $this->stopwatch?->start('api_platform.processor.respond'); + $headers = [ 'Content-Type' => \sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())), 'Vary' => 'Accept', @@ -144,6 +149,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = } } + $this->stopwatch?->stop('api_platform.processor.respond'); + return new Response( $data, $status, diff --git a/src/State/Processor/SerializeProcessor.php b/src/State/Processor/SerializeProcessor.php index b56bd332a4d..10f07ce4a60 100644 --- a/src/State/Processor/SerializeProcessor.php +++ b/src/State/Processor/SerializeProcessor.php @@ -17,6 +17,8 @@ use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ResourceList; use ApiPlatform\State\SerializerContextBuilderInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; @@ -33,8 +35,10 @@ * * @author Kévin Dunglas */ -final class SerializeProcessor implements ProcessorInterface +final class SerializeProcessor implements ProcessorInterface, StopwatchAwareInterface { + use StopwatchAwareTrait; + /** * @param ProcessorInterface|null $processor */ @@ -48,6 +52,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = return $this->processor ? $this->processor->process($data, $operation, $uriVariables, $context) : $data; } + $this->stopwatch?->start('api_platform.processor.serialize'); + // @see ApiPlatform\State\Processor\RespondProcessor $context['original_data'] = $data; @@ -60,6 +66,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $serializerContext['uri_variables'] = $uriVariables; if (isset($serializerContext['output']) && \array_key_exists('class', $serializerContext['output']) && null === $serializerContext['output']['class']) { + $this->stopwatch?->stop('api_platform.processor.serialize'); + return $this->processor ? $this->processor->process(null, $operation, $uriVariables, $context) : null; } @@ -81,6 +89,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $request->attributes->set('_api_platform_links', $linkProvider); } + $this->stopwatch?->stop('api_platform.processor.serialize'); + return $this->processor ? $this->processor->process($serialized, $operation, $uriVariables, $context) : $serialized; } } diff --git a/src/State/Processor/WriteProcessor.php b/src/State/Processor/WriteProcessor.php index ed378372743..987c1229cfa 100644 --- a/src/State/Processor/WriteProcessor.php +++ b/src/State/Processor/WriteProcessor.php @@ -16,6 +16,8 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use Symfony\Component\HttpFoundation\Response; /** @@ -29,9 +31,10 @@ * @author Kévin Dunglas * @author Baptiste Meyer */ -final class WriteProcessor implements ProcessorInterface +final class WriteProcessor implements ProcessorInterface, StopwatchAwareInterface { use ClassInfoTrait; + use StopwatchAwareTrait; /** * @param ProcessorInterface $processor @@ -51,7 +54,9 @@ public function process(mixed $data, Operation $operation, array $uriVariables = return $this->processor ? $this->processor->process($data, $operation, $uriVariables, $context) : $data; } + $this->stopwatch?->start('api_platform.processor.write'); $data = $this->callableProcessor->process($data, $operation, $uriVariables, $context); + $this->stopwatch?->stop('api_platform.processor.write'); return $this->processor ? $this->processor->process($data, $operation, $uriVariables, $context) : $data; } diff --git a/src/State/Provider/ContentNegotiationProvider.php b/src/State/Provider/ContentNegotiationProvider.php index b7f7733d8d2..09b693ed8a7 100644 --- a/src/State/Provider/ContentNegotiationProvider.php +++ b/src/State/Provider/ContentNegotiationProvider.php @@ -18,13 +18,16 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Util\ContentNegotiationTrait; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use Negotiation\Negotiator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; -final class ContentNegotiationProvider implements ProviderInterface +final class ContentNegotiationProvider implements ProviderInterface, StopwatchAwareInterface { use ContentNegotiationTrait; + use StopwatchAwareTrait; /** * @param array $formats @@ -41,12 +44,14 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $this->decorated?->provide($operation, $uriVariables, $context); } + $this->stopwatch?->start('api_platform.provider.content_negotiation'); $isErrorOperation = $operation instanceof ErrorOperation; $formats = $operation->getOutputFormats() ?? ($isErrorOperation ? $this->errorFormats : $this->formats); $this->addRequestFormats($request, $formats); $request->attributes->set('input_format', $this->getInputFormat($operation, $request)); $request->setRequestFormat($this->getRequestFormat($request, $formats, !$isErrorOperation)); + $this->stopwatch?->stop('api_platform.provider.content_negotiation'); return $this->decorated?->provide($operation, $uriVariables, $context); } diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index e911d3018b0..7d4599bbea4 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -17,6 +17,8 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -30,8 +32,10 @@ use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; -final class DeserializeProvider implements ProviderInterface +final class DeserializeProvider implements ProviderInterface, StopwatchAwareInterface { + use StopwatchAwareTrait; + public function __construct( private readonly ?ProviderInterface $decorated, private readonly SerializerInterface $serializer, @@ -59,6 +63,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $data; } + $this->stopwatch?->start('api_platform.provider.deserialize'); + $contentType = $request->headers->get('CONTENT_TYPE'); if (null === $contentType || '' === $contentType) { throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.'); @@ -94,7 +100,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c unset($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE]); try { - return $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext); + $data = $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext); } catch (PartialDenormalizationException $e) { if (!class_exists(ConstraintViolationList::class)) { throw $e; @@ -118,6 +124,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } } + $this->stopwatch?->stop('api_platform.provider.deserialize'); + return $data; } diff --git a/src/State/Provider/ParameterProvider.php b/src/State/Provider/ParameterProvider.php index bdabbaa1c81..fb934993890 100644 --- a/src/State/Provider/ParameterProvider.php +++ b/src/State/Provider/ParameterProvider.php @@ -21,6 +21,8 @@ use ApiPlatform\State\ParameterNotFound; use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use ApiPlatform\State\Util\ParameterParserTrait; use ApiPlatform\State\Util\RequestParser; use Psr\Container\ContainerInterface; @@ -32,9 +34,10 @@ * * @experimental */ -final class ParameterProvider implements ProviderInterface +final class ParameterProvider implements ProviderInterface, StopwatchAwareInterface { use ParameterParserTrait; + use StopwatchAwareTrait; public function __construct(private readonly ?ProviderInterface $decorated = null, private readonly ?ContainerInterface $locator = null) { @@ -42,6 +45,7 @@ public function __construct(private readonly ?ProviderInterface $decorated = nul public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { + $this->stopwatch?->start('api_platform.provider.parameter'); $request = $context['request'] ?? null; if ($request && null === $request->attributes->get('_api_query_parameters')) { $queryString = RequestParser::getQueryString($request); @@ -95,6 +99,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $request?->attributes->set('_api_operation', $operation); $context['operation'] = $operation; + $this->stopwatch?->stop('api_platform.provider.parameter'); return $this->decorated?->provide($operation, $uriVariables, $context); } diff --git a/src/State/Provider/ReadProvider.php b/src/State/Provider/ReadProvider.php index f5ecb5510c0..0e1abf17c34 100644 --- a/src/State/Provider/ReadProvider.php +++ b/src/State/Provider/ReadProvider.php @@ -20,6 +20,8 @@ use ApiPlatform\State\Exception\ProviderNotFoundException; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; +use ApiPlatform\State\StopwatchAwareInterface; +use ApiPlatform\State\StopwatchAwareTrait; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\State\Util\RequestParser; @@ -31,10 +33,11 @@ * * @author Kévin Dunglas */ -final class ReadProvider implements ProviderInterface +final class ReadProvider implements ProviderInterface, StopwatchAwareInterface { use CloneTrait; use OperationRequestInitiatorTrait; + use StopwatchAwareTrait; use UriVariablesResolverTrait; public function __construct( @@ -55,6 +58,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return null; } + $this->stopwatch?->start('api_platform.provider.read'); + if (null === ($filters = $request?->attributes->get('_api_filters')) && $request) { $queryString = RequestParser::getQueryString($request); $filters = $queryString ? RequestParser::parseRequestParams($queryString) : null; @@ -96,6 +101,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $request?->attributes->set('data', $data); $request?->attributes->set('previous_data', $this->clone($data)); + $this->stopwatch?->stop('api_platform.provider.read'); + return $data; } } diff --git a/src/State/StopwatchAwareInterface.php b/src/State/StopwatchAwareInterface.php new file mode 100644 index 00000000000..c76ed58b2b1 --- /dev/null +++ b/src/State/StopwatchAwareInterface.php @@ -0,0 +1,31 @@ + + * + * 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\State; + +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * Interface for classes that can be injected with a Stopwatch instance. + * + * @internal + * + * @author Kévin Dunglas + */ +interface StopwatchAwareInterface +{ + /** + * Sets the Stopwatch instance. + */ + public function setStopwatch(Stopwatch $stopwatch): void; +} diff --git a/src/State/StopwatchAwareTrait.php b/src/State/StopwatchAwareTrait.php new file mode 100644 index 00000000000..c309802e30a --- /dev/null +++ b/src/State/StopwatchAwareTrait.php @@ -0,0 +1,29 @@ + + * + * 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\State; + +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @internal + */ +trait StopwatchAwareTrait +{ + private ?Stopwatch $stopwatch = null; + + public function setStopwatch(Stopwatch $stopwatch): void + { + $this->stopwatch = $stopwatch; + } +} diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 45136e4d3dd..333adf97e25 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -226,6 +226,33 @@ static function (ChildDefinition $definition, AsOperationMutator $attribute, \Re if (!$container->has('api_platform.state.item_provider')) { $container->setAlias('api_platform.state.item_provider', 'api_platform.state_provider.object'); } + + if ($container->getParameter('kernel.debug')) { + $this->injectStopwatch($container); + } + } + + private function injectStopwatch(ContainerBuilder $container): void + { + $services = [ + 'api_platform.state_processor.add_link_header', + 'api_platform.state_processor.respond', + 'api_platform.state_processor.serialize', + 'api_platform.state_processor.write', + 'api_platform.state_provider.content_negotiation', + 'api_platform.state_provider.deserialize', + 'api_platform.state_provider.parameter', + 'api_platform.state_provider.read', + ]; + + foreach ($services as $id) { + if (!$container->hasDefinition($id)) { + continue; + } + + $definition = $container->getDefinition($id); + $definition->addMethodCall('setStopwatch', [new Reference('debug.stopwatch', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)]); + } } private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats, array $docsFormats): void From bde86e9a2bd0ae63b790400b0d27fb42b6eb025e Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 19 Aug 2025 08:42:15 +0200 Subject: [PATCH 2/2] test: skip mongodb bundle when extension is not loaded --- tests/Fixtures/app/AppKernel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 4e1a74cded7..5f43f244bf9 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -80,7 +80,7 @@ public function registerBundles(): array $bundles[] = new FriendsOfBehatSymfonyExtensionBundle(); } - if (class_exists(DoctrineMongoDBBundle::class)) { + if (extension_loaded('mongodb') && class_exists(DoctrineMongoDBBundle::class)) { $bundles[] = new DoctrineMongoDBBundle(); }