DoctrineDataCollector.php 9.9 KB

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