├── tests └── .gitkeep ├── symfony_profiler.jpg ├── symfony_profiler_panel.jpg ├── src ├── Bridge │ ├── ComposerOverloadClass │ │ └── Doctrine │ │ │ └── ORM │ │ │ └── Internal │ │ │ ├── ArrayHydrator.php │ │ │ ├── ObjectHydrator.php │ │ │ ├── ScalarHydrator.php │ │ │ ├── SimpleObjectHydrator.php │ │ │ └── SingleScalarHydrator.php │ └── DoctrineStatsBundle │ │ ├── DoctrineStatsBundle.php │ │ ├── DependencyInjection │ │ ├── Compiler │ │ │ └── AddSqlLoggerPass.php │ │ ├── Configuration.php │ │ └── DoctrineStatsExtension.php │ │ ├── DataCollector │ │ ├── DoctrineCollectorInterface.php │ │ └── DoctrineStatsCollector.php │ │ └── Resources │ │ ├── config │ │ └── services.yml │ │ └── views │ │ └── DataCollector │ │ └── template.html.twig ├── Doctrine │ ├── ORM │ │ ├── Event │ │ │ ├── PreLazyLoadEventArgs.php │ │ │ ├── PreHydrationEventArgs.php │ │ │ ├── PostHydrationEventArgs.php │ │ │ ├── PostLazyLoadEventArgs.php │ │ │ ├── OverloadedHydratorTrait.php │ │ │ ├── PostCreateEntityEventArgs.php │ │ │ └── HydrationEventsTrait.php │ │ ├── UnitOfWork.php │ │ ├── Proxy │ │ │ └── ProxyFactory.php │ │ └── EntityManager.php │ └── DBAL │ │ └── Logger │ │ └── SqlLogger.php └── EventSubscriber │ └── DoctrineEventSubscriber.php ├── composer.json ├── changelog.md └── README.md /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /symfony_profiler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steevanb/doctrine-stats/HEAD/symfony_profiler.jpg -------------------------------------------------------------------------------- /symfony_profiler_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steevanb/doctrine-stats/HEAD/symfony_profiler_panel.jpg -------------------------------------------------------------------------------- /src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/ArrayHydrator.php: -------------------------------------------------------------------------------- 1 | eventId = uniqid('ladyLoad_'); 19 | } 20 | 21 | public function getEventId(): string 22 | { 23 | return $this->eventId; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DoctrineStatsBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new AddSqlLoggerPass()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DependencyInjection/Compiler/AddSqlLoggerPass.php: -------------------------------------------------------------------------------- 1 | getDefinition('doctrine.dbal.logger.chain'); 18 | $definition->addMethodCall('addLogger', [new Reference('doctrine_stats.logger.sql')]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 18 | 19 | $rootNode 20 | ->children() 21 | ->booleanNode('query_backtrace')->defaultValue(false)->end() 22 | ->end(); 23 | 24 | return $treeBuilder; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/PreHydrationEventArgs.php: -------------------------------------------------------------------------------- 1 | eventId = uniqid('hydration_'); 22 | $this->hydratorClassName = $hydratorClassName; 23 | } 24 | 25 | public function getEventId(): string 26 | { 27 | return $this->eventId; 28 | } 29 | 30 | public function getHydratorClassName(): string 31 | { 32 | return $this->hydratorClassName; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DataCollector/DoctrineCollectorInterface.php: -------------------------------------------------------------------------------- 1 | $classIdentifiers 24 | */ 25 | public function addHydratedEntity(string $hydratorClassName, string $className, array $classIdentifiers): self; 26 | } 27 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | doctrine_stats.data_collector.stats: 3 | class: Steevanb\DoctrineStats\Bridge\DoctrineStatsBundle\DataCollector\DoctrineStatsCollector 4 | arguments: [ '@doctrine_stats.logger.sql', '@doctrine' ] 5 | public: false 6 | tags: 7 | - 8 | name: data_collector 9 | id: doctrine_stats 10 | template: '@DoctrineStats/DataCollector/template.html.twig' 11 | priority: 249 12 | 13 | doctrine_stats.event_subscriber.doctrine: 14 | class: Steevanb\DoctrineStats\EventSubscriber\DoctrineEventSubscriber 15 | arguments: [ '@doctrine_stats.data_collector.stats' ] 16 | tags: 17 | - { name: doctrine.event_subscriber } 18 | 19 | doctrine_stats.logger.sql: 20 | class: Steevanb\DoctrineStats\Doctrine\DBAL\Logger\SqlLogger 21 | public: false 22 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/PostHydrationEventArgs.php: -------------------------------------------------------------------------------- 1 | preHydrationEventId = $preHydrationEventId; 23 | $this->hydratorClassName = $hydratorClassName; 24 | } 25 | 26 | public function getPreHydrationEventId(): string 27 | { 28 | return $this->preHydrationEventId; 29 | } 30 | 31 | /** @return class-string */ 32 | public function getHydratorClassName(): string 33 | { 34 | return $this->hydratorClassName; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/PostLazyLoadEventArgs.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 26 | $this->entity = $entity; 27 | } 28 | 29 | public function getEntityManager(): EntityManagerInterface 30 | { 31 | return $this->entityManager; 32 | } 33 | 34 | /** @return object */ 35 | public function getEntity() 36 | { 37 | return $this->entity; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/UnitOfWork.php: -------------------------------------------------------------------------------- 1 | getParentEntityManager(); 21 | foreach ($collection->toArray() as $element) { 22 | $postLazyloadEventArgs = new PostLazyLoadEventArgs($em, $element); 23 | $em->getEventManager()->dispatchEvent(PostLazyLoadEventArgs::EVENT_NAME, $postLazyloadEventArgs); 24 | } 25 | } 26 | 27 | protected function getParentEntityManager(): EntityManagerInterface 28 | { 29 | $reflectionProperty = new \ReflectionProperty(get_parent_class($this), 'em'); 30 | $reflectionProperty->setAccessible(true); 31 | $entityManager = $reflectionProperty->getValue($this); 32 | $reflectionProperty->setAccessible(false); 33 | 34 | return $entityManager; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/OverloadedHydratorTrait.php: -------------------------------------------------------------------------------- 1 | _em; 20 | } 21 | 22 | /** 23 | * @param Result|ResultStatement $stmt 24 | * @param object $resultSetMapping 25 | * @param array $hints 26 | * @return array 27 | */ 28 | public function hydrateAll($stmt, $resultSetMapping, array $hints = []): array 29 | { 30 | $eventId = $this->dispatchPreHydrationEvent(); 31 | $return = parent::hydrateAll($stmt, $resultSetMapping, $hints); 32 | $this->dispatchPostHydrationEvent($eventId); 33 | 34 | return $return; 35 | } 36 | 37 | /** @return array|false */ 38 | public function hydrateRow() 39 | { 40 | $eventId = $this->dispatchPreHydrationEvent(); 41 | $return = parent::hydrateRow(); 42 | $this->dispatchPostHydrationEvent($eventId); 43 | 44 | return $return; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steevanb/doctrine-stats", 3 | "description": "Count managed and lazy loaded entities, hydration time etc", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.1 || ^8.0", 7 | "doctrine/orm": "^2.4.8" 8 | }, 9 | "require-dev": { 10 | "doctrine/persistence": "^2.0", 11 | "phpunit/phpunit": "8.5.*", 12 | "steevanb/php-backtrace": "^2.0", 13 | "symfony/config": "^5.0", 14 | "symfony/console": "5.3.*", 15 | "symfony/dependency-injection": "^5.0", 16 | "symfony/http-foundation": "^5.0", 17 | "symfony/http-kernel": "^5.0" 18 | }, 19 | "suggest": { 20 | "steevanb/composer-overload-class ^1.0": "Add hydration time to statistics", 21 | "steevanb/php-backtrace ^1.1": "Add backtrace to queries" 22 | }, 23 | "config": { 24 | "cache-dir": "/app/var/composer/cache", 25 | "sort-packages": true 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Steevanb\\DoctrineStats\\": "src/" 30 | } 31 | }, 32 | "scripts": { 33 | "post-install-cmd": [ 34 | "@rm-composer-lock" 35 | ], 36 | "post-update-cmd": [ 37 | "@rm-composer-lock" 38 | ], 39 | "rm-composer-lock": "rm composer.lock" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/PostCreateEntityEventArgs.php: -------------------------------------------------------------------------------- 1 | */ 20 | protected $classIdentifiers = []; 21 | 22 | /** 23 | * @param class-string $hydratorClassName 24 | * @param class-string $className 25 | * @param array $classIdentifiers 26 | */ 27 | public function __construct(string $hydratorClassName, string $className, array $classIdentifiers) 28 | { 29 | $this->hydratorClassName = $hydratorClassName; 30 | $this->className = $className; 31 | $this->classIdentifiers = $classIdentifiers; 32 | } 33 | 34 | /** @return class-string */ 35 | public function getHydratorClassName(): string 36 | { 37 | return $this->hydratorClassName; 38 | } 39 | 40 | /** @return class-string */ 41 | public function getClassName(): string 42 | { 43 | return $this->className; 44 | } 45 | 46 | /** @return array */ 47 | public function getClassIdentifiers(): array 48 | { 49 | return $this->classIdentifiers; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DependencyInjection/DoctrineStatsExtension.php: -------------------------------------------------------------------------------- 1 | $configs */ 17 | public function load(array $configs, ContainerBuilder $container): void 18 | { 19 | $this 20 | ->loadServices($container) 21 | ->loadConfigs($configs, $container); 22 | } 23 | 24 | protected function loadServices(ContainerBuilder $container): self 25 | { 26 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 27 | $loader->load('services.yml'); 28 | 29 | return $this; 30 | } 31 | 32 | /** @param array $configs */ 33 | protected function loadConfigs(array $configs, ContainerBuilder $container): self 34 | { 35 | $configuration = new Configuration(); 36 | $config = $this->processConfiguration($configuration, $configs); 37 | $sqlLogger = $container->getDefinition('doctrine_stats.logger.sql'); 38 | $sqlLogger->addMethodCall('setBacktraceEnabled', [$config['query_backtrace']]); 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Event/HydrationEventsTrait.php: -------------------------------------------------------------------------------- 1 | getEntityManager()->getEventManager()->dispatchEvent(PreHydrationEventArgs::EVENT_NAME, $eventArgs); 20 | 21 | return $eventArgs->getEventId(); 22 | } 23 | 24 | protected function dispatchPostHydrationEvent(string $preHydrationEventId): self 25 | { 26 | $eventArgs = new PostHydrationEventArgs($preHydrationEventId, get_class($this)); 27 | $this->getEntityManager()->getEventManager()->dispatchEvent(PostHydrationEventArgs::EVENT_NAME, $eventArgs); 28 | 29 | return $this; 30 | } 31 | 32 | /** @param array $data */ 33 | protected function dispatchPostCreateEntityEvent(ClassMetadata $classMetaData, array $data): self 34 | { 35 | $identifiers = []; 36 | foreach ($classMetaData->getIdentifierFieldNames() as $identifier) { 37 | if (array_key_exists($identifier, $data)) { 38 | $identifiers[$identifier] = $data[$identifier]; 39 | } 40 | } 41 | 42 | $eventArgs = new PostCreateEntityEventArgs(get_class($this), $classMetaData->name, $identifiers); 43 | $this->getEntityManager()->getEventManager()->dispatchEvent(PostCreateEntityEventArgs::EVENT_NAME, $eventArgs); 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/Proxy/ProxyFactory.php: -------------------------------------------------------------------------------- 1 | initializer; 24 | // Doctrine\ORM\Proxy\ProxyFactory::$uow is private, so we use Reflection to get it 25 | $reflectionProperty = new \ReflectionProperty(get_parent_class($this), 'uow'); 26 | $reflectionProperty->setAccessible(true); 27 | $entityPersister = $reflectionProperty->getValue($this)->getEntityPersister($className); 28 | $reflectionProperty->setAccessible(false); 29 | 30 | $initializer = function (Proxy $proxy) use ($doctrineInitializer, $entityPersister) { 31 | $entityManager = $this->getEntityManager($entityPersister); 32 | 33 | call_user_func($doctrineInitializer, $proxy); 34 | 35 | $postLazyLoadEventArgs = new PostLazyLoadEventArgs($entityManager, $proxy); 36 | $entityManager->getEventManager()->dispatchEvent(PostLazyLoadEventArgs::EVENT_NAME, $postLazyLoadEventArgs); 37 | }; 38 | 39 | $proxyDefinition->initializer = $initializer; 40 | 41 | return $proxyDefinition; 42 | } 43 | 44 | protected function getEntityManager(EntityPersister $entityPersister): EntityManagerInterface 45 | { 46 | $property = new \ReflectionProperty(get_class($entityPersister), 'em'); 47 | $property->setAccessible(true); 48 | /** @var EntityManagerInterface $return */ 49 | $return = $property->getValue($entityPersister); 50 | $property->setAccessible(false); 51 | 52 | return $return; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Doctrine/DBAL/Logger/SqlLogger.php: -------------------------------------------------------------------------------- 1 | > */ 12 | protected $queries = []; 13 | 14 | /** @var int */ 15 | protected $currentQueryIndex = -1; 16 | 17 | /** @var float|null */ 18 | protected $start = null; 19 | 20 | /** @var bool */ 21 | protected $backtraceEnabled = false; 22 | 23 | public function setBacktraceEnabled(bool $enabled): self 24 | { 25 | $this->backtraceEnabled = $enabled; 26 | 27 | return $this; 28 | } 29 | 30 | /** @param string $sql */ 31 | public function startQuery($sql, array $params = null, array $types = null) 32 | { 33 | if ($this->backtraceEnabled) { 34 | $isDumpBacktrace = class_exists('\DumpBacktrace'); 35 | $isDebugBacktrace = class_exists('\DebugBacktrace'); 36 | if ($isDumpBacktrace === false && $isDebugBacktrace === false) { 37 | throw new \Exception( 38 | 'You need require steevanb/php-backtrace ^1.1||^2.0 to activate query backtrace. ' 39 | . 'Example with composer : composer require --dev steevanb/php-backtrace ^1.1||^2.0' 40 | ); 41 | } 42 | $backtrace = $isDumpBacktrace ? \DumpBacktrace::getBacktraces() : \DebugBacktrace::getBacktraces(); 43 | } else { 44 | $backtrace = null; 45 | } 46 | 47 | $this->start = microtime(true); 48 | $this->queries[++$this->currentQueryIndex] = [ 49 | 'sql' => $sql, 50 | 'params' => $params, 51 | 'types' => $types, 52 | 'time' => 0, 53 | 'backtrace' => $backtrace 54 | ]; 55 | } 56 | 57 | public function stopQuery(): void 58 | { 59 | $this->queries[$this->currentQueryIndex]['time'] = microtime(true) - ($this->start ?? 0); 60 | } 61 | 62 | public function getCurrentQueryIndex(): int 63 | { 64 | return $this->currentQueryIndex; 65 | } 66 | 67 | /** @return array> */ 68 | public function getQueries(): array 69 | { 70 | return $this->queries; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/EventSubscriber/DoctrineEventSubscriber.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 30 | } 31 | 32 | public function getSubscribedEvents(): array 33 | { 34 | return [ 35 | PostLazyLoadEventArgs::EVENT_NAME, 36 | PreHydrationEventArgs::EVENT_NAME, 37 | PostHydrationEventArgs::EVENT_NAME, 38 | PostCreateEntityEventArgs::EVENT_NAME 39 | ]; 40 | } 41 | 42 | public function postLazyLoad(PostLazyLoadEventArgs $eventArgs): void 43 | { 44 | $this 45 | ->collector 46 | ->addLazyLoadedEntity($eventArgs->getEntityManager(), $eventArgs->getEntity()); 47 | } 48 | 49 | public function preHydration(PreHydrationEventArgs $eventArgs): void 50 | { 51 | if ($this->preHydrationEventId === null) { 52 | $this->preHydrationEventId = $eventArgs->getEventId(); 53 | $this->preHydrationTime = microtime(true); 54 | } 55 | } 56 | 57 | public function postHydration(PostHydrationEventArgs $eventArgs): void 58 | { 59 | if ($this->preHydrationEventId === $eventArgs->getPreHydrationEventId()) { 60 | $postHydrationTime = microtime(true); 61 | $this->collector->addHydrationTime( 62 | $eventArgs->getHydratorClassName(), 63 | ($postHydrationTime - ($this->preHydrationTime ?? 0)) * 1000 64 | ); 65 | $this->preHydrationEventId = null; 66 | $this->preHydrationTime = null; 67 | } 68 | } 69 | 70 | public function postCreateEntity(PostCreateEntityEventArgs $eventArgs): void 71 | { 72 | $this->collector->addHydratedEntity( 73 | $eventArgs->getHydratorClassName(), 74 | $eventArgs->getClassName(), 75 | $eventArgs->getClassIdentifiers() 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/EntityManager.php: -------------------------------------------------------------------------------- 1 | getMetadataDriverImpl() instanceof MappingDriver === false) { 28 | throw ORMException::missingMappingDriverImpl(); 29 | } 30 | 31 | switch (true) { 32 | case (is_array($conn)): 33 | $conn = \Doctrine\DBAL\DriverManager::getConnection( 34 | $conn, 35 | $config, 36 | $eventManager ?? new EventManager() 37 | ); 38 | break; 39 | 40 | case ($conn instanceof Connection): 41 | if ($eventManager !== null && $conn->getEventManager() !== $eventManager) { 42 | throw ORMException::mismatchedEventManager(); 43 | } 44 | break; 45 | 46 | default: 47 | throw new \InvalidArgumentException('Invalid argument: ' . $conn); 48 | } 49 | 50 | return new static($conn, $config, $conn->getEventManager()); 51 | } 52 | 53 | protected function __construct(Connection $conn, Configuration $config, EventManager $eventManager) 54 | { 55 | parent::__construct($conn, $config, $eventManager); 56 | 57 | $proxyDir = $config->getProxyDir(); 58 | if (is_string($proxyDir) === false) { 59 | throw new \Exception('Proxy directory must be configured.'); 60 | } 61 | 62 | $proxyNamespace = $config->getProxyNamespace(); 63 | if (is_string($proxyNamespace) === false) { 64 | throw new \Exception('Proxy namespace must be configured.'); 65 | } 66 | 67 | $this->setParentPrivatePropertyValue( 68 | 'proxyFactory', 69 | new ProxyFactory( 70 | $this, 71 | $proxyDir, 72 | $proxyNamespace, 73 | $config->getAutoGenerateProxyClasses() 74 | ) 75 | ); 76 | $this->setParentPrivatePropertyValue('unitOfWork', new UnitOfWork($this)); 77 | } 78 | 79 | /** @param mixed $value */ 80 | protected function setParentPrivatePropertyValue(string $name, $value): self 81 | { 82 | $reflectionProperty = new \ReflectionProperty(get_parent_class($this), $name); 83 | $reflectionProperty->setAccessible(true); 84 | $reflectionProperty->setValue($this, $value); 85 | $reflectionProperty->setAccessible(false); 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### master 2 | 3 | ### [2.0.0](../../compare/1.4.0...2.0.0) - 2021-11-29 4 | 5 | - [BC break] Move source files to `src/` 6 | - [BC break] Move `ComposerOverloadClass` directory from source root to `src/Bridge/ComposerOverloadClass` 7 | - [BC break] Change PHP version from `^5.4 || ^7.0` to `^7.1 || ^8.0` 8 | - [BC Break] Rename root namespace from `steevanb` to `Steevanb` 9 | - Add PHP 7.1 type hints 10 | - Add `bin`, `config` and `docker` directories 11 | - Add binaries to start the project, build Docker images, run CI tools etc 12 | - Add `.gitattributes` 13 | 14 | ### [1.4.0](../../compare/1.3.3...1.4.0) - 2020-12-07 15 | 16 | - Replace `Symfony\Bridge\Doctrine\RegistryInterface` (removed) by `Doctrine\Common\Persistence\ManagerRegistry` in `DoctrineStatsCollector` 17 | 18 | ### [1.3.3](../../compare/1.3.2...1.3.3) - 2017-12-07 19 | 20 | - [[Gemorroj](https://github.com/Gemorroj)] Fix template path for Symfony 4 21 | - Add _DoctrineStatsCollector::reset()_ for Symfony 4 22 | 23 | ### [1.3.2](../../compare/1.3.1...1.3.2) - 2017-11-17 24 | 25 | - [[gsdevme](https://github.com/gsdevme)] Use _ManagerRegistry::getManagers()_ instead of _getEntityManagers()_ 26 | 27 | ### [1.3.1](../../compare/1.3.0...1.3.1) - 2017-08-09 28 | 29 | - Fix ternary operator syntax in _DoctrineStatsCollector_ 30 | - Fix plural for _Show identifiers_ and _Hide identifiers_ in Symfony WebProfiler panel 31 | 32 | ### [1.3.0](../../compare/1.2.1...1.3.0) - 2017-08-09 33 | 34 | - [BC] Remove _DoctrineCollectorInterface::addManagedEntity()_ : Doctrine do not always trigger _postLoad_ event, so we can't use it to retrieve informations 35 | - [BC] Remove _DoctrineStatsCollector::addManagedEntity()_ 36 | - _steevanb/php-backtrace_ dependency could be _^1.1_ or _^2.0_ now (_^1.1_ before) 37 | - Group backtraces in _Show backtraces_, instead of one _Show backtrace #X_ per query 38 | 39 | ### [1.2.1](../../compare/1.2.0...1.2.1) - 2017-04-13 40 | 41 | - #4 Fix division by zero if query and hydration time equal 0 42 | 43 | ### [1.2.0](../../compare/1.1.0...1.2.0) - 2016-11-10 44 | 45 | - Disable panels when hydrators are not overloaded 46 | - Add nice debug_backtrace() for each query 47 | - Add SQL time, hydration time and Doctrine time for each query 48 | - Add type (Manual / Lazy loading) for each query 49 | - Add Show entity for each query 50 | 51 | ### [1.1.0](../../compare/1.0.3...1.1.0) - 2016-08-17 52 | 53 | - Add $identifiers type in DoctrineCollectorInterface::addManagedentity($className, array $identifiers) 54 | - Add DoctrineCollectorInterface::addHydratedEntity($hydratorClassName, $className, $classIdentifiers) 55 | - Add DoctrineStatsCollector::addHydratedEntity($hydratorClassName, $className, $classIdentifiers) 56 | - All identifiers now shown with same graphism in Symfony profiler 57 | - All class names now shown with same graphism in Symfony profiler 58 | - Remove useless monolog.logger tag to doctrine_stats.event_subscriber.doctrine Symfony service 59 | - Add postCreateEntity event 60 | - Add hydrated entities to Symfony profiler, for all hydrators who dispatch postCreateEntity event 61 | (can't do that for Doctrine hydrators at the moment) 62 | - Add HydrationEventsTrait::dispatchPostCreateEntityEvent(ClassMetadata $classMetaData, array $data) 63 | - Add DoctrineEventSubscriber::postCreateEntity(PostCreateEntityEventArgs $eventArgs) 64 | 65 | ### [1.0.3](../../compare/1.0.2...1.0.3) - 2016-08-10 66 | 67 | - Fix division by zero in DoctrineStatsCollector::getHydrationTimePercent() and getQueriesTimePercent() 68 | 69 | ### [1.0.2](../../compare/1.0.1...1.0.2) - 2016-08-08 70 | 71 | - Add queries time in Symfony WebProfiler 72 | - Add queries time percent and hydration time percent in Symfony WebProfiler 73 | 74 | ### [1.0.1](../../compare/1.0.0...1.0.1) - 2016-08-05 75 | 76 | - Fix DoctrineEventSubscriber::postLoad() call to addManagedEntity(), only managed entities will trigger this call 77 | 78 | ### 1.0.0 - 2016-07-21 79 | 80 | - Add steevanb\DoctrineStats\Doctrine\ORM\EntityManager to overload Doctrine\ORM\Proxy\ProxyFactory 81 | - Add steevanb\DoctrineStats\Doctrine\ORM\Proxy\ProxyFactory to add postLazyLoad event 82 | - Add steevanb\DoctrineStats\EventSubscriber\DoctrineEventSubscriber to collect doctrine statistics 83 | - Add ArrayHydrator, ObjectHydrator, ScalarHydrator, SimpleObjectHydrator and SingleScalarHydrator 84 | to ComposerOverloadClass, to add preHydration and postHydration events 85 | - Add Symfony2 and Symfony3 bridge with DoctrineStatsBundle 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/badge/version-2.0.0-4B9081.svg)](https://github.com/steevanb/doctrine-stats/tree/2.0.0) 2 | [![doctrine](https://img.shields.io/badge/doctrine/orm-^2.4.8-blue.svg)](http://www.doctrine-project.org) 3 | [![php](https://img.shields.io/badge/php-^5.4.6%20||%20^7.0||%20^8.0-blue.svg)](http://www.php.net) 4 | ![Lines](https://img.shields.io/badge/code%20lines-2,909-blue.svg) 5 | ![Total Downloads](https://poser.pugx.org/steevanb/doctrine-stats/downloads) 6 | 7 | ### doctrine-stats 8 | 9 | Add important Doctrine statistics: 10 | * Count managed entities 11 | * Count lazy loaded entities 12 | * Hydration time by hydrator and query 13 | * Group queries by query string, show differents parameters used by same query string 14 | * Count different query string used 15 | 16 | [Changelog](changelog.md) 17 | 18 | ### Installation 19 | 20 | ```bash 21 | composer require --dev steevanb/doctrine-stats ^2.0 22 | ``` 23 | 24 | If you want to add hydration time to your statistics: 25 | 26 | `composer.json` 27 | ```json 28 | { 29 | "autoload": { 30 | "psr-4": { 31 | "ComposerOverloadClass\\": "var/cache/ComposerOverloadClass" 32 | } 33 | }, 34 | "scripts": { 35 | "pre-autoload-dump": "steevanb\\ComposerOverloadClass\\OverloadClass::overload" 36 | }, 37 | "extra": { 38 | "composer-overload-cache-dir": "var/cache", 39 | "composer-overload-class-dev": { 40 | "Doctrine\\ORM\\Internal\\Hydration\\ArrayHydrator": { 41 | "original-file": "vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php", 42 | "overload-file": "vendor/steevanb/doctrine-stats/src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/ArrayHydrator.php" 43 | }, 44 | "Doctrine\\ORM\\Internal\\Hydration\\ObjectHydrator": { 45 | "original-file": "vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php", 46 | "overload-file": "vendor/steevanb/doctrine-stats/src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/ObjectHydrator.php" 47 | }, 48 | "Doctrine\\ORM\\Internal\\Hydration\\ScalarHydrator": { 49 | "original-file": "vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php", 50 | "overload-file": "vendor/steevanb/doctrine-stats/src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/ScalarHydrator.php" 51 | }, 52 | "Doctrine\\ORM\\Internal\\Hydration\\SimpleObjectHydrator": { 53 | "original-file": "vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php", 54 | "overload-file": "vendor/steevanb/doctrine-stats/src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/SimpleObjectHydrator.php" 55 | }, 56 | "Doctrine\\ORM\\Internal\\Hydration\\SingleScalarHydrator": { 57 | "original-file": "vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php", 58 | "overload-file": "vendor/steevanb/doctrine-stats/src/Bridge/ComposerOverloadClass/Doctrine/ORM/Internal/SingleScalarHydrator.php" 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | ```bash 65 | composer dumpautoload 66 | ``` 67 | 68 | ### Symfony 2.x, 3.x and 4.x integration 69 | 70 | Read Installation paragraph before. 71 | 72 | ```php 73 | # app/AppKernel.php 74 | class AppKernel 75 | { 76 | public function registerBundles() 77 | { 78 | if ($this->getEnvironment() === 'dev') { 79 | $bundles[] = new \Steevanb\DoctrineStats\Bridge\DoctrineStatsBundle\DoctrineStatsBundle(); 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | If you want to add lazy loaded entities to your statistics: 86 | 87 | ```yml 88 | # app/config/config_dev.yml 89 | parameters: 90 | doctrine.orm.entity_manager.class: Steevanb\DoctrineStats\Doctrine\ORM\EntityManager 91 | ``` 92 | 93 | ### Manual integration 94 | 95 | To retrieve statistics, you need to register `Steevanb\DoctrineStats\EventSubscriber\DoctrineEventSubscriber` in your event manager. 96 | 97 | If you want to add lazy loaded entities to your statistics, you need to overload default EntityManager, with `Steevanb\DoctrineStats\Doctrine\ORM\EntityManager`. 98 | 99 | ### Screenshots 100 | 101 | ![Symfony profiler](symfony_profiler.jpg) 102 | 103 | ![Symfony profiler panel](symfony_profiler_panel.jpg) 104 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/DataCollector/DoctrineStatsCollector.php: -------------------------------------------------------------------------------- 1 | */ 19 | protected $lazyLoadedEntities = []; 20 | 21 | /** @var SqlLogger */ 22 | protected $sqlLogger; 23 | 24 | /** @var array */ 25 | protected $hydrationTimes = []; 26 | 27 | /** @var int */ 28 | protected $queriesAlert = 1; 29 | 30 | /** @var int */ 31 | protected $managedEntitiesAlert = 10; 32 | 33 | /** @var int */ 34 | protected $lazyLoadedEntitiesAlert = 1; 35 | 36 | /** @var int */ 37 | protected $hydrationTimeAlert = 5; 38 | 39 | /** @var array */ 40 | protected $hydratedEntities = []; 41 | 42 | /** @var ManagerRegistry */ 43 | protected $doctrine; 44 | 45 | public function __construct(SqlLogger $sqlLogger, ManagerRegistry $doctrine) 46 | { 47 | $this->sqlLogger = $sqlLogger; 48 | $this->doctrine = $doctrine; 49 | } 50 | 51 | /** @return string */ 52 | public function getName() 53 | { 54 | return 'doctrine_stats'; 55 | } 56 | 57 | public function reset(): void 58 | { 59 | $this->data = []; 60 | } 61 | 62 | public function setQueriesAlert(int $count): self 63 | { 64 | $this->queriesAlert = $count; 65 | 66 | return $this; 67 | } 68 | 69 | public function setManagedEntitiesAlert(int $count): self 70 | { 71 | $this->managedEntitiesAlert = $count; 72 | 73 | return $this; 74 | } 75 | 76 | public function setLazyLoadedEntitiesAlert(int $count): self 77 | { 78 | $this->lazyLoadedEntitiesAlert = $count; 79 | 80 | return $this; 81 | } 82 | 83 | /** @param int $time Time in milliseconds */ 84 | public function setHydrationTimeAlert(int $time): self 85 | { 86 | $this->hydrationTimeAlert = $time; 87 | 88 | return $this; 89 | } 90 | 91 | /** @param object $entity */ 92 | public function addLazyLoadedEntity(EntityManagerInterface $entityManager, $entity): DoctrineCollectorInterface 93 | { 94 | $className = get_class($entity); 95 | $classMetaData = $entityManager->getClassMetadata($className); 96 | $associations = []; 97 | foreach ($entityManager->getMetadataFactory()->getAllMetadata() as $metaData) { 98 | foreach ($metaData->associationMappings as $field => $mapping) { 99 | if ($mapping['targetEntity'] === $classMetaData->name) { 100 | $associations[] = array_merge( 101 | $this->explodeClassParts($metaData->name), 102 | ['field' => $field] 103 | ); 104 | } 105 | } 106 | } 107 | 108 | $this->lazyLoadedEntities[] = array_merge( 109 | $this->explodeClassParts($classMetaData->name), 110 | [ 111 | 'identifiers' => $classMetaData->getIdentifierValues($entity), 112 | 'associations' => $associations, 113 | 'queryIndex' => $this->sqlLogger->getCurrentQueryIndex() 114 | ] 115 | ); 116 | 117 | return $this; 118 | } 119 | 120 | /** @param float $time Time, in milliseconds */ 121 | public function addHydrationTime(string $hydratorClassName, float $time): DoctrineCollectorInterface 122 | { 123 | if (array_key_exists($hydratorClassName, $this->hydrationTimes) === false) { 124 | $this->hydrationTimes[$hydratorClassName] = []; 125 | } 126 | $this->hydrationTimes[$hydratorClassName][] = [ 127 | 'queryIndex' => $this->sqlLogger->getCurrentQueryIndex(), 128 | 'time' => $time 129 | ]; 130 | 131 | return $this; 132 | } 133 | 134 | /** @param array $classIdentifiers */ 135 | public function addHydratedEntity( 136 | string $hydratorClassName, 137 | string $className, 138 | array $classIdentifiers 139 | ): DoctrineCollectorInterface { 140 | if (array_key_exists($hydratorClassName, $this->hydratedEntities) === false) { 141 | $this->hydratedEntities[$hydratorClassName] = []; 142 | } 143 | if (array_key_exists($className, $this->hydratedEntities[$hydratorClassName]) === false) { 144 | $this->hydratedEntities[$hydratorClassName][$className] = []; 145 | } 146 | 147 | $this->hydratedEntities[$hydratorClassName][$className][] = $this->identifiersAsString($classIdentifiers); 148 | 149 | return $this; 150 | } 151 | 152 | public function collect(Request $request, Response $response, \Throwable $exception = null): void 153 | { 154 | $this->data = [ 155 | 'lazyLoadedEntities' => $this->lazyLoadedEntities, 156 | 'queries' => $this->sqlLogger->getQueries(), 157 | 'managedEntities' => $this->parseManagedEntities(), 158 | 'hydrationTimes' => $this->hydrationTimes, 159 | 'queriesAlert' => $this->queriesAlert, 160 | 'managedEntitiesAlert' => $this->managedEntitiesAlert, 161 | 'lazyLoadedEntitiesAlert' => $this->lazyLoadedEntitiesAlert, 162 | 'hydrationTimeAlert' => $this->hydrationTimeAlert, 163 | 'hydratedEntities' => $this->hydratedEntities 164 | ]; 165 | } 166 | 167 | /** @return array */ 168 | public function getQueries(): array 169 | { 170 | static $return = false; 171 | 172 | if ($return === false) { 173 | $return = []; 174 | foreach ($this->data['queries'] as $index => $query) { 175 | if (array_key_exists($query['sql'], $return) === false) { 176 | $return[$query['sql']] = [ 177 | 'queryTime' => 0, 178 | 'queryTimePercent' => 0, 179 | 'data' => [], 180 | 'lazyLoadedEntities' => [], 181 | 'hydrationTime' => 0, 182 | 'hydrationTimePercent' => 0, 183 | ]; 184 | } 185 | $return[$query['sql']]['queryTime'] += $query['time'] * 1000; 186 | $return[$query['sql']]['data'][] = ['params' => $query['params']]; 187 | $return[$query['sql']]['backtraces'][$index] = 188 | $query['backtrace'] === null 189 | ? null 190 | : ( 191 | class_exists('\DumpBacktrace') 192 | ? \DumpBacktrace::getDump($query['backtrace']) 193 | : \DebugBacktraceHtml::getDump($query['backtrace']) 194 | ); 195 | 196 | foreach ($this->data['lazyLoadedEntities'] as $lazyLoadedEntity) { 197 | if ($lazyLoadedEntity['queryIndex'] === $index) { 198 | if (array_key_exists($index, $return[$query['sql']]['lazyLoadedEntities']) === false) { 199 | $return[$query['sql']]['lazyLoadedEntities'][$index] = []; 200 | } 201 | $return[$query['sql']]['lazyLoadedEntities'][$index][] = $lazyLoadedEntity; 202 | } 203 | } 204 | 205 | foreach ($this->data['hydrationTimes'] as $hydrationTimes) { 206 | foreach ($hydrationTimes as $hydrationTime) { 207 | if ($hydrationTime['queryIndex'] === $index) { 208 | $return[$query['sql']]['hydrationTime'] += $hydrationTime['time']; 209 | } 210 | } 211 | } 212 | } 213 | 214 | foreach ($return as &$queryData) { 215 | if ($queryData['hydrationTime'] === 0 && $queryData['queryTime'] === 0) { 216 | $queryData['queryTimePercent'] = 100; 217 | } else { 218 | $queryData['queryTimePercent'] = round( 219 | ($queryData['queryTime'] * 100) 220 | / ($queryData['hydrationTime'] + $queryData['queryTime']) 221 | ); 222 | } 223 | $queryData['hydrationTimePercent'] = 100 - $queryData['queryTimePercent']; 224 | } 225 | 226 | uasort( 227 | $return, 228 | function (array $queryA, array $queryB) { 229 | return count($queryA['data']) < count($queryB['data']) ? 1 : -1; 230 | } 231 | ); 232 | } 233 | 234 | return $return; 235 | } 236 | 237 | public function countQueries(): int 238 | { 239 | return count($this->data['queries']); 240 | } 241 | 242 | public function getQueriesTime(): float 243 | { 244 | $return = 0; 245 | foreach ($this->getQueries() as $query) { 246 | $return += $query['queryTime']; 247 | } 248 | 249 | return round($return, 2); 250 | } 251 | 252 | public function getQueriesTimePercent(): float 253 | { 254 | return $this->getDoctrineTime() > 0 255 | ? round(($this->getQueriesTime() * 100) / $this->getDoctrineTime()) 256 | : 0; 257 | } 258 | 259 | public function countDifferentQueries(): int 260 | { 261 | return count($this->getQueries()); 262 | } 263 | 264 | /** @return array */ 265 | public function getLazyLoadedEntities(): array 266 | { 267 | return $this->data['lazyLoadedEntities']; 268 | } 269 | 270 | public function countLazyLoadedEntities(): int 271 | { 272 | return count($this->data['lazyLoadedEntities']); 273 | } 274 | 275 | public function countWarnings(): int 276 | { 277 | return $this->countLazyLoadedEntities(); 278 | } 279 | 280 | public function countLazyLoadedClass(string $fullyQualifiedClassName): int 281 | { 282 | $count = 0; 283 | foreach ($this->getLazyLoadedEntities() as $lazyLoaded) { 284 | if ($lazyLoaded['namespace'] . '\\' . $lazyLoaded['className'] === $fullyQualifiedClassName) { 285 | $count++; 286 | } 287 | } 288 | 289 | return $count; 290 | } 291 | 292 | /** @return array */ 293 | public function getManagedEntities(): array 294 | { 295 | static $ordered = false; 296 | if ($ordered === false) { 297 | arsort($this->data['managedEntities']); 298 | $ordered = true; 299 | } 300 | 301 | return $this->data['managedEntities']; 302 | } 303 | 304 | public function countManagedEntities(): int 305 | { 306 | $return = 0; 307 | foreach ($this->getManagedEntities() as $stats) { 308 | $return += $stats['count']; 309 | } 310 | 311 | return $return; 312 | } 313 | 314 | public function getHydrationTotalTime(): float 315 | { 316 | $return = 0; 317 | foreach ($this->data['hydrationTimes'] as $times) { 318 | foreach ($times as $time) { 319 | $return += $time['time']; 320 | } 321 | } 322 | 323 | return round($return, 2); 324 | } 325 | 326 | /** @return array */ 327 | public function getHydrationTimesByHydrator(): array 328 | { 329 | $return = []; 330 | foreach ($this->data['hydrationTimes'] as $hydratorClassName => $times) { 331 | $return[$hydratorClassName] = 0; 332 | foreach ($times as $time) { 333 | $return[$hydratorClassName] += $time['time']; 334 | } 335 | } 336 | 337 | return $return; 338 | } 339 | 340 | public function getHydrationTimePercent(): float 341 | { 342 | return $this->getDoctrineTime() > 0 343 | ? round(($this->getHydrationTotalTime() * 100) / $this->getDoctrineTime()) 344 | : 0; 345 | } 346 | 347 | public function getDoctrineTime(): float 348 | { 349 | return round($this->getQueriesTime() + $this->getHydrationTotalTime(), 2); 350 | } 351 | 352 | public function getQueriesAlert(): int 353 | { 354 | return $this->data['queriesAlert']; 355 | } 356 | 357 | public function getManagedEntitiesAlert(): int 358 | { 359 | return $this->data['managedEntitiesAlert']; 360 | } 361 | 362 | public function getLazyLoadedEntitiesAlert(): int 363 | { 364 | return $this->data['lazyLoadedEntitiesAlert']; 365 | } 366 | 367 | public function getHydrationTimealert(): int 368 | { 369 | return $this->data['hydrationTimeAlert']; 370 | } 371 | 372 | public function getToolbarStatus(): ?string 373 | { 374 | $alert = 375 | $this->countQueries() >= $this->getQueriesAlert() 376 | || $this->countManagedEntities() >= $this->data['managedEntitiesAlert'] 377 | || $this->countLazyLoadedEntities() >= $this->data['lazyLoadedEntitiesAlert'] 378 | || $this->getHydrationTotalTime() >= $this->data['hydrationTimeAlert']; 379 | 380 | return $alert ? 'red' : null; 381 | } 382 | 383 | public function isHydratorsOverloaded(): bool 384 | { 385 | return $this->countQueries() === 0 || ($this->countQueries() > 0 && count($this->data['hydrationTimes']) > 0); 386 | } 387 | 388 | /** @return array */ 389 | public function getHydratedEntities(string $hydrator): array 390 | { 391 | return array_key_exists($hydrator, $this->data['hydratedEntities']) 392 | ? $this->data['hydratedEntities'][$hydrator] 393 | : []; 394 | } 395 | 396 | public function countHydratedEntities(string $hydrator): int 397 | { 398 | $count = 0; 399 | foreach ($this->getHydratedEntities($hydrator) as $classes) { 400 | foreach ($classes as $identifiers) { 401 | $count += count($identifiers); 402 | } 403 | } 404 | 405 | return $count; 406 | } 407 | 408 | /** @return array */ 409 | public function explodeClassParts(string $fullyClassifiedClassName): array 410 | { 411 | $posBackSlash = strrpos($fullyClassifiedClassName, '\\'); 412 | 413 | if (is_int($posBackSlash) === false) { 414 | $return = [ 415 | 'namespace' => 'NOT FOUND', 416 | 'className' => 'NOT FOUND' 417 | ]; 418 | } else { 419 | $return = [ 420 | 'namespace' => substr($fullyClassifiedClassName, 0, $posBackSlash), 421 | 'className' => substr($fullyClassifiedClassName, $posBackSlash + 1) 422 | ]; 423 | } 424 | 425 | return $return; 426 | } 427 | 428 | /** 429 | * @param array $identifiers 430 | * @return array 431 | */ 432 | public function mergeIdentifiers(array $identifiers): array 433 | { 434 | $return = []; 435 | foreach ($identifiers as $identifier) { 436 | if (array_key_exists($identifier, $return) === false) { 437 | $return[$identifier] = 0; 438 | } 439 | $return[$identifier]++; 440 | } 441 | 442 | return $return; 443 | } 444 | 445 | /** @param array $identifiers */ 446 | public function identifiersAsString(array $identifiers): string 447 | { 448 | $return = []; 449 | foreach ($identifiers as $name => $value) { 450 | $return[] = $name . ': ' . $value; 451 | } 452 | 453 | return implode(', ', $return); 454 | } 455 | 456 | /** @return array> */ 457 | protected function parseManagedEntities(): array 458 | { 459 | $return = []; 460 | foreach ($this->doctrine->getManagers() as $manager) { 461 | if ($manager instanceof EntityManagerInterface === false) { 462 | continue; 463 | } 464 | 465 | foreach ($manager->getUnitOfWork()->getIdentityMap() as $class => $entities) { 466 | $return[$class] = [ 467 | 'count' => count($entities), 468 | 'ids' => array_keys($entities) 469 | ]; 470 | sort($return[$class]['ids']); 471 | } 472 | } 473 | 474 | return $return; 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/Bridge/DoctrineStatsBundle/Resources/views/DataCollector/template.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | {{ include('@Doctrine/Collector/icon.svg') }} 6 | 7 | {% set status = collector.getToolbarStatus() %} 8 | 9 | {% if collector.isHydratorsOverloaded() %} 10 | {{ collector.getDoctrineTime() }} ms / 11 | {% endif %} 12 | {{ collector.countManagedEntities() }} / 13 | {{ collector.countLazyLoadedEntities() }} 14 | 15 | {% endset %} 16 | 17 | {% set text %} 18 |
19 | Quer{% if collector.countQueries() > 1 %}ies{% else %}y{% endif %} 20 | 21 | {{ collector.countQueries() }} 22 | 23 |
24 |
25 | Different quer{% if collector.countDifferentQueries() > 1 %}ies{% else %}y{% endif %} 26 | 27 | {{ collector.countDifferentQueries() }} 28 | 29 |
30 |
31 | SQL 32 | 33 | {{ collector.getQueriesTime() }} ms 34 | {% if collector.isHydratorsOverloaded() %} 35 | ({{ collector.getQueriesTimePercent() }}%) 36 | {% endif %} 37 | 38 |
39 | {% if collector.isHydratorsOverloaded() %} 40 |
41 | Hydration 42 | 43 | {{ collector.getHydrationTotalTime() }} ms 44 | ({{ collector.getHydrationTimePercent() }}%) 45 | 46 |
47 |
48 | Total 49 | 50 | {{ collector.getDoctrineTime() }} ms 51 | 52 |
53 | {% endif %} 54 |
55 | Managed entit{% if collector.countManagedEntities() > 1 %}ies{% else %}y{% endif %} 56 | 57 | {{ collector.countManagedEntities() }} 58 | 59 |
60 |
61 | Lazy loaded entit{% if collector.countLazyLoadedEntities() > 1 %}ies{% else %}y{% endif %} 62 | 63 | {{ collector.countLazyLoadedEntities() }} 64 | 65 |
66 | {% endset %} 67 | 68 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true }) }} 69 | {% endblock %} 70 | 71 | 72 | {% block menu %} 73 | 74 | {{ include('@Doctrine/Collector/icon.svg') }} 75 | Statistics 76 | {% if collector.isHydratorsOverloaded() %} 77 | 78 | 79 | {{ collector.getDoctrineTime() }} ms 80 | 81 | 82 | {% endif %} 83 | 84 | {% endblock %} 85 | 86 | {% block head %} 87 | {{ parent() }} 88 | 89 | 158 | {% endblock %} 159 | 160 | {% block panel %} 161 | {% import _self as self %} 162 | 163 |

