merged branch stof/doctrine_collector (PR #3173)
Commits -------e37783f
[DoctrineBridge] Refactored the query sanitization in the collector3b260d2
Refactored the collector to separate the loggers per connection Discussion ---------- Doctrine collector Bug fix: no Feature addition: yes Backwards compatibility break: yes (for the end user, it will require deleting old profiler data) Symfony2 tests pass: yes ![Build Status](https://secure.travis-ci.org/stof/symfony.png?branch=doctrine_collector) This refactors the Doctrine collector to allow implementing doctrine/DoctrineBundle#7 The first commit splits the logging of queries per connection to be able to know which connection was used instead of using a shared stack. The second commit refactors the sanitation of the parameters to apply the DBAL conversion and then keep the param whenever possible (i.e. when we are sure it is serializable). Such queries will then be explainable in the profiler as we will be able to use the parameters again. Due to the way PDO works, the only cases where we would get an unexplainable queries due to the parameters are queries using a LOB parameter (as it is a resource) or broken queries (passing an object to PDO for instance). And this second case does not make sense to explain the query of course. --------------------------------------------------------------------------- by stof at 2012-01-23T12:32:16Z Merging this PR should be synchronized with the DoctrineBundle PR due to the BC break in the collector
This commit is contained in:
commit
fbbea2f369
@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\DataCollector;
|
|||||||
|
|
||||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||||
use Doctrine\DBAL\Logging\DebugStack;
|
use Doctrine\DBAL\Logging\DebugStack;
|
||||||
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@ -24,15 +25,27 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
*/
|
*/
|
||||||
class DoctrineDataCollector extends DataCollector
|
class DoctrineDataCollector extends DataCollector
|
||||||
{
|
{
|
||||||
|
private $registry;
|
||||||
private $connections;
|
private $connections;
|
||||||
private $managers;
|
private $managers;
|
||||||
private $logger;
|
private $loggers = array();
|
||||||
|
|
||||||
public function __construct(ManagerRegistry $registry, DebugStack $logger = null)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
|
$this->registry = $registry;
|
||||||
$this->connections = $registry->getConnectionNames();
|
$this->connections = $registry->getConnectionNames();
|
||||||
$this->managers = $registry->getManagerNames();
|
$this->managers = $registry->getManagerNames();
|
||||||
$this->logger = $logger;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the stack logger for a connection.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param DebugStack $logger
|
||||||
|
*/
|
||||||
|
public function addLogger($name, DebugStack $logger)
|
||||||
|
{
|
||||||
|
$this->loggers[$name] = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,8 +53,13 @@ class DoctrineDataCollector extends DataCollector
|
|||||||
*/
|
*/
|
||||||
public function collect(Request $request, Response $response, \Exception $exception = null)
|
public function collect(Request $request, Response $response, \Exception $exception = null)
|
||||||
{
|
{
|
||||||
|
$queries = array();
|
||||||
|
foreach ($this->loggers as $name => $logger) {
|
||||||
|
$queries[$name] = $this->sanitizeQueries($name, $logger->queries);
|
||||||
|
}
|
||||||
|
|
||||||
$this->data = array(
|
$this->data = array(
|
||||||
'queries' => null !== $this->logger ? $this->sanitizeQueries($this->logger->queries) : array(),
|
'queries' => $queries,
|
||||||
'connections' => $this->connections,
|
'connections' => $this->connections,
|
||||||
'managers' => $this->managers,
|
'managers' => $this->managers,
|
||||||
);
|
);
|
||||||
@ -59,7 +77,7 @@ class DoctrineDataCollector extends DataCollector
|
|||||||
|
|
||||||
public function getQueryCount()
|
public function getQueryCount()
|
||||||
{
|
{
|
||||||
return count($this->data['queries']);
|
return array_sum(array_map('count', $this->data['queries']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getQueries()
|
public function getQueries()
|
||||||
@ -70,9 +88,11 @@ class DoctrineDataCollector extends DataCollector
|
|||||||
public function getTime()
|
public function getTime()
|
||||||
{
|
{
|
||||||
$time = 0;
|
$time = 0;
|
||||||
foreach ($this->data['queries'] as $query) {
|
foreach ($this->data['queries'] as $queries) {
|
||||||
|
foreach ($queries as $query) {
|
||||||
$time += $query['executionMS'];
|
$time += $query['executionMS'];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $time;
|
return $time;
|
||||||
}
|
}
|
||||||
@ -85,48 +105,73 @@ class DoctrineDataCollector extends DataCollector
|
|||||||
return 'db';
|
return 'db';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sanitizeQueries($queries)
|
private function sanitizeQueries($connectionName, $queries)
|
||||||
{
|
{
|
||||||
foreach ($queries as $i => $query) {
|
foreach ($queries as $i => $query) {
|
||||||
foreach ((array) $query['params'] as $j => $param) {
|
$queries[$i] = $this->sanitizeQuery($connectionName, $query);
|
||||||
$queries[$i]['params'][$j] = $this->varToString($param);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $queries;
|
return $queries;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function varToString($var)
|
private function sanitizeQuery($connectionName, $query)
|
||||||
|
{
|
||||||
|
$query['explainable'] = true;
|
||||||
|
$query['params'] = (array) $query['params'];
|
||||||
|
foreach ($query['params'] as $j => &$param) {
|
||||||
|
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();
|
||||||
|
$param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list($param, $explainable) = $this->sanitizeParam($param);
|
||||||
|
if (!$explainable) {
|
||||||
|
$query['explainable'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
private function sanitizeParam($var)
|
||||||
{
|
{
|
||||||
if (is_object($var)) {
|
if (is_object($var)) {
|
||||||
return sprintf('Object(%s)', get_class($var));
|
return array(sprintf('Object(%s)', get_class($var)), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($var)) {
|
if (is_array($var)) {
|
||||||
$a = array();
|
$a = array();
|
||||||
|
$original = true;
|
||||||
foreach ($var as $k => $v) {
|
foreach ($var as $k => $v) {
|
||||||
$a[] = sprintf('%s => %s', $k, $this->varToString($v));
|
list($value, $orig) = $this->sanitizeParam($v);
|
||||||
|
$original = $original && $orig;
|
||||||
|
$a[$k] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf("Array(%s)", implode(', ', $a));
|
return array($a, $original);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_resource($var)) {
|
if (is_resource($var)) {
|
||||||
return sprintf('Resource(%s)', get_resource_type($var));
|
return array(sprintf('Resource(%s)', get_resource_type($var)), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $var) {
|
return array($var, true);
|
||||||
return 'null';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === $var) {
|
|
||||||
return 'false';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === $var) {
|
|
||||||
return 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $var;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Tests\Bridge\Doctrine\DataCollector;
|
namespace Symfony\Tests\Bridge\Doctrine\DataCollector;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||||
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
|
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@ -70,49 +71,58 @@ class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider paramProvider
|
* @dataProvider paramProvider
|
||||||
*/
|
*/
|
||||||
public function testCollectQueries($param, $expected)
|
public function testCollectQueries($param, $types, $expected, $explainable)
|
||||||
{
|
{
|
||||||
$queries = array(
|
$queries = array(
|
||||||
array('sql' => "SELECT * FROM table1 WHERE field1 = ?1", 'params' => array($param), 'types' => array(), 'executionMS' => 1)
|
array('sql' => "SELECT * FROM table1 WHERE field1 = ?1", 'params' => array($param), 'types' => $types, 'executionMS' => 1)
|
||||||
);
|
);
|
||||||
$c = $this->createCollector($queries);
|
$c = $this->createCollector($queries);
|
||||||
$c->collect(new Request(), new Response());
|
$c->collect(new Request(), new Response());
|
||||||
|
|
||||||
$collected_queries = $c->getQueries();
|
$collected_queries = $c->getQueries();
|
||||||
$this->assertEquals($expected, $collected_queries[0]['params'][0]);
|
$this->assertEquals($expected, $collected_queries['default'][0]['params'][0]);
|
||||||
|
$this->assertEquals($explainable, $collected_queries['default'][0]['explainable']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider paramProvider
|
* @dataProvider paramProvider
|
||||||
*/
|
*/
|
||||||
public function testSerialization($param, $expected)
|
public function testSerialization($param, $types, $expected, $explainable)
|
||||||
{
|
{
|
||||||
$queries = array(
|
$queries = array(
|
||||||
array('sql' => "SELECT * FROM table1 WHERE field1 = ?1", 'params' => array($param), 'types' => array(), 'executionMS' => 1)
|
array('sql' => "SELECT * FROM table1 WHERE field1 = ?1", 'params' => array($param), 'types' => $types, 'executionMS' => 1)
|
||||||
);
|
);
|
||||||
$c = $this->createCollector($queries);
|
$c = $this->createCollector($queries);
|
||||||
$c->collect(new Request(), new Response());
|
$c->collect(new Request(), new Response());
|
||||||
$c = unserialize(serialize($c));
|
$c = unserialize(serialize($c));
|
||||||
|
|
||||||
$collected_queries = $c->getQueries();
|
$collected_queries = $c->getQueries();
|
||||||
$this->assertEquals($expected, $collected_queries[0]['params'][0]);
|
$this->assertEquals($expected, $collected_queries['default'][0]['params'][0]);
|
||||||
|
$this->assertEquals($explainable, $collected_queries['default'][0]['explainable']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paramProvider()
|
public function paramProvider()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
array('some value', 'some value'),
|
array('some value', array(), 'some value', true),
|
||||||
array(1, '1'),
|
array(1, array(), 1, true),
|
||||||
array(true, 'true'),
|
array(true, array(), true, true),
|
||||||
array(null, 'null'),
|
array(null, array(), null, true),
|
||||||
array(new \stdClass(), 'Object(stdClass)'),
|
array(new \DateTime('2011-09-11'), array('date'), '2011-09-11', true),
|
||||||
array(fopen(__FILE__, 'r'), 'Resource(stream)'),
|
array(fopen(__FILE__, 'r'), array(), 'Resource(stream)', false),
|
||||||
array(new \SplFileInfo(__FILE__), 'Object(SplFileInfo)'),
|
array(new \SplFileInfo(__FILE__), array(), 'Object(SplFileInfo)', false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createCollector($queries)
|
private function createCollector($queries)
|
||||||
{
|
{
|
||||||
|
$connection = $this->getMockBuilder('Doctrine\DBAL\Connection')
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$connection->expects($this->any())
|
||||||
|
->method('getDatabasePlatform')
|
||||||
|
->will($this->returnValue(new MySqlPlatform()));
|
||||||
|
|
||||||
$registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry');
|
$registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry');
|
||||||
$registry
|
$registry
|
||||||
->expects($this->any())
|
->expects($this->any())
|
||||||
@ -122,10 +132,16 @@ class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase
|
|||||||
->expects($this->any())
|
->expects($this->any())
|
||||||
->method('getManagerNames')
|
->method('getManagerNames')
|
||||||
->will($this->returnValue(array('default' => 'doctrine.orm.default_entity_manager')));
|
->will($this->returnValue(array('default' => 'doctrine.orm.default_entity_manager')));
|
||||||
|
$registry->expects($this->any())
|
||||||
|
->method('getConnection')
|
||||||
|
->will($this->returnValue($connection));
|
||||||
|
|
||||||
$logger = $this->getMock('Doctrine\DBAL\Logging\DebugStack');
|
$logger = $this->getMock('Doctrine\DBAL\Logging\DebugStack');
|
||||||
$logger->queries = $queries;
|
$logger->queries = $queries;
|
||||||
|
|
||||||
return new DoctrineDataCollector($registry, $logger);
|
$collector = new DoctrineDataCollector($registry);
|
||||||
|
$collector->addLogger('default', $logger);
|
||||||
|
|
||||||
|
return $collector;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user