vendor/doctrine/doctrine-bundle/src/DataCollector/DoctrineDataCollector.php line 108

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