Doctrine statistics

164 |
165 |
166 | {{ collector.countQueries() }} 167 | Queries 168 |
169 |
170 | {{ collector.countDifferentQueries() }} 171 | Different quer{% if collector.countDifferentQueries() > 1 %}ies{% else %}y{% endif %} 172 |
173 |
174 | {{ collector.getQueriesTime() }} ms 175 | SQL ({{ collector.getQueriesTimePercent() }}%) 176 |
177 | {% if collector.isHydratorsOverloaded() == false %} 178 | {% set panelClass = 'metric-disabled' %} 179 | {% set hydrationPanelClass = 'metric-disabled' %} 180 | {% elseif collector.getHydrationTotalTime() >= collector.getHydrationTimeAlert() %} 181 | {% set panelClass = null %} 182 | {% set hydrationPanelClass = 'metric-red' %} 183 | {% else %} 184 | {% set panelClass = null %} 185 | {% set hydrationPanelClass = null %} 186 | {% endif %} 187 |
188 | 189 | {% if collector.isHydratorsOverloaded() %} 190 | {{ collector.getHydrationTotalTime() }} ms 191 | {% else %} 192 | - 193 | {% endif %} 194 | 195 | 196 | {% if collector.isHydratorsOverloaded() %} 197 | Hydration ({{ collector.getHydrationTimePercent() }}%) 198 | {% else %} 199 | Hydration (disabled) 200 | {% endif %} 201 | 202 |
203 | {% if collector.isHydratorsOverloaded() %} 204 |
205 | 206 | {{ collector.getDoctrineTime() }} ms 207 | 208 | Total 209 |
210 | {% endif %} 211 |
212 | {{ collector.countManagedEntities() }} 213 | Managed entit{% if collector.countManagedEntities() <= 1 %}y{% else %}ies{% endif %} 214 |
215 |
216 | {{ collector.countLazyLoadedEntities() }} 217 | Lazy loaded entit{% if collector.countLazyLoadedEntities() > 1 %}ies{% else %}y{% endif %} 218 |
219 |
220 | 221 | {% if collector.isHydratorsOverloaded() == false %} 222 |
223 | You can see hydration time and total time by configuring 224 | steevanb/composer-overload-class 225 | (view how to configure it). 226 |
227 | {% endif %} 228 | 229 |

