<?php
namespace App\Package\Toolkit\Doctrine\DataCollector;
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector,
Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\Cache\Logging\{ CacheLoggerChain, StatisticsCacheLogger };
use Doctrine\ORM\Configuration,
Doctrine\ORM\Mapping\ClassMetadataFactory,
Doctrine\ORM\Version,
Doctrine\DBAL\Logging\DebugStack,
Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpFoundation\{ Request, Response };
use Throwable;
use Symfony\Bridge\Doctrine\DataCollector\ObjectParameter;
use App\Package\Toolkit\Doctrine\DataCollector\SchemaValidator;
/**
* DataCollector
*
* Collects informations for profiler
*
* @brief manually setting logger as default logger from loggers
*
* @origin
* - type: extended and overwritten
* - vendor: doctrine/doctrine-bundle [ version = 1.6 ]
* - class: Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector
* - reason: doctrine inheritance system interference (see Documentation\CHANGELOG.md)
* (had to manually set logger as default logger from loggers)
*
* @overwritten
* - __construct
* - collect
*
* @copied [ from DoctrineDataCollector (parent) ]
* - $registry
*
* @copied [ from parent of DoctrineDataCollector -
* Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector (parent of parent) ]
* - $connections
* - $managers
* - $loggers
*
* @copiedAndRenamed [ from parent of DoctrineDataCollector -
* Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector (parent of parent) ]
* - sanitizeQueries => renamed to => customSanitizeQueries
* - sanitizeQuery => renamed to => customSanitizeQuery
* - sanitizeParam => renamed to => customSanitizeParam
*
* @see @openformCustomized tags for customized content
* modified functions are marked with @OVEWRITTEN
* renamed and modified functions are marekd with @RENAMED_AND_MODIFIED
*
* @author Doctrine
* @author Daniel Balowski <d.balowski@openform.pl> (_developer)
* @copyright Openform
* @since 03.2019
*/
class DataCollector extends DoctrineDataCollector
{
/**
* Copied from DoctrineDataCollector (parent)
*
* @var ManagerRegistry
*/
protected $registry;
/**
* Copied from Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector (parent of parent)
*
* @var array
*/
protected $connections;
/**
* Copied from Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector (parent of parent)
*
* @var array
*/
protected $managers;
/**
* Copied from Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector (parent of parent)
*
* @var array
*/
protected $loggers = [];
/**
* @OVERWRITTEN
*
* @openformCustomized
* - added check if logger passed then set default logger in loggers
*
* {@inheritDoc}
*/
public function __construct(ManagerRegistry $registry, DebugStack $logger = null)
{
$this->registry = $registry;
$this->connections = $registry->getConnectionNames();
$this->managers = $registry->getManagerNames();
if ($logger) {
$this->loggers['default'] = $logger;
}
parent::__construct($registry);
}
/**
* @OVERWRITTEN
*
* @openformCustomized
* - changed to use customSanitizeQueries instead of sanitizeQueries
*
* {@inheritDoc}
*/
public function collect(Request $request, Response $response, ?Throwable $exception = null)
{
$queries = array();
foreach ($this->loggers as $name => $logger) {
$queries[$name] = $this->customSanitizeQueries($name, $logger->queries);
}
$this->data = array(
'queries' => $queries,
'connections' => $this->connections,
'managers' => $this->managers,
);
$errors = [];
$entities = [];
$caches = [
'enabled' => false,
'log_enabled' => false,
'counts' => [
'puts' => 0,
'hits' => 0,
'misses' => 0,
],
'regions' => [
'puts' => [],
'hits' => [],
'misses' => [],
],
];
/** @var \Doctrine\ORM\EntityManagerInterface $em */
foreach ($this->registry->getManagers() as $name => $em) {
$entities[$name] = [];
/** @var ClassMetadataFactory $factory */
$factory = $em->getMetadataFactory();
$validator = new SchemaValidator($em);
/** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $class*/
foreach ($factory->getLoadedMetadata() as $class) {
if (isset($entities[$name][$class->getName()])) {
continue;
}
$classErrors = $validator->validateClass($class);
$entities[$name][$class->getName()] = $class->getName();
if (empty($classErrors)) {
continue;
}
$errors[$name][$class->getName()] = $classErrors;
}
if (version_compare(Version::VERSION, '2.5.0-DEV') < 0) {
continue;
}
/** @var Configuration $emConfig */
$emConfig = $em->getConfiguration();
$slcEnabled = $emConfig->isSecondLevelCacheEnabled();
if (! $slcEnabled) {
continue;
}
$caches['enabled'] = true;
/** @var \Doctrine\ORM\Cache\CacheConfiguration */
$cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration();
/** @var \Doctrine\ORM\Cache\Logging\CacheLoggerChain */
$cacheLoggerChain = $cacheConfiguration->getCacheLogger();
if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) {
continue;
}
/** @var StatisticsCacheLogger $cacheLoggerStats */
$cacheLoggerStats = $cacheLoggerChain->getLogger('statistics');
$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;
}
}
// HttpKernel < 3.2 compatibility layer
if (method_exists($this, 'cloneVar')) {
// Might be good idea to replicate this block in doctrine bridge so we can drop this from here after some time.
// This code is compatible with such change, because cloneVar is supposed to check if input is already cloned.
foreach ($this->data['queries'] as &$queries) {
foreach ($queries as &$query) {
$query['params'] = $this->cloneVar($query['params']);
}
}
}
$this->data['entities'] = $entities;
$this->data['errors'] = $errors;
$this->data['caches'] = $caches;
$this->groupedQueries = null;
}
/**
* @RENAMED_AND_MODIFIED
*
* @openformCustomized
* - changed to use customSanitizeQuery instead of sanitizeQuery
*
* @param $connectionName
* @param $queries
*
* @return array
*/
protected function customSanitizeQueries($connectionName, $queries)
{
foreach ($queries as $i => $query) {
$queries[$i] = $this->customSanitizeQuery($connectionName, $query);
}
return $queries;
}
/**
* @RENAMED_AND_MODIFIED
*
* @openformCustomized
* - changed to use customSanitizeParam instead of sanitizeParam
*
* @param $connectionName
* @param $query
*
* @return array
*/
protected function customSanitizeQuery($connectionName, $query)
{
$query['explainable'] = true;
$query['runnable'] = true;
if (null === $query['params']) {
$query['params'] = array();
}
if (!\is_array($query['params'])) {
$query['params'] = array($query['params']);
}
if (!\is_array($query['types'])) {
$query['types'] = [];
}
foreach ($query['params'] as $j => $param) {
$e = null;
if (isset($query['types'][$j])) {
// Transform the param according to the type
$type = $query['types'][$j];
if (\is_string($type)) {
$type = Type::getType($type);
}
if ($type instanceof Type) {
$query['types'][$j] = $type->getBindingType();
try {
$param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform());
} catch (\TypeError $e) {
} catch (\Doctrine\DBAL\Types\ConversionException $e) {
}
}
}
[$query['params'][$j], $explainable, $runnable] = $this->customSanitizeParam($param, $e);
if (!$explainable) {
$query['explainable'] = false;
}
if (!$runnable) {
$query['runnable'] = false;
}
}
$query['params'] = $this->cloneVar($query['params']);
return $query;
}
/**
* @RENAMED_AND_MODIFIED
*
* @openformCustomized
* - changed to use customSanitizeParam instead of sanitizeParam
*
* Sanitizes a param
*
* The return value is an array with the sanitized value and a boolean
* indicating if the original value was kept (allowing to use the sanitized
* value to explain the query)
*
* @param mixed $var
*
* @return array
*/
protected function customSanitizeParam($var, ?\Throwable $error)
{
if (\is_object($var)) {
return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error];
}
if ($error) {
return ['⚠ '.$error->getMessage(), false, false];
}
if (\is_array($var)) {
$a = array();
$explainable = $runnable = true;
foreach ($var as $k => $v) {
[$value, $e, $r] = $this->customSanitizeParam($v, null);
$explainable = $explainable && $e;
$runnable = $runnable && $r;
$a[$k] = $value;
}
return [$a, $explainable, $runnable];
}
if (\is_resource($var)) {
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false];
}
return [$var, true, true];
}
}