diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 8a3eb9021b..4e58c74acf 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -22,6 +22,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ProfilerPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -56,5 +57,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); $container->addCompilerPass(new SerializerMappingLoaderPass()); + $container->addCompilerPass(new ProfilerPass()); } } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ProfilerPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ProfilerPass.php new file mode 100644 index 0000000000..c082d4844f --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ProfilerPass.php @@ -0,0 +1,103 @@ + + * + * 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\Symfony\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\Bundle\State\TraceableProcessor; +use ApiPlatform\Symfony\Bundle\State\TraceableProvider; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +final class ProfilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('debug.stopwatch')) { + return; + } + + $this->decorateProviders($container); + $this->decorateProcessors($container); + } + + private function decorateProviders(ContainerBuilder $container): void + { + foreach ($this->findServiceIds($container, ProviderInterface::class, TraceableProvider::class) as $providerId) { + $decoratorId = $providerId.'.traceable'; + $container->register($decoratorId, TraceableProvider::class) + ->setDecoratedService($providerId, null, -\PHP_INT_MAX) + ->setArguments([ + new Reference($decoratorId.'.inner'), + new Reference('debug.stopwatch'), + $providerId, + ]); + } + } + + private function decorateProcessors(ContainerBuilder $container): void + { + foreach ($this->findServiceIds($container, ProcessorInterface::class, TraceableProcessor::class) as $processorId) { + $decoratorId = $processorId.'.traceable'; + $container->register($decoratorId, TraceableProcessor::class) + ->setDecoratedService($processorId, null, -\PHP_INT_MAX) + ->setArguments([ + new Reference($decoratorId.'.inner'), + new Reference('debug.stopwatch'), + $processorId, + ]); + } + } + + /** + * @param class-string $interface + * @param class-string $excludeClass + * + * @return string[] + */ + private function findServiceIds(ContainerBuilder $container, string $interface, string $excludeClass): array + { + $serviceIds = []; + foreach (array_keys($container->getDefinitions()) as $id) { + if (!$container->hasDefinition($id)) { + continue; + } + + $definition = $container->getDefinition($id); + if ($definition->isAbstract() || $definition->isSynthetic() || !$definition->getClass()) { + continue; + } + + if (is_a($definition->getClass(), $excludeClass, true)) { + continue; + } + + try { + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + if (!$class || (!class_exists($class) && !interface_exists($class))) { + continue; + } + $reflectionClass = new \ReflectionClass($class); + if ($reflectionClass->implementsInterface($interface)) { + $serviceIds[] = $id; + } + } catch (\ReflectionException) { + // ignore + } + } + + return $serviceIds; + } +} diff --git a/src/Symfony/Bundle/State/TraceableProcessor.php b/src/Symfony/Bundle/State/TraceableProcessor.php new file mode 100644 index 0000000000..a0ec8eda22 --- /dev/null +++ b/src/Symfony/Bundle/State/TraceableProcessor.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\Symfony\Bundle\State; + +use ApiPlatform\State\ProcessorInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +final class TraceableProcessor implements ProcessorInterface +{ + public function __construct(private readonly ProcessorInterface $processor, private readonly Stopwatch $stopwatch, private readonly string $name) + { + } + + public function process(mixed ...$args): mixed + { + $this->stopwatch->start($this->name); + $result = $this->processor->process(...$args); + $this->stopwatch->stop($this->name); + + return $result; + } +} diff --git a/src/Symfony/Bundle/State/TraceableProvider.php b/src/Symfony/Bundle/State/TraceableProvider.php new file mode 100644 index 0000000000..3131184b94 --- /dev/null +++ b/src/Symfony/Bundle/State/TraceableProvider.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\Symfony\Bundle\State; + +use ApiPlatform\State\ProviderInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +final class TraceableProvider implements ProviderInterface +{ + public function __construct(private readonly ProviderInterface $provider, private readonly Stopwatch $stopwatch, private readonly string $name) + { + } + + public function provide(mixed ...$args): object|array|null + { + $this->stopwatch->start($this->name); + $result = $this->provider->provide(...$args); + $this->stopwatch->stop($this->name); + + return $result; + } +}