230 | {{ collector.getQueriesTime() }} ms, 231 | {{ collector.countQueries() }} quer{% if collector.countQueries() > 1 %}ies{% else %}y{% endif %}, 232 | {{ collector.countDifferentQueries() }} different{% if collector.countDifferentQueries() > 1 %}s{% endif %} 233 | {% if collector.isHydratorsOverloaded() %} 234 | ({{ collector.getQueriesTimePercent() }}%) 235 | {% endif %} 236 |

237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | {% for sql,queries in collector.getQueries() %} 248 | 249 | 254 | 261 | 268 | 349 | 350 | {% endfor %} 351 | 352 |
CountTypeTimeInfo
250 | 251 | {{ queries.data|length }} 252 | 253 | {% apply spaceless %} 255 | {% if queries.lazyLoadedEntities|length > 0 %} 256 | Lazy loading 257 | {% else %} 258 | Manual 259 | {% endif %} 260 | {% endapply %}{% apply spaceless %} 262 | {{ '%0.2f'|format(queries.queryTime) }} ms ({{ queries.queryTimePercent }}%)
263 | {% if collector.isHydratorsOverloaded() %} 264 | {{ '%0.2f'|format(queries.hydrationTime) }} ms ({{ queries.hydrationTimePercent }}%)
265 | {{ '%0.2f'|format(queries.queryTime + queries.hydrationTime) }} ms 266 | {% endif %} 267 | {% endapply %}
269 |
{{ sql|doctrine_pretty_query(highlight_only = true) }}
270 | 271 |
272 | Show parameters 277 | 278 | {% set countLazyLoadedEntities = 0 %} 279 | {% for lazyLoadedEntities in queries.lazyLoadedEntities %} 280 | {% set countLazyLoadedEntities = countLazyLoadedEntities + lazyLoadedEntities|length %} 281 | {% endfor %} 282 | 283 | {% if countLazyLoadedEntities > 0 %} 284 |      285 | Show {{ countLazyLoadedEntities }} entit{% if countLazyLoadedEntities == 1 %}y{% else %}ies{% endif %} 290 | {% endif %} 291 | 292 | {% set queryIndex = loop.index %} 293 |      294 | Show backtraces 299 |
300 | {% for backtrace in queries.backtraces %} 301 | Show backtrace #{{ loop.index }} 307 | {% if not loop.last %} 308 |      309 | {% endif %} 310 | {% endfor %} 311 |
312 | 313 |
314 | {% for query in queries.data %} 315 | #{{ loop.index }} 316 | {{ query.params|yaml_encode }} 317 |
318 | {% endfor %} 319 |
320 | 321 | {% if queries.lazyLoadedEntities|length > 0 %} 322 |
323 | {% for lazyLoadedeEntities in queries.lazyLoadedEntities %} 324 | #{{ loop.index }} 325 |
326 | {% for lazyLoadedeEntity in lazyLoadedeEntities %} 327 | {{ self.className(lazyLoadedeEntity) }}, 328 | {{ collector.identifiersAsString(lazyLoadedeEntity.identifiers) }} 329 |
330 | {% endfor %} 331 |
332 | {% endfor %} 333 |
334 | {% endif %} 335 | 336 | {% for backtrace in queries.backtraces %} 337 |
338 | {% if backtrace is null %} 339 | 340 | No backtrace informations available. Set doctrine_stats.query_backtrace to true to enable it. 341 | 342 | {% else %} 343 | {{ backtrace|raw }} 344 | {% endif %} 345 |
346 | {% endfor %} 347 |
348 |
353 | 354 | {% if collector.isHydratorsOverloaded() %} 355 |

