vendor/doctrine/doctrine-bundle/DataCollector/DoctrineDataCollector.php line 85

Open in your IDE?
  1. <?php
  2. namespace Doctrine\Bundle\DoctrineBundle\DataCollector;
  3. use Doctrine\DBAL\Types\Type;
  4. use Doctrine\ORM\Cache\CacheConfiguration;
  5. use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
  6. use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
  7. use Doctrine\ORM\Configuration;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  10. use Doctrine\ORM\Tools\SchemaValidator;
  11. use Doctrine\Persistence\ManagerRegistry;
  12. use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
  13. use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector;
  14. use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Throwable;
  18. use function array_map;
  19. use function array_sum;
  20. use function assert;
  21. use function count;
  22. use function usort;
  23. /**
  24. * @psalm-type QueryType = array{
  25. * executionMS: float,
  26. * explainable: bool,
  27. * sql: string,
  28. * params: ?array<array-key, mixed>,
  29. * runnable: bool,
  30. * types: ?array<array-key, Type|int|string|null>,
  31. * }
  32. * @psalm-type DataType = array{
  33. * caches: array{
  34. * enabled: bool,
  35. * counts: array<"puts"|"hits"|"misses", int>,
  36. * log_enabled: bool,
  37. * regions: array<"puts"|"hits"|"misses", array<string, int>>,
  38. * },
  39. * connections: list<string>,
  40. * entities: array<string, array<class-string, class-string>>,
  41. * errors: array<string, array<class-string, list<string>>>,
  42. * managers: list<string>,
  43. * queries: array<string, list<QueryType>>,
  44. * }
  45. * @psalm-property DataType $data
  46. */
  47. class DoctrineDataCollector extends BaseCollector
  48. {
  49. /** @var ManagerRegistry */
  50. private $registry;
  51. /** @var int|null */
  52. private $invalidEntityCount;
  53. /**
  54. * @var mixed[][]
  55. * @psalm-var ?array<string, list<QueryType&array{count: int, index: int, executionPercent: float}>>
  56. */
  57. private $groupedQueries;
  58. /** @var bool */
  59. private $shouldValidateSchema;
  60. /** @psalm-suppress UndefinedClass */
  61. public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true, ?DebugDataHolder $debugDataHolder = null)
  62. {
  63. $this->registry = $registry;
  64. $this->shouldValidateSchema = $shouldValidateSchema;
  65. if ($debugDataHolder === null) {
  66. parent::__construct($registry);
  67. } else {
  68. /** @psalm-suppress TooManyArguments */
  69. parent::__construct($registry, $debugDataHolder);
  70. }
  71. }
  72. /**
  73. * {@inheritdoc}
  74. */
  75. public function collect(Request $request, Response $response, ?Throwable $exception = null)
  76. {
  77. parent::collect($request, $response, $exception);
  78. $errors = [];
  79. $entities = [];
  80. $caches = [
  81. 'enabled' => false,
  82. 'log_enabled' => false,
  83. 'counts' => [
  84. 'puts' => 0,
  85. 'hits' => 0,
  86. 'misses' => 0,
  87. ],
  88. 'regions' => [
  89. 'puts' => [],
  90. 'hits' => [],
  91. 'misses' => [],
  92. ],
  93. ];
  94. foreach ($this->registry->getManagers() as $name => $em) {
  95. assert($em instanceof EntityManagerInterface);
  96. if ($this->shouldValidateSchema) {
  97. $entities[$name] = [];
  98. $factory = $em->getMetadataFactory();
  99. $validator = new SchemaValidator($em);
  100. assert($factory instanceof AbstractClassMetadataFactory);
  101. foreach ($factory->getLoadedMetadata() as $class) {
  102. assert($class instanceof ClassMetadataInfo);
  103. if (isset($entities[$name][$class->getName()])) {
  104. continue;
  105. }
  106. $classErrors = $validator->validateClass($class);
  107. $entities[$name][$class->getName()] = $class->getName();
  108. if (empty($classErrors)) {
  109. continue;
  110. }
  111. $errors[$name][$class->getName()] = $classErrors;
  112. }
  113. }
  114. $emConfig = $em->getConfiguration();
  115. assert($emConfig instanceof Configuration);
  116. $slcEnabled = $emConfig->isSecondLevelCacheEnabled();
  117. if (! $slcEnabled) {
  118. continue;
  119. }
  120. $caches['enabled'] = true;
  121. $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration();
  122. assert($cacheConfiguration instanceof CacheConfiguration);
  123. $cacheLoggerChain = $cacheConfiguration->getCacheLogger();
  124. assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null);
  125. if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) {
  126. continue;
  127. }
  128. $cacheLoggerStats = $cacheLoggerChain->getLogger('statistics');
  129. assert($cacheLoggerStats instanceof StatisticsCacheLogger);
  130. $caches['log_enabled'] = true;
  131. $caches['counts']['puts'] += $cacheLoggerStats->getPutCount();
  132. $caches['counts']['hits'] += $cacheLoggerStats->getHitCount();
  133. $caches['counts']['misses'] += $cacheLoggerStats->getMissCount();
  134. foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) {
  135. if (! isset($caches['regions']['puts'][$key])) {
  136. $caches['regions']['puts'][$key] = 0;
  137. }
  138. $caches['regions']['puts'][$key] += $value;
  139. }
  140. foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) {
  141. if (! isset($caches['regions']['hits'][$key])) {
  142. $caches['regions']['hits'][$key] = 0;
  143. }
  144. $caches['regions']['hits'][$key] += $value;
  145. }
  146. foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) {
  147. if (! isset($caches['regions']['misses'][$key])) {
  148. $caches['regions']['misses'][$key] = 0;
  149. }
  150. $caches['regions']['misses'][$key] += $value;
  151. }
  152. }
  153. $this->data['entities'] = $entities;
  154. $this->data['errors'] = $errors;
  155. $this->data['caches'] = $caches;
  156. $this->groupedQueries = null;
  157. }
  158. /** @return array<string, array<string, string>> */
  159. public function getEntities()
  160. {
  161. return $this->data['entities'];
  162. }
  163. /** @return array<string, array<string, list<string>>> */
  164. public function getMappingErrors()
  165. {
  166. return $this->data['errors'];
  167. }
  168. /** @return int */
  169. public function getCacheHitsCount()
  170. {
  171. return $this->data['caches']['counts']['hits'];
  172. }
  173. /** @return int */
  174. public function getCachePutsCount()
  175. {
  176. return $this->data['caches']['counts']['puts'];
  177. }
  178. /** @return int */
  179. public function getCacheMissesCount()
  180. {
  181. return $this->data['caches']['counts']['misses'];
  182. }
  183. /** @return bool */
  184. public function getCacheEnabled()
  185. {
  186. return $this->data['caches']['enabled'];
  187. }
  188. /**
  189. * @return array<string, array<string, int>>
  190. * @psalm-return array<"puts"|"hits"|"misses", array<string, int>>
  191. */
  192. public function getCacheRegions()
  193. {
  194. return $this->data['caches']['regions'];
  195. }
  196. /** @return array<string, int> */
  197. public function getCacheCounts()
  198. {
  199. return $this->data['caches']['counts'];
  200. }
  201. /** @return int */
  202. public function getInvalidEntityCount()
  203. {
  204. if ($this->invalidEntityCount === null) {
  205. $this->invalidEntityCount = array_sum(array_map('count', $this->data['errors']));
  206. }
  207. return $this->invalidEntityCount;
  208. }
  209. /**
  210. * @return string[][]
  211. * @psalm-return array<string, list<QueryType&array{count: int, index: int, executionPercent: float}>>
  212. */
  213. public function getGroupedQueries()
  214. {
  215. if ($this->groupedQueries !== null) {
  216. return $this->groupedQueries;
  217. }
  218. $this->groupedQueries = [];
  219. $totalExecutionMS = 0;
  220. foreach ($this->data['queries'] as $connection => $queries) {
  221. $connectionGroupedQueries = [];
  222. foreach ($queries as $i => $query) {
  223. $key = $query['sql'];
  224. if (! isset($connectionGroupedQueries[$key])) {
  225. $connectionGroupedQueries[$key] = $query;
  226. $connectionGroupedQueries[$key]['executionMS'] = 0;
  227. $connectionGroupedQueries[$key]['count'] = 0;
  228. $connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'.
  229. }
  230. $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS'];
  231. $connectionGroupedQueries[$key]['count']++;
  232. $totalExecutionMS += $query['executionMS'];
  233. }
  234. usort($connectionGroupedQueries, static function ($a, $b) {
  235. if ($a['executionMS'] === $b['executionMS']) {
  236. return 0;
  237. }
  238. return $a['executionMS'] < $b['executionMS'] ? 1 : -1;
  239. });
  240. $this->groupedQueries[$connection] = $connectionGroupedQueries;
  241. }
  242. foreach ($this->groupedQueries as $connection => $queries) {
  243. foreach ($queries as $i => $query) {
  244. $this->groupedQueries[$connection][$i]['executionPercent'] =
  245. $this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
  246. }
  247. }
  248. return $this->groupedQueries;
  249. }
  250. private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float
  251. {
  252. if (! $totalExecutionTimeMS) {
  253. return 0;
  254. }
  255. return $executionTimeMS / $totalExecutionTimeMS * 100;
  256. }
  257. /** @return int */
  258. public function getGroupedQueryCount()
  259. {
  260. $count = 0;
  261. foreach ($this->getGroupedQueries() as $connectionGroupedQueries) {
  262. $count += count($connectionGroupedQueries);
  263. }
  264. return $count;
  265. }
  266. }