diff --git a/Controller/PanelController.php b/Controller/PanelController.php
new file mode 100644
index 0000000..4f3e9a5
--- /dev/null
+++ b/Controller/PanelController.php
@@ -0,0 +1,82 @@
+
+ */
+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('
This query cannot be explained.
');
+ }
+
+ return $this->container->get('templating')->renderResponse(
+ 'PropelBundle:Panel:explain.html.twig',
+ array(
+ 'data' => $results,
+ 'query' => $query,
+ )
+ );
+ }
+}
diff --git a/DataCollector/PropelDataCollector.php b/DataCollector/PropelDataCollector.php
new file mode 100644
index 0000000..d347be6
--- /dev/null
+++ b/DataCollector/PropelDataCollector.php
@@ -0,0 +1,109 @@
+
+ *
+ * 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
+ */
+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());
+ }
+}
diff --git a/Logger/PropelLogger.php b/Logger/PropelLogger.php
new file mode 100644
index 0000000..f7620c4
--- /dev/null
+++ b/Logger/PropelLogger.php
@@ -0,0 +1,102 @@
+
+ */
+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;
+ }
+}
diff --git a/PropelBundle.php b/PropelBundle.php
index 800e1b5..cf96b09 100644
--- a/PropelBundle.php
+++ b/PropelBundle.php
@@ -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')));
+ }
+ }
}
diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml
index e562719..93de36b 100644
--- a/Resources/config/propel.xml
+++ b/Resources/config/propel.xml
@@ -7,12 +7,30 @@
default
+ Propel\Runtime\Propel
Propel\PropelBundle\Service\SchemaLocator
+ Propel\PropelBundle\DataCollector\PropelDataCollector
+ Propel\PropelBundle\Logger\PropelLogger
+ Propel\PropelBundle\Twig\Extension\SyntaxExtension
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/views/Collector/propel.html.twig b/Resources/views/Collector/propel.html.twig
new file mode 100644
index 0000000..ad1c45d
--- /dev/null
+++ b/Resources/views/Collector/propel.html.twig
@@ -0,0 +1,111 @@
+{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
+
+{% block toolbar %}
+ {# the web debug toolbar content #}
+ {% set icon %}
+
+ {{ collector.querycount }}
+ {% endset %}
+ {% set text %}
+
+ DB Queries
+ {{ collector.querycount }}
+
+
+ Query time
+ {{ '%0.2f'|format(collector.time * 1000) }} ms
+
+{% endset %}
+ {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %}
+{% endblock %}
+
+{% block menu %}
+ {# the menu content #}
+
+
+ Propel
+
+ {{ collector.querycount }}
+ {{ '%0.0f'|format(collector.time * 1000) }} ms
+
+
+{% endblock %}
+
+{% block panel %}
+ {# the panel content #}
+
+
+ Queries
+
+
+
+ SQL queries |
+
+
+
+ {% if not collector.querycount %}
+ No queries. |
+ {% else %}
+ {% for i, query in collector.queries %}
+
+
+
+ {{ query.sql|format_sql }}
+ {% if app.request.query.has('query') and app.request.query.get('query') == i %}
+
+ {% render controller('PropelBundle:Panel:explain', {
+ 'token': token,
+ 'panel': 'propel',
+ 'query': app.request.query.get('query'),
+ 'connection': app.request.query.get('connection')
+ }) %}
+
+ {% endif %}
+
+ Time: {{ query.time }} - Memory: {{ query.memory|format_memory }} - Connection: {{ query.connection }}
+
+ {% if app.request.query.get('query', -1) != i %}
+ - Explain the query
+ {% endif %}
+
+ |
+
+ {% endfor %}
+ {% endif %}
+
+
+
+ {% render controller('PropelBundle:Panel:configuration') %}
+{% endblock %}
diff --git a/Resources/views/Panel/configuration.html.twig b/Resources/views/Panel/configuration.html.twig
new file mode 100644
index 0000000..596d244
--- /dev/null
+++ b/Resources/views/Panel/configuration.html.twig
@@ -0,0 +1,69 @@
+Propel configuration
+
+
+
+
+
+
+ Propel version |
+ {{ propel_version }} |
+
+
+ Default connection |
+ {{ default_connection }} |
+
+ Logging |
+ {{ logging ? 'enabled' : 'disabled' }} |
+
+
+
+
+Propel connections
+
+
+
+
+ Connection name |
+ Configuration parameters |
+
+
+
+ {% for name, config in configuration %}
+
+
+ {{ name }}
+ |
+ Adapter |
+ {{ config.adapter }} |
+
+
+ DSN |
+ {{ config.connection.dsn }} |
+
+
+ Class |
+ {{ config.connection.classname }} |
+
+
+ Options |
+
+
+ {% for key, value in config.connection.options %}
+ - {{ key }} : {{ value }}
+ {% endfor %}
+
+ |
+
+
+ Attributes |
+
+
+ {% for key, value in config.connection.attributes %}
+ - {{ key }} : {{ value }}
+ {% endfor %}
+
+ |
+
+ {% endfor %}
+
+
diff --git a/Resources/views/Panel/explain.html.twig b/Resources/views/Panel/explain.html.twig
new file mode 100644
index 0000000..709bf57
--- /dev/null
+++ b/Resources/views/Panel/explain.html.twig
@@ -0,0 +1,16 @@
+Explanation
+
+
+
+ {% for label in data[0]|keys %}
+ {{ label }} |
+ {% endfor %}
+
+ {% for row in data %}
+
+ {% for item in row %}
+ {{ item }} |
+ {% endfor %}
+
+ {% endfor %}
+
diff --git a/Twig/Extension/SyntaxExtension.php b/Twig/Extension/SyntaxExtension.php
new file mode 100644
index 0000000..14ceee9
--- /dev/null
+++ b/Twig/Extension/SyntaxExtension.php
@@ -0,0 +1,139 @@
+
+ */
+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(
+ '
\\1',
+ '\\1',
+ '',
+ '\\1',
+ '\\1',
+ ), $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);
+ }
+}