├── LICENSE.md ├── PHPExperts └── DoctrineDetectiveBundle │ ├── DependencyInjection │ ├── Configuration.php │ └── DoctrineDetectiveExtension.php │ ├── DoctrineDetectiveBundle.php │ ├── EventListener │ └── QueryListener.php │ ├── Resources │ └── config │ │ └── services.yml │ └── Services │ └── QueryLogger.php ├── README.md └── composer.json /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 PHP Experts, Inc. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PHPExperts/DoctrineDetectiveBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('synapse_core'); 22 | 23 | // Here you should define the parameters that are allowed to 24 | // configure your bundle. See the documentation linked above for 25 | // more information on that topic. 26 | 27 | return $treeBuilder; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /PHPExperts/DoctrineDetectiveBundle/DependencyInjection/DoctrineDetectiveExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /PHPExperts/DoctrineDetectiveBundle/DoctrineDetectiveBundle.php: -------------------------------------------------------------------------------- 1 | em = $em; 21 | } 22 | 23 | protected function getRequestActionName($request) 24 | { 25 | return $request->attributes->get('_controller'); 26 | } 27 | 28 | public function onKernelController(FilterControllerEvent $event) 29 | { 30 | $actionName = $this->getRequestActionName($event->getRequest()); 31 | $this->queryLogger = new QueryLogger($actionName); 32 | $this->em->getConnection()->getConfiguration()->setSQLLogger( 33 | $this->queryLogger 34 | ); 35 | } 36 | 37 | public function onKernelResponse(FilterResponseEvent $event) 38 | { 39 | if (!$this->queryLogger) { 40 | return; 41 | } 42 | 43 | $request = $event->getRequest(); 44 | 45 | // Only do something when the requested format is "json". 46 | if ($request->getRequestFormat() != 'json') { 47 | return; 48 | } 49 | 50 | $sqlLog = $this->queryLogger->getLog(); 51 | 52 | $response = $event->getResponse(); 53 | $content = json_decode($response->getContent(), true) + [ 'sqlLog' => $sqlLog ]; 54 | $response->setContent(json_encode($content)); 55 | $event->setResponse($response); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PHPExperts/DoctrineDetectiveBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | doctrinedetective.query_listener: 3 | class: PHPExperts\DoctrineDetectiveBundle\EventListener\QueryListener 4 | arguments: [@doctrine.orm.entity_manager] 5 | tags: 6 | - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } 7 | - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } 8 | 9 | -------------------------------------------------------------------------------- /PHPExperts/DoctrineDetectiveBundle/Services/QueryLogger.php: -------------------------------------------------------------------------------- 1 | requestId = $requestName; 21 | if (!$requestName) { 22 | $this->requestId = count(self::$requests); 23 | } 24 | 25 | $this->requestStartTime = microtime(true); 26 | } 27 | 28 | /** 29 | * Logs a SQL statement somewhere. 30 | * 31 | * @param string $sql The SQL to be executed. 32 | * @param array|null $params The SQL parameters. 33 | * @param array|null $types The SQL parameter types. 34 | * 35 | * @return void 36 | */ 37 | public function startQuery($sql, array $params = null, array $types = null) 38 | { 39 | $backtrace = debug_backtrace(); 40 | $previousTrace = []; 41 | foreach ($backtrace as $idx => $trace) { 42 | if (isset($previousTrace['class']) && $previousTrace['class'] === 'Doctrine\\DBAL\\Statement') { 43 | $this->serviceName = substr($trace['class'], strrpos($trace['class'], '\\')); 44 | $this->methodName = $trace['function']; 45 | $this->lineNumber = $previousTrace['line']; 46 | break; 47 | } 48 | else if (isset($trace['class']) && substr($trace['class'], -7) === 'Service') { 49 | $this->serviceName = substr($trace['class'], strrpos($trace['class'], '\\')); 50 | $this->methodName = $trace['function']; 51 | 52 | $this->lineNumber = -1; 53 | if (isset($backtrace[$idx - 1]['line'])) { 54 | $this->lineNumber = $backtrace[$idx - 1]['line']; 55 | } 56 | break; 57 | } 58 | 59 | $previousTrace = $trace; 60 | } 61 | 62 | $this->queryStartTime = microtime(true); 63 | if (!isset(self::$requests[$this->requestId]['time'])) { 64 | self::$requests[$this->requestId]['time'] = 0; 65 | } 66 | if (!isset(self::$requests[$this->requestId][$this->serviceName]['time'])) { 67 | self::$requests[$this->requestId][$this->serviceName]['time'] = 0; 68 | } 69 | 70 | if (!isset(self::$requests[$this->requestId][$this->serviceName][$this->methodName]['time'])) { 71 | self::$requests[$this->requestId][$this->serviceName][$this->methodName]['time'] = 0; 72 | } 73 | 74 | if (empty(self::$requests[$this->requestId][$this->serviceName][$this->methodName]['queries'])) { 75 | $this->queryId = 0; 76 | } else { 77 | $this->queryId = count(self::$requests[$this->requestId][$this->serviceName][$this->methodName]['queries']); 78 | } 79 | $sql = $this->interpolateQuery($sql, $params); 80 | 81 | self::$requests[$this->requestId][$this->serviceName][$this->methodName]['queries'][$this->queryId] = [ 82 | 'query' => $sql, 83 | ]; 84 | } 85 | 86 | /** 87 | * Marks the last started query as stopped. This can be used for timing of queries. 88 | *d 89 | * @return void 90 | */ 91 | public function stopQuery() 92 | { 93 | $currentTime = microtime(true); 94 | $totalRequestTime = ($currentTime - $this->requestStartTime) * 1000; 95 | $totalQueryTime = ($currentTime - $this->queryStartTime) * 1000; 96 | self::$requests[$this->requestId]['time'] = $totalRequestTime; 97 | self::$requests[$this->requestId][$this->serviceName]['time'] += $totalQueryTime; 98 | self::$requests[$this->requestId][$this->serviceName][$this->methodName]['time'] += $totalQueryTime; 99 | self::$requests[$this->requestId][$this->serviceName][$this->methodName]['queries'][$this->queryId]['line'] = $this->lineNumber; 100 | self::$requests[$this->requestId][$this->serviceName][$this->methodName]['queries'][$this->queryId]['time'] = $totalQueryTime; 101 | } 102 | 103 | public function getLog() 104 | { 105 | return self::$requests; 106 | } 107 | 108 | /** 109 | * Replaces any parameter placeholders in a query with the value of that 110 | * parameter. Useful for debugging. Assumes anonymous parameters from 111 | * $params are are in the same order as specified in $query 112 | * 113 | * @param string $query The sql query with parameter placeholders 114 | * @param array $params The array of substitution parameters 115 | * @return string The interpolated query 116 | */ 117 | protected function interpolateQuery($query, $params) { 118 | $keys = array(); 119 | $values = $params; 120 | 121 | if (!$params) { 122 | return $query; 123 | } 124 | 125 | # build a regular expression for each parameter 126 | foreach ($params as $key => $value) { 127 | if (is_string($key)) { 128 | $keys[] = '/:'.$key.'/'; 129 | } else { 130 | $keys[] = '/[?]/'; 131 | } 132 | 133 | if (is_string($value)) 134 | $values[$key] = "'" . $value . "'"; 135 | 136 | if (is_array($value)) 137 | $values[$key] = "'" . implode("','", $value) . "'"; 138 | 139 | if (is_null($value)) 140 | $values[$key] = 'NULL'; 141 | } 142 | 143 | $query = preg_replace($keys, $values, $query, 1, $count); 144 | 145 | return $query; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Detective Bundle 2 | 3 | **Doctrine Detective** is a Symfony2 Bundle that provides a detailed SQL query 4 | log for both HTML and JSON responses, including the SQL query, its location, 5 | and duration, organized by Controller -> Service -> Repository. 6 | 7 | It is mainly useful for debugging, profiling and refactoring your Doctrine ORM 8 | queries into far more efficient Doctrine DBAL queries. 9 | 10 | Unlike other SQL loggers, *Doctrine Detective* has the following features: 11 | 12 | 1. Queries are organized hierarchically by class and method. 13 | 2. Prepared statements have the parameters interpolated, so you can directly query 14 | them against the database. 15 | 3. RESTful API support. 16 | 17 | ## Installation 18 | 19 | 0. Add `"phpexpertsinc/doctrine-detective" : "1.*"` to your *composer.json*. 20 | 1. Run `$ composer update phpexpertsinc/doctrine-detective`. 21 | 2. Edit `app/appKernel.php`. 22 | 3. Add `new PHPExperts\DoctrineDetectiveBundle\DoctrineDetectiveBundle(),` to 23 | the `AppKernel::registerBundles()` array. -or- (**prefered**), add it to just 24 | the `dev` and `test` environments: 25 | ``` 26 | if (in_array($this->getEnvironment(), array('dev', 'test'))) { 27 | $bundles[] = new PHPExperts\DoctrineDetectiveBundle\DoctrineDetectiveBundle(); 28 | } 29 | ``` 30 | 31 | ## Output 32 | 33 | ### HTML Response 34 | 35 | At the end of every HTML response, you will find the following: 36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
TestController::getActiveUsersAction3.886604999847429 ms-
UserService3.37965652160646 ms-
UserService::getUsers(), Line 2102.622127532959 msSELECT * FROM users WHERE ids IN (1, 2, 3, 4, 5)
UserRepository0.75697898864746 ms-
UserRepository::isActive(), Line 1150.75697898864746SELECT last_visit FROM login_log WHERE userId IN (1, 2, 3, 4, 5)
65 |
66 | 67 | 68 | ### JSON Response 69 | 70 | At the end of your JSON response, you will find the `sqlLog` array: 71 | 72 | 73 | "sqlLog": { 74 | "TestController::getActiveUsersAction": { 75 | "time": 3.886604999847429, 76 | "UserService": { 77 | "time": 3.37965652160646, 78 | "getUsers": { 79 | "time": 2.622127532959, 80 | "queries": [ 81 | { 82 | "query": "SELECT * FROM users WHERE ids IN (1, 2, 3, 4, 5)", 83 | "line": 210, 84 | "time": 2.622127532959 85 | } 86 | ] 87 | }, 88 | }, 89 | "UserRepository": { 90 | "isActive": { 91 | "time": 0.75697898864746, 92 | "queries": [ 93 | { 94 | "query": "SELECT last_visit FROM login_log WHERE userId IN (1, 2, 3, 4, 5)", 95 | "line": 115, 96 | "time": 0.75697898864746 97 | } 98 | ] 99 | }, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpexpertsinc/doctrine-detective", 3 | "description": "A Symfony2 Bundle that provides a detailed SQL query log for both HTML and JSON responses, including the SQL query, its location, and duration, organized by Controller -> Service -> Repository.", 4 | "require": { 5 | "php": ">=5.4", 6 | "symfony/http-kernel": "2.*", 7 | "symfony/dependency-injection": "2.*", 8 | "doctrine/orm": "*", 9 | "doctrine/doctrine-bundle": "*" 10 | }, 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Theodore R. Smith", 15 | "email": "theodore@phpexperts.pro" 16 | } 17 | ], 18 | "minimum-stability": "dev", 19 | "autoload": { 20 | "psr-0": { 21 | "PHPExperts\\DoctrineDetectiveBundle\\": "" 22 | } 23 | } 24 | } 25 | 26 | --------------------------------------------------------------------------------