Fixed the DataCollector

This commit is contained in:
Kévin Gomez 2013-11-01 17:49:56 +00:00
parent 44729e47f5
commit 3889566728
9 changed files with 664 additions and 0 deletions

View file

@ -0,0 +1,82 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\Controller;
use Propel\Runtime\Propel;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Response;
/**
* PanelController is designed to display information in the Propel Panel.
*
* @author William DURAND <william.durand1@gmail.com>
*/
class PanelController extends ContainerAware
{
/**
* This method renders the global Propel configuration.
*/
public function configurationAction()
{
$templating = $this->container->get('templating');
return $templating->renderResponse(
'PropelBundle:Panel:configuration.html.twig',
array(
'propel_version' => Propel::VERSION,
'configuration' => $this->container->getParameter('propel.configuration'),
'default_connection' => $this->container->getParameter('propel.dbal.default_connection'),
'logging' => $this->container->getParameter('propel.logging'),
)
);
}
/**
* Renders the profiler panel for the given token.
*
* @param string $token The profiler token
* @param string $connection The connection name
* @param integer $query
*
* @return Symfony\Component\HttpFoundation\Response A Response instance
*/
public function explainAction($token, $connection, $query)
{
$profiler = $this->container->get('profiler');
$profiler->disable();
$profile = $profiler->loadProfile($token);
$queries = $profile->getCollector('propel')->getQueries();
if (!isset($queries[$query])) {
return new Response('This query does not exist.');
}
// Open the connection
$con = Propel::getConnection($connection);
try {
$dataFetcher = $con->query('EXPLAIN ' . $queries[$query]['sql']);
$results = array();
while (($results[] = $dataFetcher->fetch(\PDO::FETCH_ASSOC)));
} catch (\Exception $e) {
return new Response('<div class="error">This query cannot be explained.</div>');
}
return $this->container->get('templating')->renderResponse(
'PropelBundle:Panel:explain.html.twig',
array(
'data' => $results,
'query' => $query,
)
);
}
}

View file

@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Propel\PropelBundle\DataCollector;
use Propel\PropelBundle\Logger\PropelLogger;
use Propel\Runtime\Util\Profiler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
/**
* The PropelDataCollector collector class collects information.
*
* @author Kévin Gomez <contact@kevingomez.fr>
*/
class PropelDataCollector extends DataCollector
{
protected $logger;
public function __construct(PropelLogger $logger)
{
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
$this->data = array(
'queries' => $this->buildQueries(),
'querycount' => $this->countQueries(),
);
}
/**
* Returns the collector name.
*
* @return string The collector name.
*/
public function getName()
{
return 'propel';
}
/**
* Returns queries.
*
* @return array Queries
*/
public function getQueries()
{
return $this->data['queries'];
}
/**
* Returns the query count.
*
* @return int The query count
*/
public function getQueryCount()
{
return $this->data['querycount'];
}
/**
* Returns the total time of queries.
*
* @return float The total time of queries
*/
public function getTime()
{
$time = 0;
foreach ($this->data['queries'] as $query) {
$time += (float) $query['time'];
}
return $time;
}
/**
* Creates an array of Build objects.
*
* @return array An array of Build objects
*/
private function buildQueries()
{
return $this->logger->getQueries();
}
/**
* Count queries.
*
* @return int The number of queries.
*/
private function countQueries()
{
return count($this->logger->getQueries());
}
}

102
Logger/PropelLogger.php Normal file
View file

@ -0,0 +1,102 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\Logger;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* @author Kévin Gomez <contact@kevingomez.fr>
*/
class PropelLogger implements LoggerInterface
{
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var array
*/
protected $queries = array();
/**
* @var Stopwatch
*/
protected $stopwatch;
use LoggerTrait;
/**
* Constructor.
*
* @param LoggerInterface $logger A LoggerInterface instance
* @param Stopwatch $stopwatch A Stopwatch instance
*/
public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null)
{
$this->logger = $logger;
$this->stopwatch = $stopwatch;
$this->isPrepared = false;
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
* @return null
*/
public function log($level, $message, array $context = array())
{
if (null === $this->logger) {
return;
}
$add = true;
if (null !== $this->stopwatch) {
$trace = debug_backtrace();
$method = $trace[3]['function'];
$watch = 'Propel Query '.(count($this->queries)+1);
if ('prepare' === $method) {
$this->isPrepared = true;
$this->stopwatch->start($watch, 'propel');
$add = false;
} elseif ($this->isPrepared) {
$this->isPrepared = false;
$event = $this->stopwatch->stop($watch);
}
}
if ($add && isset($event)) {
$connection = $trace[2]['object'];
$this->queries[] = array(
'sql' => $message,
'connection' => $connection->getName(),
'time' => $event->getDuration() / 1000,
'memory' => $event->getMemory(),
);
}
$this->logger->log($level, $message, $context);
}
public function getQueries()
{
return $this->queries;
}
}