356 | {{ collector.getHydrationTotalTime() }} ms hydration 357 | ({{ collector.getHydrationTimePercent() }}%) 358 |

359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | {% for hydrator,time in collector.getHydrationTimesByHydrator() %} 369 | {% set countHydratedEntities = collector.countHydratedEntities(hydrator) %} 370 | 371 | 372 | 385 | 399 | 400 | {% endfor %} 401 | 402 |
TimeHydratorEntities
{{ '%0.2f'|format(time) }} ms 373 | {{ self.className(collector.explodeClassParts(hydrator)) }} 374 | {% if countHydratedEntities > 0 %} 375 |
376 | {% for className,identifiers in collector.getHydratedEntities(hydrator) %} 377 | {{ self.className(collector.explodeClassParts(className)) }}[{{ identifiers|length }}] 378 |
379 | {{ self.identifiers(collector, identifiers) }} 380 |
381 | {% endfor %} 382 |
383 | {% endif %} 384 |
386 | {% if countHydratedEntities > 0 %} 387 | 392 | Show {{ countHydratedEntities }} 393 | entit{% if countHydratedEntities > 1 %}ies{% else %}y{% endif %} 394 | 395 | {% else %} 396 | Unavailable 397 | {% endif %} 398 |
403 | {% endif %} 404 | 405 |

