merged branch stof/doctrine_collector (PR #3173)

Commits
-------

e37783f [DoctrineBridge] Refactored the query sanitization in the collector
3b260d2 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:
Fabien Potencier 2012-01-23 13:45:25 +01:00
commit fbbea2f369
2 changed files with 104 additions and 43 deletions

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\DataCollector;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\DBAL\Logging\DebugStack;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -24,15 +25,27 @@ use Symfony\Component\HttpFoundation\Response;
*/
class DoctrineDataCollector extends DataCollector
{
private $registry;
private $connections;
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->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)
{
$queries = array();
foreach ($this->loggers as $name => $logger) {
$queries[$name] = $this->sanitizeQueries($name, $logger->queries);
}
$this->data = array(
'queries' => null !== $this->logger ? $this->sanitizeQueries($this->logger->queries) : array(),
'queries' => $queries,
'connections' => $this->connections,
'managers' => $this->managers,
);
@ -59,7 +77,7 @@ class DoctrineDataCollector extends DataCollector
public function getQueryCount()
{
return count($this->data['queries']);
return array_sum(array_map('count', $this->data['queries']));
}
public function getQueries()
@ -70,8 +88,10 @@ class DoctrineDataCollector extends DataCollector
public function getTime()
{
$time = 0;
foreach ($this->data['queries'] as $query) {
$time += $query['executionMS'];
foreach ($this->data['queries'] as $queries) {
foreach ($queries as $query) {
$time += $query['executionMS'];
}
}
return $time;
@ -85,48 +105,73 @@ class DoctrineDataCollector extends DataCollector
return 'db';
}
private function sanitizeQueries($queries)
private function sanitizeQueries($connectionName, $queries)
{
foreach ($queries as $i => $query) {
foreach ((array) $query['params'] as $j => $param) {
$queries[$i]['params'][$j] = $this->varToString($param);
}
$queries[$i] = $this->sanitizeQuery($connectionName, $query);
}
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)) {
return sprintf('Object(%s)', get_class($var));
return array(sprintf('Object(%s)', get_class($var)), false);
}
if (is_array($var)) {
$a = array();
$original = true;
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)) {
return sprintf('Resource(%s)', get_resource_type($var));
return array(sprintf('Resource(%s)', get_resource_type($var)), false);
}
if (null === $var) {
return 'null';
}
if (false === $var) {
return 'false';
}
if (true === $var) {
return 'true';
}
return (string) $var;
return array($var, true);
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Tests\Bridge\Doctrine\DataCollector;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -70,49 +71,58 @@ class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase
/**
* @dataProvider paramProvider
*/
public function testCollectQueries($param, $expected)
public function testCollectQueries($param, $types, $expected, $explainable)
{
$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->collect(new Request(), new Response());
$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
*/
public function testSerialization($param, $expected)
public function testSerialization($param, $types, $expected, $explainable)
{
$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->collect(new Request(), new Response());
$c = unserialize(serialize($c));
$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()
{
return array(
array('some value', 'some value'),
array(1, '1'),
array(true, 'true'),
array(null, 'null'),
array(new \stdClass(), 'Object(stdClass)'),
array(fopen(__FILE__, 'r'), 'Resource(stream)'),
array(new \SplFileInfo(__FILE__), 'Object(SplFileInfo)'),
array('some value', array(), 'some value', true),
array(1, array(), 1, true),
array(true, array(), true, true),
array(null, array(), null, true),
array(new \DateTime('2011-09-11'), array('date'), '2011-09-11', true),
array(fopen(__FILE__, 'r'), array(), 'Resource(stream)', false),
array(new \SplFileInfo(__FILE__), array(), 'Object(SplFileInfo)', false),
);
}
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
->expects($this->any())
@ -122,10 +132,16 @@ class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase
->expects($this->any())
->method('getManagerNames')
->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->queries = $queries;
return new DoctrineDataCollector($registry, $logger);
$collector = new DoctrineDataCollector($registry);
$collector->addLogger('default', $logger);
return $collector;
}
}