├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src └── Sorien ├── DataCollector └── DoctrineDataCollector.php ├── Logger └── DbalLogger.php ├── Provider └── DoctrineProfilerServiceProvider.php └── Resources └── views └── Collector └── db.html.twig /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Stanislav Turza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | silex-dbal-profiler 2 | =================== 3 | 4 | Provides missing Doctrine database queries logging for [Silex Web Profiler](https://github.com/silexphp/Silex-WebProfiler) 5 | 6 | Installation 7 | ------------ 8 | Install the silex-dbal-profiler using [composer](http://getcomposer.org/). This project uses [sematic versioning](http://semver.org/). 9 | 10 | **Silex 2.0** 11 | 12 | ```bash 13 | composer require sorien/silex-dbal-profiler "~2.0" 14 | ``` 15 | 16 | **Silex 1.x** 17 | 18 | ```bash 19 | composer require sorien/silex-dbal-profiler "~1.1" 20 | ``` 21 | 22 | Registering 23 | ----------- 24 | ```php 25 | $app->register(new Sorien\Provider\DoctrineProfilerServiceProvider()); 26 | ``` 27 | 28 | Be sure to do this after registering `WebProfilerServiceProvider`. 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sorien/silex-dbal-profiler", 3 | "description": "Doctrine DBAL Profiler for Silex", 4 | "keywords": [ 5 | "silex", 6 | "profiler", 7 | "doctrine", 8 | "dbal" 9 | ], 10 | "homepage": "", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Stanislav Turza", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "silex/silex": "~2.0", 20 | "silex/web-profiler": "~2.0" 21 | }, 22 | "autoload": { 23 | "psr-0": { 24 | "Sorien": "src" 25 | } 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "2.0.x-dev" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Sorien/DataCollector/DoctrineDataCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sorien\DataCollector; 13 | 14 | use Doctrine\DBAL\Connection; 15 | use Doctrine\DBAL\Logging\DebugStack; 16 | use Doctrine\DBAL\Query\QueryBuilder; 17 | use Doctrine\DBAL\Types\Type; 18 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | /** 23 | * DoctrineDataCollector. 24 | * 25 | * @author Fabien Potencier 26 | */ 27 | class DoctrineDataCollector extends DataCollector 28 | { 29 | private $loggers = array(); 30 | 31 | /** 32 | * @var Connection[] $dbs 33 | */ 34 | private $dbs; 35 | 36 | function __construct($dbs) 37 | { 38 | $this->dbs = $dbs; 39 | } 40 | 41 | /** 42 | * Adds the stack logger for a connection. 43 | * 44 | * @param string $name 45 | * @param DebugStack $logger 46 | */ 47 | public function addLogger($name, DebugStack $logger) 48 | { 49 | $this->loggers[$name] = $logger; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function collect(Request $request, Response $response, \Exception $exception = null) 56 | { 57 | $queries = array(); 58 | foreach ($this->loggers as $name => $logger) { 59 | $queries[$name] = $this->sanitizeQueries($logger->queries, $name); 60 | } 61 | 62 | $this->data = array( 63 | 'queries' => $queries, 64 | ); 65 | } 66 | 67 | public function getQueryCount() 68 | { 69 | return array_sum(array_map('count', $this->data['queries'])); 70 | } 71 | 72 | public function getQueries() 73 | { 74 | return $this->data['queries']; 75 | } 76 | 77 | public function getTime() 78 | { 79 | $time = 0; 80 | foreach ($this->data['queries'] as $queries) { 81 | foreach ($queries as $query) { 82 | $time += $query['executionMS']; 83 | } 84 | } 85 | 86 | return $time; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function getName() 93 | { 94 | return 'db'; 95 | } 96 | 97 | private function sanitizeQueries($queries, $connectionName) 98 | { 99 | foreach ($queries as $i => $query) { 100 | $queries[$i] = $this->sanitizeQuery($query, $connectionName); 101 | } 102 | 103 | return $queries; 104 | } 105 | 106 | private function sanitizeQuery($query, $connectionName) 107 | { 108 | $query['params'] = (array) $query['params']; 109 | foreach ($query['params'] as $j => &$param) { 110 | if (isset($query['types'][$j])) { 111 | // Transform the param according to the type 112 | $type = $query['types'][$j]; 113 | if (is_string($type)) { 114 | $type = Type::getType($type); 115 | } 116 | if ($type instanceof Type) { 117 | $query['types'][$j] = $type->getBindingType(); 118 | $param = $type->convertToDatabaseValue($param, $this->dbs[$connectionName]->getDatabasePlatform()); 119 | } 120 | } 121 | 122 | list($param, $explainable) = $this->sanitizeParam($param); 123 | if (!$explainable) { 124 | $query['explainable'] = false; 125 | } 126 | } 127 | 128 | if ($query['sql'] instanceof QueryBuilder) { 129 | $query['sql'] = $query['sql']->getSQL(); 130 | } 131 | 132 | return $query; 133 | } 134 | 135 | /** 136 | * Sanitizes a param. 137 | * 138 | * The return value is an array with the sanitized value and a boolean 139 | * indicating if the original value was kept (allowing to use the sanitized 140 | * value to explain the query). 141 | * 142 | * @param mixed $var 143 | * 144 | * @return array 145 | */ 146 | private function sanitizeParam($var) 147 | { 148 | if (is_object($var)) { 149 | return array(sprintf('Object(%s)', get_class($var)), false); 150 | } 151 | 152 | if (is_array($var)) { 153 | $a = array(); 154 | $original = true; 155 | foreach ($var as $k => $v) { 156 | list($value, $orig) = $this->sanitizeParam($v); 157 | $original = $original && $orig; 158 | $a[$k] = $value; 159 | } 160 | 161 | return array($a, $original); 162 | } 163 | 164 | if (is_resource($var)) { 165 | return array(sprintf('Resource(%s)', get_resource_type($var)), false); 166 | } 167 | 168 | return array($var, true); 169 | } 170 | } -------------------------------------------------------------------------------- /src/Sorien/Logger/DbalLogger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sorien\Logger; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\Stopwatch\Stopwatch; 16 | use Doctrine\DBAL\Logging\SQLLogger; 17 | 18 | /** 19 | * DbalLogger. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | class DbalLogger implements SQLLogger 24 | { 25 | const MAX_STRING_LENGTH = 32; 26 | const BINARY_DATA_VALUE = '(binary value)'; 27 | 28 | protected $logger; 29 | protected $stopwatch; 30 | 31 | /** 32 | * Constructor. 33 | * 34 | * @param LoggerInterface $logger A LoggerInterface instance 35 | * @param Stopwatch $stopwatch A Stopwatch instance 36 | */ 37 | public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null) 38 | { 39 | $this->logger = $logger; 40 | $this->stopwatch = $stopwatch; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function startQuery($sql, array $params = null, array $types = null) 47 | { 48 | if (null !== $this->stopwatch) { 49 | $this->stopwatch->start('doctrine', 'doctrine'); 50 | } 51 | 52 | if (is_array($params)) { 53 | array_walk($params, function(&$param) { 54 | if (!is_string($param)) { 55 | return; 56 | } 57 | 58 | // non utf-8 strings break json encoding 59 | if (!preg_match('#[\p{L}\p{N} ]#u', $param)) { 60 | $param = self::BINARY_DATA_VALUE; 61 | return; 62 | } 63 | 64 | // detect if the too long string must be shorten 65 | if (function_exists('mb_detect_encoding') && false !== $encoding = mb_detect_encoding($param)) { 66 | if (self::MAX_STRING_LENGTH < mb_strlen($param, $encoding)) { 67 | $param = mb_substr($param, 0, self::MAX_STRING_LENGTH - 6, $encoding).' [...]'; 68 | return; 69 | } 70 | } else { 71 | if (self::MAX_STRING_LENGTH < strlen($param)) { 72 | $param = substr($param, 0, self::MAX_STRING_LENGTH - 6).' [...]'; 73 | return; 74 | } 75 | } 76 | }); 77 | } 78 | 79 | if (null !== $this->logger) { 80 | $this->log($sql, null === $params ? array() : $params); 81 | } 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function stopQuery() 88 | { 89 | if (null !== $this->stopwatch) { 90 | $this->stopwatch->stop('doctrine'); 91 | } 92 | } 93 | 94 | /** 95 | * Logs a message. 96 | * 97 | * @param string $message A message to log 98 | * @param array $params The context 99 | */ 100 | protected function log($message, array $params) 101 | { 102 | $this->logger->debug($message, $params); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Sorien/Provider/DoctrineProfilerServiceProvider.php: -------------------------------------------------------------------------------- 1 | extend('data_collectors', function ($collectors, $app) { 33 | 34 | $collectors['db'] = function ($app) { 35 | 36 | $collector = new DoctrineDataCollector($app['dbs']); 37 | $timeLogger = new DbalLogger($app['logger'], $app['stopwatch']); 38 | 39 | foreach ($app['dbs.options'] as $name => $params) 40 | { 41 | /** @var Connection $db */ 42 | $db = $app['dbs'][$name]; 43 | 44 | $loggerChain = new LoggerChain(); 45 | $logger = new DebugStack(); 46 | 47 | $loggerChain->addLogger($logger); 48 | $loggerChain->addLogger($timeLogger); 49 | 50 | $db->getConfiguration()->setSQLLogger($loggerChain); 51 | 52 | $collector->addLogger($name, $logger); 53 | 54 | } 55 | 56 | return $collector; 57 | }; 58 | 59 | return $collectors; 60 | }); 61 | 62 | $app['data_collector.templates'] = $app->extend('data_collector.templates', function ($templates) { 63 | $templates[] = array('db', '@DoctrineBundle/Collector/db.html.twig'); 64 | return $templates; 65 | }); 66 | 67 | $app['twig.loader.filesystem'] = $app->extend('twig.loader.filesystem', function ($loader) { 68 | /** @var \Twig_Loader_Filesystem $loader */ 69 | $loader->addPath(dirname(__DIR__).'/Resources/views', 'DoctrineBundle'); 70 | return $loader; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Sorien/Resources/views/Collector/db.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | 6 | 8 | 9 | {{ collector.querycount }} 10 | {% if collector.querycount > 0 %} 11 | 12 | in 13 | {{ '%0.2f'|format(collector.time * 1000) }} 14 | ms 15 | 16 | {% endif %} 17 | {% endset %} 18 | {% set text %} 19 | {% spaceless %} 20 |
21 | Database Queries 22 | {{ collector.querycount }} 23 |
24 |
25 | Query time 26 | {{ '%0.2f'|format(collector.time * 1000) }} ms 27 |
28 | {% endspaceless %} 29 | {% endset %} 30 | {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} 31 | {% endblock %} 32 | 33 | {% block menu %} 34 | 35 | 36 | 37 | 39 | 40 | 41 | Doctrine 42 | 43 | {{ collector.querycount }} 44 | {{ '%0.0f'|format(collector.time * 1000) }} ms 45 | 46 | 47 | {% endblock %} 48 | 49 | {% block panel %} 50 | 60 |

Queries

61 | {% set count = collector.queries|length %} 62 | {% for name, queries in collector.queries %} 63 | {% if count > 1 %} 64 |

{{ name }}

65 | {% endif %} 66 | {% if queries is empty %} 67 |

68 | No queries. 69 |

70 | {% else %} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {% for i, query in queries %} 81 | 82 | 83 | 84 | 90 | 91 | {% endfor %} 92 | 93 |
#TimeInfo
{{ loop.index }}{{ '%0.2f'|format(query.executionMS * 1000) }} ms 85 |
{{ query.sql }}
86 |
87 | Parameters: {{ query.params|json_encode() }} 88 |
89 |
94 | {% endif %} 95 | {% endfor %} 96 | 172 | {% endblock %} --------------------------------------------------------------------------------