406 | {{ collector.countManagedEntities() }} 407 | managed entit{% if collector.countManagedEntities() > 1 %}ies{% else %}y{% endif %} 408 |

409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | {% for className, stats in collector.getManagedEntities() %} 419 | 420 | 425 | 437 | 446 | 447 | {% endfor %} 448 | 449 |
CountEntityIdentifiers
421 | 422 | {{ stats.count }} 423 | 424 | 426 | {{ self.className(collector.explodeClassParts(className)) }} 427 | 428 |
429 | {% for id in stats.ids %} 430 | {{ id }} 431 | {% if not loop.last %} 432 |
433 | {% endif %} 434 | {% endfor %} 435 |
436 |
438 | 443 | Show identifier{% if stats.ids|length > 1 %}s{% endif %} 444 | 445 |
450 | 451 |

{{ collector.countLazyLoadedEntities() }} lazy loaded entit{% if collector.countLazyLoadedEntities() > 1 %}ies{% else %}y{% endif %}

452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | {% for lazyLoadedEntity in collector.getLazyLoadedEntities() %} 463 | 464 | 468 | 474 | 477 | 494 | 495 | {% endfor %} 496 | 497 |
ClassnameIdentifiersSame FQCNAssociations
465 | {{ lazyLoadedEntity.namespace }}\{{ lazyLoadedEntity.className }} 467 | 469 | {% for name,value in lazyLoadedEntity.identifiers %} 470 | {{ name }}: {{ value }} 471 | {% if loop.last == false %}, {% endif %} 472 | {% endfor %} 473 | 475 | {{ collector.countLazyLoadedClass(lazyLoadedEntity.namespace ~ '\\' ~ lazyLoadedEntity.className) }} 476 | 478 | 483 | Show {{ lazyLoadedEntity.associations|length }} 484 | association{% if lazyLoadedEntity.associations|length > 1 %}s{% endif %} 485 | 486 | 487 |
488 | {% for association in lazyLoadedEntity.associations %} 489 | {{ self.className(association) }}::${{ association.field }} 490 |
491 | {% endfor %} 492 |
493 |
498 | {% endblock %} 499 | 500 | {% macro className(classNameParts) %} 501 | {% apply spaceless %} 502 | {{ classNameParts.namespace }}\ 503 | {{ classNameParts.className }} 504 | {% endapply %} 505 | {% endmacro %} 506 | 507 | {% macro identifiers(collector, identifiers, merged = false) %} 508 | {% if not merged %} 509 | {% set identifiers = collector.mergeIdentifiers(identifiers) %} 510 | {% endif %} 511 | {% for identifier,count in identifiers %} 512 | {{ identifier }} 513 | {% if count > 1 %} 514 | (Loaded {{ count }} times) 515 | {% endif %} 516 | {% if loop.last == false %} 517 |
518 | {% endif %} 519 | {% endfor %} 520 | {% endmacro %} 521 | --------------------------------------------------------------------------------