View file

@ -30,6 +30,10 @@ class PropelBundle extends Bundle
public function boot()
{
$this->configureConnections();
if ($this->container->getParameter('propel.logging')) {
$this->configureLogging();
}
}
/**
@ -64,4 +68,18 @@ class PropelBundle extends Bundle
$serviceContainer->setConnectionManager($name, $manager);
}
}
protected function configureLogging()
{
$serviceContainer = Propel::getServiceContainer();
$serviceContainer->setLogger('defaultLogger', $this->container->get('propel.logger'));
foreach ($serviceContainer->getConnectionManagers() as $manager) {
$connection = $manager->getReadConnection($serviceContainer->getAdapter($manager->getName()));
$connection->setLogMethods(array_merge($connection->getLogMethods(), array('prepare')));
$connection = $manager->getWriteConnection();
$connection->setLogMethods(array_merge($connection->getLogMethods(), array('prepare')));
}
}
}

View file

@ -7,12 +7,30 @@
<parameters>
<parameter key="propel.dbal.default_connection">default</parameter>
<parameter key="propel.class">Propel\Runtime\Propel</parameter>
<parameter key="propel.schema_locator.class">Propel\PropelBundle\Service\SchemaLocator</parameter>
<parameter key="propel.data_collector.class">Propel\PropelBundle\DataCollector\PropelDataCollector</parameter>
<parameter key="propel.logger.class">Propel\PropelBundle\Logger\PropelLogger</parameter>
<parameter key="propel.twig.extension.syntax.class">Propel\PropelBundle\Twig\Extension\SyntaxExtension</parameter>
</parameters>
<services>
<service id="propel.schema_locator" class="%propel.schema_locator.class%">
<argument type="service" id="file_locator" />
</service>
<service id="propel.logger" class="%propel.logger.class%">
<argument type="service" id="logger" on-invalid="null" />
<argument type="service" id="debug.stopwatch" on-invalid="null" />
</service>
<service id="propel.data_collector" class="%propel.data_collector.class%" public="false">
<tag name="data_collector" template="PropelBundle:Collector:propel" id="propel" />
<argument type="service" id="propel.logger" />
</service>
<service id="propel.twig.extension.syntax" class="%propel.twig.extension.syntax.class%">
<tag name="twig.extension" />
</service>
</services>
</container>

View file

@ -0,0 +1,111 @@
{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
{% block toolbar %}
{# the web debug toolbar content #}
{% set icon %}
<img alt="Propel" src="data:image/png;base64,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" />
<span class="sf-toolbar-status">{{ collector.querycount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>DB Queries</b>
<span>{{ collector.querycount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Query time</b>
<span>{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
</div>
{% endset %}
{% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %}
{% endblock %}
{% block menu %}
{# the menu content #}
<span class="label">
<span class="icon"><img src="{{ asset('bundles/propel/images/profiler/propel.png') }}" alt="" /></span>
<strong>Propel</strong>
<span class="count">
<span>{{ collector.querycount }}</span>
<span>{{ '%0.0f'|format(collector.time * 1000) }} ms</span>
</span>
</span>
{% endblock %}
{% block panel %}
{# the panel content #}
<style type="text/css">
.SQLKeyword {
color: blue;
white-space: nowrap;
}
.SQLName {
color: #464646;
white-space: nowrap;
}
.SQLInfo, .SQLComment {
color: gray;
display: block;
font-size: 0.9em;
margin: 3px 0;
}
.SQLExplain {
margin: 5px;
}
.SQLExplain .error {
background-color: #F2DEDE;
border-color: #EED3D7;
color: #B94A48;
padding: 8px 35px 8px 14px;
font-weight: bold;
}
#content .SQLExplain h2 {
font-size: 17px;
margin-bottom: 0;
}
</style>
<h2>Queries</h2>
<table summary="Show logged queries">
<thead>
<tr>
<th>SQL queries</th>
</tr>
</thead>
<tbody>
{% if not collector.querycount %}
<tr><td>No queries.</td></tr>
{% else %}
{% for i, query in collector.queries %}
<tr>
<td>
<a name="propel-query-{{ i }}" ></a>
<code>{{ query.sql|format_sql }}</code>
{% if app.request.query.has('query') and app.request.query.get('query') == i %}
<div class="SQLExplain">
{% render controller('PropelBundle:Panel:explain', {
'token': token,
'panel': 'propel',
'query': app.request.query.get('query'),
'connection': app.request.query.get('connection')
}) %}
</div>
{% endif %}
<div class="SQLInfo">
Time: {{ query.time }} - Memory: {{ query.memory|format_memory }} - Connection: {{ query.connection }}
{% if app.request.query.get('query', -1) != i %}
- <a href="{{ path('_profiler', {'panel': 'propel', 'token': token, 'connection': query.connection, 'query': i}) }}#propel-query-{{ i }}">Explain the query</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
{% render controller('PropelBundle:Panel:configuration') %}
{% endblock %}

View file

@ -0,0 +1,69 @@
<h2>Propel configuration</h2>
<table summary="Current Propel configuration">
<thead>
</thead>
<tbody>
<tr>
<th>Propel version</th>
<td>{{ propel_version }}</td>
</tr>
<tr>
<th>Default connection</th>
<td>{{ default_connection }}</td>
<tr>
<th>Logging</th>
<td>{{ logging ? 'enabled' : 'disabled' }}</td>
</tr>
</tbody>
</table>
<h2>Propel connections</h2>
<table summary="Current Propel connections">
<thead>
<tr>
<th>Connection name</th>
<th colspan="2" style="text-align: center;">Configuration parameters</th>
</tr>
</thead>
<tbody>
{% for name, config in configuration %}
<tr>
<th rowspan="5" style="vertical-align: top;">
{{ name }}
</th>
<th>Adapter</th>
<td>{{ config.adapter }}</td>
</tr>
<tr>
<th>DSN</th>
<td>{{ config.connection.dsn }}</td>
</tr>
<tr>
<th>Class</th>
<td>{{ config.connection.classname }}</td>
</tr>
<tr>
<th>Options</th>
<td>
<ul>
{% for key, value in config.connection.options %}
<li>{{ key }} : {{ value }}</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th>Attributes</th>
<td>
<ul>
{% for key, value in config.connection.attributes %}
<li>{{ key }} : {{ value }}</li>
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,16 @@
<h2>Explanation</h2>
<table>
<tr>
{% for label in data[0]|keys %}
<th>{{ label }}</th>
{% endfor %}
</tr>
{% for row in data %}
<tr>
{% for item in row %}
<td>{{ item }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>

View file

@ -0,0 +1,139 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\Twig\Extension;
/**
* SyntaxExtension class
*
* @package PropelBundle
* @subpackage Extension
* @author William DURAND <william.durand1@gmail.com>
*/
class SyntaxExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
'format_sql' => new \Twig_Filter_Method($this, 'formatSQL', array('is_safe' => array('html'))),
'format_memory' => new \Twig_Filter_Method($this, 'formatMemory'),
);
}
public function getName()
{
return 'propel_syntax_extension';
}
/**
* Format a byte count into a human-readable representation.
*
* @param integer $bytes Byte count to convert. Can be negative.
* @param integer $precision How many decimals to include.
*
* @return string
*/
public function formatMemory($bytes, $precision = 3)
{
$absBytes = abs($bytes);
$sign = ($bytes == $absBytes) ? 1 : -1;
$suffix = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
$total = count($suffix);
for ($i = 0; $absBytes > 1024 && $i < $total; $i++) {
$absBytes /= 1024;
}
return self::toPrecision($sign * $absBytes, $precision) . $suffix[$i];
}
public function formatSQL($sql)
{
// list of keywords to prepend a newline in output
$newlines = array(
'FROM',
'(((FULL|LEFT|RIGHT)? ?(OUTER|INNER)?|CROSS|NATURAL)? JOIN)',
'VALUES',
'WHERE',
'ORDER BY',
'GROUP BY',
'HAVING',
'LIMIT',
);
// list of keywords to highlight
$keywords = array_merge($newlines, array(
// base
'SELECT', 'UPDATE', 'DELETE', 'INSERT', 'REPLACE',
'SET',
'INTO',
'AS',
'DISTINCT',
// most used methods
'COUNT',
'AVG',
'MIN',
'MAX',
// joins
'ON', 'USING',
// where clause
'(IS (NOT)?)?NULL',
'(NOT )?IN',
'(NOT )?I?LIKE',
'AND', 'OR', 'XOR',
'BETWEEN',
// order, group, limit ..
'ASC',
'DESC',
'OFFSET',
));
$sql = preg_replace(array(
'/\b('.implode('|', $newlines).')\b/',
'/\b('.implode('|', $keywords).')\b/',
'/(\/\*.*\*\/)/',
'/(`[^`.]*`)/',
'/(([0-9a-zA-Z$_]+)\.([0-9a-zA-Z$_]+))/',
), array(
'<br />\\1',
'<span class="SQLKeyword">\\1</span>',
'<span class="SQLComment">\\1</span>',
'<span class="SQLName">\\1</span>',
'<span class="SQLName">\\1</span>',
), $sql);
return $sql;
}
/**
* Rounding to significant digits (sort of like JavaScript's toPrecision()).
*
* @param float $number Value to round
* @param integer $significantFigures Number of significant figures
*
* @return float
*/
public static function toPrecision($number, $significantFigures = 3)
{
if (0 === $number) {
return 0;
}
$significantDecimals = floor($significantFigures - log10(abs($number)));
$magnitude = pow(10, $significantDecimals);
$shifted = round($number * $magnitude);
return number_format($shifted / $magnitude, $significantDecimals);
}
}