312 lines
10 KiB
PHP
312 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Doctrine\Bundle\DoctrineBundle\DataCollector;
|
|
|
|
use Doctrine\DBAL\Types\Type;
|
|
use Doctrine\ORM\Cache\CacheConfiguration;
|
|
use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
|
|
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
|
|
use Doctrine\ORM\Configuration;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
|
use Doctrine\ORM\Tools\SchemaValidator;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
|
|
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector;
|
|
use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Throwable;
|
|
|
|
use function array_map;
|
|
use function array_sum;
|
|
use function assert;
|
|
use function count;
|
|
use function usort;
|
|
|
|
/**
|
|
* @psalm-type QueryType = array{
|
|
* executionMS: float,
|
|
* explainable: bool,
|
|
* sql: string,
|
|
* params: ?array<array-key, mixed>,
|
|
* runnable: bool,
|
|
* types: ?array<array-key, Type|int|string|null>,
|
|
* }
|
|
* @psalm-type DataType = array{
|
|
* caches: array{
|
|
* enabled: bool,
|
|
* counts: array<"puts"|"hits"|"misses", int>,
|
|
* log_enabled: bool,
|
|
* regions: array<"puts"|"hits"|"misses", array<string, int>>,
|
|
* },
|
|
* connections: list<string>,
|
|
* entities: array<string, array<class-string, class-string>>,
|
|
* errors: array<string, array<class-string, list<string>>>,
|
|
* managers: list<string>,
|
|
* queries: array<string, list<QueryType>>,
|
|
* }
|
|
* @psalm-property DataType $data
|
|
*/
|
|
class DoctrineDataCollector extends BaseCollector
|
|
{
|
|
private ManagerRegistry $registry;
|
|
private ?int $invalidEntityCount = null;
|
|
|
|
/**
|
|
* @var mixed[][]|null
|
|
* @psalm-var ?array<string, list<QueryType&array{count: int, index: int, executionPercent: float}>>
|
|
*/
|
|
private ?array $groupedQueries = null;
|
|
|
|
private bool $shouldValidateSchema;
|
|
|
|
public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true, ?DebugDataHolder $debugDataHolder = null)
|
|
{
|
|
$this->registry = $registry;
|
|
$this->shouldValidateSchema = $shouldValidateSchema;
|
|
|
|
if ($debugDataHolder === null) {
|
|
parent::__construct($registry);
|
|
} else {
|
|
parent::__construct($registry, $debugDataHolder);
|
|
}
|
|
}
|
|
|
|
public function collect(Request $request, Response $response, ?Throwable $exception = null): void
|
|
{
|
|
parent::collect($request, $response, $exception);
|
|
|
|
$errors = [];
|
|
$entities = [];
|
|
$caches = [
|
|
'enabled' => false,
|
|
'log_enabled' => false,
|
|
'counts' => [
|
|
'puts' => 0,
|
|
'hits' => 0,
|
|
'misses' => 0,
|
|
],
|
|
'regions' => [
|
|
'puts' => [],
|
|
'hits' => [],
|
|
'misses' => [],
|
|
],
|
|
];
|
|
|
|
foreach ($this->registry->getManagers() as $name => $em) {
|
|
assert($em instanceof EntityManagerInterface);
|
|
if ($this->shouldValidateSchema) {
|
|
$entities[$name] = [];
|
|
|
|
$factory = $em->getMetadataFactory();
|
|
$validator = new SchemaValidator($em);
|
|
|
|
assert($factory instanceof AbstractClassMetadataFactory);
|
|
|
|
foreach ($factory->getLoadedMetadata() as $class) {
|
|
assert($class instanceof ClassMetadataInfo);
|
|
if (isset($entities[$name][$class->getName()])) {
|
|
continue;
|
|
}
|
|
|
|
$classErrors = $validator->validateClass($class);
|
|
$r = $class->getReflectionClass();
|
|
$entities[$name][$class->getName()] = [
|
|
'class' => $class->getName(),
|
|
'file' => $r->getFileName(),
|
|
'line' => $r->getStartLine(),
|
|
];
|
|
|
|
if (empty($classErrors)) {
|
|
continue;
|
|
}
|
|
|
|
$errors[$name][$class->getName()] = $classErrors;
|
|
}
|
|
}
|
|
|
|
$emConfig = $em->getConfiguration();
|
|
assert($emConfig instanceof Configuration);
|
|
$slcEnabled = $emConfig->isSecondLevelCacheEnabled();
|
|
|
|
if (! $slcEnabled) {
|
|
continue;
|
|
}
|
|
|
|
$caches['enabled'] = true;
|
|
|
|
$cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration();
|
|
assert($cacheConfiguration instanceof CacheConfiguration);
|
|
$cacheLoggerChain = $cacheConfiguration->getCacheLogger();
|
|
assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null);
|
|
|
|
if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) {
|
|
continue;
|
|
}
|
|
|
|
$cacheLoggerStats = $cacheLoggerChain->getLogger('statistics');
|
|
assert($cacheLoggerStats instanceof StatisticsCacheLogger);
|
|
$caches['log_enabled'] = true;
|
|
|
|
$caches['counts']['puts'] += $cacheLoggerStats->getPutCount();
|
|
$caches['counts']['hits'] += $cacheLoggerStats->getHitCount();
|
|
$caches['counts']['misses'] += $cacheLoggerStats->getMissCount();
|
|
|
|
foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) {
|
|
if (! isset($caches['regions']['puts'][$key])) {
|
|
$caches['regions']['puts'][$key] = 0;
|
|
}
|
|
|
|
$caches['regions']['puts'][$key] += $value;
|
|
}
|
|
|
|
foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) {
|
|
if (! isset($caches['regions']['hits'][$key])) {
|
|
$caches['regions']['hits'][$key] = 0;
|
|
}
|
|
|
|
$caches['regions']['hits'][$key] += $value;
|
|
}
|
|
|
|
foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) {
|
|
if (! isset($caches['regions']['misses'][$key])) {
|
|
$caches['regions']['misses'][$key] = 0;
|
|
}
|
|
|
|
$caches['regions']['misses'][$key] += $value;
|
|
}
|
|
}
|
|
|
|
$this->data['entities'] = $entities;
|
|
$this->data['errors'] = $errors;
|
|
$this->data['caches'] = $caches;
|
|
$this->groupedQueries = null;
|
|
}
|
|
|
|
/** @return array<string, array<string, string>> */
|
|
public function getEntities()
|
|
{
|
|
return $this->data['entities'];
|
|
}
|
|
|
|
/** @return array<string, array<string, list<string>>> */
|
|
public function getMappingErrors()
|
|
{
|
|
return $this->data['errors'];
|
|
}
|
|
|
|
/** @return int */
|
|
public function getCacheHitsCount()
|
|
{
|
|
return $this->data['caches']['counts']['hits'];
|
|
}
|
|
|
|
/** @return int */
|
|
public function getCachePutsCount()
|
|
{
|
|
return $this->data['caches']['counts']['puts'];
|
|
}
|
|
|
|
/** @return int */
|
|
public function getCacheMissesCount()
|
|
{
|
|
return $this->data['caches']['counts']['misses'];
|
|
}
|
|
|
|
/** @return bool */
|
|
public function getCacheEnabled()
|
|
{
|
|
return $this->data['caches']['enabled'];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array<string, int>>
|
|
* @psalm-return array<"puts"|"hits"|"misses", array<string, int>>
|
|
*/
|
|
public function getCacheRegions()
|
|
{
|
|
return $this->data['caches']['regions'];
|
|
}
|
|
|
|
/** @return array<string, int> */
|
|
public function getCacheCounts()
|
|
{
|
|
return $this->data['caches']['counts'];
|
|
}
|
|
|
|
/** @return int */
|
|
public function getInvalidEntityCount()
|
|
{
|
|
return $this->invalidEntityCount ??= array_sum(array_map('count', $this->data['errors']));
|
|
}
|
|
|
|
/**
|
|
* @return string[][]
|
|
* @psalm-return array<string, list<QueryType&array{count: int, index: int, executionPercent: float}>>
|
|
*/
|
|
public function getGroupedQueries()
|
|
{
|
|
if ($this->groupedQueries !== null) {
|
|
return $this->groupedQueries;
|
|
}
|
|
|
|
$this->groupedQueries = [];
|
|
$totalExecutionMS = 0;
|
|
foreach ($this->data['queries'] as $connection => $queries) {
|
|
$connectionGroupedQueries = [];
|
|
foreach ($queries as $i => $query) {
|
|
$key = $query['sql'];
|
|
if (! isset($connectionGroupedQueries[$key])) {
|
|
$connectionGroupedQueries[$key] = $query;
|
|
$connectionGroupedQueries[$key]['executionMS'] = 0;
|
|
$connectionGroupedQueries[$key]['count'] = 0;
|
|
$connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'.
|
|
}
|
|
|
|
$connectionGroupedQueries[$key]['executionMS'] += $query['executionMS'];
|
|
$connectionGroupedQueries[$key]['count']++;
|
|
$totalExecutionMS += $query['executionMS'];
|
|
}
|
|
|
|
usort($connectionGroupedQueries, static function ($a, $b) {
|
|
if ($a['executionMS'] === $b['executionMS']) {
|
|
return 0;
|
|
}
|
|
|
|
return $a['executionMS'] < $b['executionMS'] ? 1 : -1;
|
|
});
|
|
$this->groupedQueries[$connection] = $connectionGroupedQueries;
|
|
}
|
|
|
|
foreach ($this->groupedQueries as $connection => $queries) {
|
|
foreach ($queries as $i => $query) {
|
|
$this->groupedQueries[$connection][$i]['executionPercent'] =
|
|
$this->executionTimePercentage($query['executionMS'], $totalExecutionMS);
|
|
}
|
|
}
|
|
|
|
return $this->groupedQueries;
|
|
}
|
|
|
|
private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float
|
|
{
|
|
if (! $totalExecutionTimeMS) {
|
|
return 0;
|
|
}
|
|
|
|
return $executionTimeMS / $totalExecutionTimeMS * 100;
|
|
}
|
|
|
|
/** @return int */
|
|
public function getGroupedQueryCount()
|
|
{
|
|
$count = 0;
|
|
foreach ($this->getGroupedQueries() as $connectionGroupedQueries) {
|
|
$count += count($connectionGroupedQueries);
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
}
|