├── .editorconfig ├── .gitignore ├── .php_cs ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── config └── api-debugger.php ├── phpunit.xml ├── src ├── Collection.php ├── Collections │ ├── CacheCollection.php │ ├── MemoryCollection.php │ ├── ProfilingCollection.php │ └── QueriesCollection.php ├── Debugger.php ├── Events │ ├── StartProfiling.php │ └── StopProfiling.php ├── ServiceProvider.php ├── Storage.php └── Support │ ├── Facade.php │ └── helpers.php └── tests ├── DebuggerTest.php ├── QueriesCollectionTest.php ├── ServiceProviderTest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .php_cs.cache 4 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | true, 7 | // addtional rules 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'no_multiline_whitespace_before_semicolons' => true, 10 | 'no_short_echo_tag' => true, 11 | 'no_unused_imports' => true, 12 | 'not_operator_with_successor_space' => true, 13 | ]; 14 | $excludes = [ 15 | // add exclude project directory 16 | 'vendor', 17 | ]; 18 | 19 | return PhpCsFixer\Config::create() 20 | ->setRules($rules) 21 | ->setFinder( 22 | PhpCsFixer\Finder::create() 23 | ->exclude($excludes) 24 | ->in(__DIR__) 25 | ->notName('README.md') 26 | ->notName('*.xml') 27 | ->notName('*.yml') 28 | ); 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: php 4 | 5 | php: 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | - 7.4 10 | 11 | sudo: false 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | 17 | install: 18 | - travis_retry composer install --no-interaction --prefer-dist 19 | 20 | before_script: 21 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist 22 | 23 | script: 24 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 25 | 26 | after_script: 27 | - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover; fi 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | #The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel-API-Debugger 2 | [![Travis](https://img.shields.io/travis/rust-lang/rust.svg)](https://travis-ci.org/mlanin/laravel-api-debugger) 3 | 4 | > Easily debug your JSON API. 5 | 6 | When you are developing JSON API sometimes you need to debug it, but if you will use `dd()` or `var_dump()` you will break the output that will affect every client that is working with your API at the moment. Debugger is made to provide you with all your debug information and not corrupt the output. 7 | 8 | ```json 9 | { 10 | "posts": [ 11 | { 12 | "id": 1, 13 | "title": "Title 1", 14 | "body": "Body 1" 15 | }, 16 | { 17 | "id": 2, 18 | "title": "Title 2", 19 | "body": "Body 2" 20 | } 21 | ], 22 | "meta": { 23 | "total": 2 24 | }, 25 | "debug": { 26 | "database": { 27 | "total": 2, 28 | "items": [ 29 | { 30 | "connection": "accounts", 31 | "query": "select * from `users` where `email` = 'john.doe@acme.com' limit 1;", 32 | "time": 0.38 33 | }, 34 | { 35 | "connection": "posts", 36 | "query": "select * from `posts` where `author` = '1';", 37 | "time": 1.34 38 | } 39 | ] 40 | }, 41 | "dump": [ 42 | "foo", 43 | [ 44 | 1, 45 | 2, 46 | "bar" 47 | ] 48 | ] 49 | } 50 | } 51 | ``` 52 | 53 | ## Installation 54 | 55 | > This help is for Laravel 5.4 only. Readme for earlier versions can be found in the relevant branches of this repo. 56 | 57 | [PHP](https://php.net) >=5.5.9+ or [HHVM](http://hhvm.com) 3.3+, [Composer](https://getcomposer.org) and [Laravel](http://laravel.com) 5.4+ are required. 58 | 59 | To get the latest version of Laravel Laravel-API-Debugger, simply add the following line to the require block of your `composer.json` file. 60 | 61 | For PHP >= 7.1: 62 | 63 | ``` 64 | "lanin/laravel-api-debugger": "^4.0" 65 | ``` 66 | 67 | For PHP < 7.1: 68 | 69 | ``` 70 | "lanin/laravel-api-debugger": "^3.0" 71 | ``` 72 | 73 | You'll then need to run `composer install` or `composer update` to download it and have the autoloader updated. 74 | 75 | Once Laravel-API-Debugger is installed, you need to register the service provider. Open up `config/app.php` and add the following to the providers key. 76 | 77 | For Laravel 5.4 78 | ```php 79 | Lanin\Laravel\ApiDebugger\ServiceProvider::class, 80 | ``` 81 | 82 | Also you can register a Facade for easier access to the Debugger methods. 83 | 84 | For Laravel 5.4 85 | ```php 86 | 'Debugger' => Lanin\Laravel\ApiDebugger\Facade::class, 87 | ``` 88 | 89 | For Laravel 5.5 package supports [package discovery](https://laravel.com/docs/5.5/packages#package-discovery) feature. 90 | 91 | Copy the config file to your own project by running the following command: 92 | ```php 93 | php artisan vendor:publish --provider="Lanin\Laravel\ApiDebugger\ServiceProvider" 94 | ``` 95 | 96 | ## Json response 97 | 98 | Before extension will populate your answer it will try to distinguish if it is a json response. It will do it by validating if it is a JsonResponse instance. The best way to do it is to return `response()->json();` in your controller's method. 99 | 100 | Also please be careful with what you return. As if your answer will not be wrapped in any kind of `data` attribute (`pages` in the example above), frontend could be damaged because of waiting the particular set of attributes but it will return extra `debug` one. 101 | 102 | So the best way to return your responses is like this 103 | ```php 104 | $data = [ 105 | 'foo' => 'bar', 106 | 'baz' => 1, 107 | ]; 108 | 109 | return response()->json([ 110 | 'data' => [ 111 | 'foo' => 'bar', 112 | 'baz' => 1, 113 | ], 114 | ]); 115 | ``` 116 | 117 | For more info about better practices in JSON APIs you can find here http://jsonapi.org/ 118 | 119 | ## Debugging 120 | 121 | Debugger's two main tasks are to dump variables and collect anny additional info about your request. 122 | 123 | ### Var dump 124 | 125 | Debugger provides you with the easy way to dump any variable you want right in your JSON answer. This functionality sometimes very handy when you have to urgently debug your production environment. 126 | 127 | ```php 128 | $foo = 'foo'; 129 | $bar = [1, 2, 'bar']; 130 | 131 | // As a helper 132 | lad($foo, $bar); 133 | 134 | // or as a facade 135 | \Debugger::dump($foo, $bar); 136 | ``` 137 | 138 | You can simultaneously dump as many vars as you want and they will appear in the answer. 139 | 140 | **Note!** Of course it it not the best way do debug your production environment, but sometimes it is the only way. 141 | So be careful with this, because everyone will see your output, but at least debug will not break your clients. 142 | 143 | ### Collecting data 144 | 145 | **Note!** By default Debugger will collect data ONLY when you set `APP_DEBUG=true`. 146 | So you don't have to worry that someone will see your system data on production. 147 | You can overwrite that by adding `API_DEBUGGER_ENABLED=true|false` to your .env file, or by changing the value of `enabled` in the config file. 148 | 149 | All available collections can be found in `api-debugger.php` config that you can publish and update as you wish. 150 | 151 | #### QueriesCollection 152 | 153 | This collections listens to all queries events and logs them in `connections`, `query`, `time` structure. 154 | 155 | #### CacheCollection 156 | 157 | It can show you cache hits, misses, writes and forgets. 158 | 159 | #### ProfilingCollection 160 | 161 | It allows you to measure time taken to perform actions in your code. 162 | There are 2 ways to do it. 163 | 164 | Automatically: 165 | 166 | ```php 167 | Debugger::profileMe('event-name', function () { 168 | sleep(1); 169 | }); 170 | ``` 171 | 172 | Or manually: 173 | 174 | ```php 175 | Debugger::startProfiling('event-name'); 176 | usleep(300); 177 | Debugger::stopProfiling('event-name'); 178 | ``` 179 | 180 | Also helpers are available: 181 | ```php 182 | lad_pr_start(); 183 | lad_pr_stop(); 184 | lad_pr_me(); 185 | ``` 186 | 187 | ### Extending 188 | 189 | You can easily add your own data collections to debug output. 190 | Just look at how it was done in the package itself and repeat for anything you want (for example HTTP requests). 191 | 192 | ## Contributing 193 | 194 | Please feel free to fork this package and contribute by submitting a pull request to enhance the functionalities. 195 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lanin/laravel-api-debugger", 3 | "description": "Easily debug your JSON API.", 4 | "keywords": ["laravel", "framework", "api", "debug", "debugger", "json", "dump", "queries", "log"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Maxim Lanin", 9 | "email": "max@lanin.me", 10 | "homepage": "http://lanin.me" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.1", 15 | "illuminate/support": ">=5.4.0" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": ">=3.4.0", 19 | "phpunit/phpunit": "^7", 20 | "mockery/mockery": "^1.3.1" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Lanin\\Laravel\\ApiDebugger\\": "src/" 25 | }, 26 | "files": [ 27 | "src/Support/helpers.php" 28 | ] 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Lanin\\Laravel\\ApiDebugger\\Tests\\": "tests/" 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "Lanin\\Laravel\\ApiDebugger\\ServiceProvider" 41 | ], 42 | "aliases": { 43 | "Debugger": "Lanin\\Laravel\\ApiDebugger\\Support\\Facade" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/api-debugger.php: -------------------------------------------------------------------------------- 1 | (bool) env('API_DEBUGGER_ENABLED', env('APP_DEBUG', false)), 5 | /** 6 | * Specify what data to collect. 7 | */ 8 | 'collections' => [ 9 | // Database queries. 10 | \Lanin\Laravel\ApiDebugger\Collections\QueriesCollection::class, 11 | 12 | // Show cache events. 13 | \Lanin\Laravel\ApiDebugger\Collections\CacheCollection::class, 14 | 15 | // Profile custom events. 16 | \Lanin\Laravel\ApiDebugger\Collections\ProfilingCollection::class, 17 | 18 | // Memory usage. 19 | \Lanin\Laravel\ApiDebugger\Collections\MemoryCollection::class, 20 | ], 21 | 22 | 'response_key' => env('API_DEBUGGER_KEY', 'debug') 23 | ]; 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | ['keys' => [], 'total' => 0], 25 | 'miss' => ['keys' => [], 'total' => 0], 26 | 'write' => ['keys' => [], 'total' => 0], 27 | 'forget' => ['keys' => [], 'total' => 0], 28 | ]; 29 | 30 | /** 31 | * CacheCollection constructor. 32 | * 33 | * @param Dispatcher $dispatcher 34 | */ 35 | public function __construct(Dispatcher $dispatcher) 36 | { 37 | $this->dispatcher = $dispatcher; 38 | 39 | $this->listen(); 40 | } 41 | 42 | /** 43 | * Collection name. 44 | * 45 | * @return string 46 | */ 47 | public function name() 48 | { 49 | return 'cache'; 50 | } 51 | 52 | /** 53 | * Returns resulting collection. 54 | * 55 | * @return array 56 | */ 57 | public function items() 58 | { 59 | return $this->events; 60 | } 61 | 62 | /** 63 | * Listen query events. 64 | */ 65 | public function listen() 66 | { 67 | $this->dispatcher->listen(CacheHit::class, [$this, 'hit']); 68 | $this->dispatcher->listen(CacheMissed::class, [$this, 'miss']); 69 | $this->dispatcher->listen(KeyWritten::class, [$this, 'write']); 70 | $this->dispatcher->listen(KeyForgotten::class, [$this, 'forget']); 71 | } 72 | 73 | /** 74 | * Store hit. 75 | * 76 | * @param CacheHit $event 77 | */ 78 | public function hit(CacheHit $event) 79 | { 80 | $this->store(__FUNCTION__, $event); 81 | } 82 | 83 | /** 84 | * Store miss. 85 | * 86 | * @param CacheMissed $event 87 | */ 88 | public function miss(CacheMissed $event) 89 | { 90 | $this->store(__FUNCTION__, $event); 91 | } 92 | 93 | /** 94 | * Store write. 95 | * 96 | * @param KeyWritten $event 97 | */ 98 | public function write(KeyWritten $event) 99 | { 100 | $this->store(__FUNCTION__, $event); 101 | } 102 | 103 | /** 104 | * Store forget. 105 | * 106 | * @param KeyForgotten $event 107 | */ 108 | public function forget(KeyForgotten $event) 109 | { 110 | $this->store(__FUNCTION__, $event); 111 | } 112 | 113 | /** 114 | * Store event. 115 | * 116 | * @param string $label 117 | * @param CacheEvent $event 118 | */ 119 | protected function store($label, CacheEvent $event) 120 | { 121 | $tags = $event->tags; 122 | 123 | $this->events[$label]['keys'][] = ! empty($tags) 124 | ? ['tags' => $tags, 'key' => $event->key] 125 | : $event->key; 126 | 127 | $this->events[$label]['total']++; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Collections/MemoryCollection.php: -------------------------------------------------------------------------------- 1 | memory_get_usage(), 28 | 'peak' => memory_get_peak_usage(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Collections/ProfilingCollection.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 37 | 38 | $this->listen(); 39 | } 40 | 41 | /** 42 | * Collection name. 43 | * 44 | * @return string 45 | */ 46 | public function name() 47 | { 48 | return 'profiling'; 49 | } 50 | 51 | /** 52 | * Returns resulting collection. 53 | * 54 | * @return array 55 | */ 56 | public function items() 57 | { 58 | return $this->timers; 59 | } 60 | 61 | /** 62 | * Listen query events. 63 | */ 64 | public function listen() 65 | { 66 | if (defined('LARAVEL_START')) { 67 | $this->start(static::REQUEST_TIMER, LARAVEL_START); 68 | } 69 | 70 | $this->dispatcher->listen(StartProfiling::class, function (StartProfiling $event) { 71 | $this->start($event->name); 72 | }); 73 | 74 | $this->dispatcher->listen(StopProfiling::class, function (StopProfiling $event) { 75 | $this->stop($event->name); 76 | }); 77 | } 78 | 79 | /** 80 | * Start timer. 81 | * 82 | * @param string $name 83 | * @param float|null $time 84 | */ 85 | protected function start(string $name, ?float $time = null) 86 | { 87 | $this->started[$name] = $time ?: microtime(true); 88 | } 89 | 90 | /** 91 | * Stop timer. 92 | * 93 | * @param string $name 94 | */ 95 | protected function stop($name) 96 | { 97 | if (array_key_exists($name, $this->started)) { 98 | $this->timers[] = [ 99 | 'event' => $name, 100 | 'time' => microtime(true) - $this->started[$name], 101 | ]; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Collections/QueriesCollection.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 29 | 30 | $this->listen(); 31 | } 32 | 33 | /** 34 | * Collection name. 35 | * 36 | * @return string 37 | */ 38 | public function name() 39 | { 40 | return 'database'; 41 | } 42 | 43 | /** 44 | * Returns resulting collection. 45 | * 46 | * @return array 47 | */ 48 | public function items() 49 | { 50 | return [ 51 | 'total' => count($this->queries), 52 | 'items' => $this->queries, 53 | ]; 54 | } 55 | 56 | /** 57 | * Listen query events. 58 | */ 59 | public function listen() 60 | { 61 | $this->connection->enableQueryLog(); 62 | 63 | $this->connection->listen(function (QueryExecuted $event) { 64 | $this->logQuery($event->connectionName, $event->sql, $event->bindings, $event->time); 65 | }); 66 | } 67 | 68 | /** 69 | * Log DB query. 70 | * 71 | * @param string $connection 72 | * @param string $query 73 | * @param array $bindings 74 | * @param float $time 75 | */ 76 | public function logQuery($connection, $query, array $bindings, $time) 77 | { 78 | if (! empty($bindings)) { 79 | $query = vsprintf( 80 | // Replace pdo bindings to printf string bindings escaping % char. 81 | str_replace(['%', '?'], ['%%', "'%s'"], $query), 82 | 83 | // Convert all query attributes to strings. 84 | $this->normalizeQueryAttributes($bindings) 85 | ); 86 | } 87 | 88 | // Finish query with semicolon. 89 | $query = rtrim($query, ';') . ';'; 90 | 91 | $this->queries[] = compact('connection', 'query', 'time'); 92 | } 93 | 94 | /** 95 | * Be sure that all attributes sent to DB layer are strings. 96 | * 97 | * @param array $attributes 98 | * @return array 99 | */ 100 | protected function normalizeQueryAttributes(array $attributes) 101 | { 102 | $result = []; 103 | 104 | foreach ($attributes as $attribute) { 105 | $result[] = $this->convertAttribute($attribute); 106 | } 107 | 108 | return $result; 109 | } 110 | 111 | /** 112 | * Convert attribute to string. 113 | * 114 | * @param mixed $attribute 115 | * @return string 116 | */ 117 | protected function convertAttribute($attribute) 118 | { 119 | try { 120 | return (string) $attribute; 121 | } catch (\Throwable $e) { 122 | switch (true) { 123 | // Handle DateTime attribute pass. 124 | case $attribute instanceof \DateTime: 125 | return $attribute->format('Y-m-d H:i:s'); 126 | 127 | // Handle callables. 128 | case $attribute instanceof \Closure: 129 | return $this->convertAttribute($attribute()); 130 | 131 | // Handle arrays using json by default or print_r if error occurred. 132 | case is_array($attribute): 133 | $json = json_encode($attribute); 134 | 135 | return json_last_error() === JSON_ERROR_NONE 136 | ? $json 137 | : print_r($attribute); 138 | 139 | // Handle all other object. 140 | case is_object($attribute): 141 | return get_class($attribute); 142 | 143 | // For all unknown. 144 | default: 145 | return '?'; 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Debugger.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 45 | $this->event = $event; 46 | 47 | $this->event->listen(RequestHandled::class, function (RequestHandled $event) { 48 | $this->updateResponse($event->request, $event->response); 49 | }); 50 | } 51 | 52 | /** 53 | * Inject custom collection. 54 | * 55 | * @param Collection $collection 56 | */ 57 | public function populateWith(Collection $collection) 58 | { 59 | $this->storage->inject($collection); 60 | } 61 | 62 | /** 63 | * Add vars to debug output. 64 | */ 65 | public function dump() 66 | { 67 | $this->storage->dump(func_get_args()); 68 | } 69 | 70 | /** 71 | * Start profiling event. 72 | * 73 | * @param string $name 74 | */ 75 | public function startProfiling($name) 76 | { 77 | $this->event->dispatch(new StartProfiling($name)); 78 | } 79 | 80 | /** 81 | * Finish profiling event. 82 | * 83 | * @param string $name 84 | */ 85 | public function stopProfiling($name) 86 | { 87 | $this->event->dispatch(new StopProfiling($name)); 88 | } 89 | 90 | /** 91 | * Profile action. 92 | * 93 | * @param string $name 94 | * @param \Closure $action 95 | * @return mixed 96 | */ 97 | public function profileMe($name, \Closure $action) 98 | { 99 | $this->startProfiling($name); 100 | $return = $action(); 101 | $this->stopProfiling($name); 102 | 103 | return $return; 104 | } 105 | 106 | /** 107 | * Update final response. 108 | * 109 | * @param Request $request 110 | * @param Response $response 111 | */ 112 | protected function updateResponse(Request $request, Response $response) 113 | { 114 | $this->stopProfiling(ProfilingCollection::REQUEST_TIMER); 115 | 116 | if ($this->needToUpdateResponse($response)) { 117 | $data = $this->getResponseData($response); 118 | 119 | if ($data === false || !is_object($data)) { 120 | return; 121 | } 122 | 123 | $data->{$this->responseKey} = $this->storage->getData(); 124 | 125 | $this->setResponseData($response, $data); 126 | } 127 | } 128 | 129 | /** 130 | * Check if debugger has to update the response. 131 | * 132 | * @param Response $response 133 | * @return bool 134 | */ 135 | protected function needToUpdateResponse(Response $response) 136 | { 137 | $isJsonResponse = $response instanceof JsonResponse || 138 | $response->headers->contains('content-type', 'application/json'); 139 | 140 | return $isJsonResponse && !$this->storage->isEmpty(); 141 | } 142 | 143 | /** 144 | * Fetches the contents of the response and parses them to an assoc array 145 | * 146 | * @param Response $response 147 | * @return object|bool 148 | */ 149 | protected function getResponseData(Response $response) 150 | { 151 | if ($response instanceof JsonResponse) { 152 | /** @var $response JsonResponse */ 153 | return $response->getData() ?: new \StdClass(); 154 | } 155 | 156 | $content = $response->getContent(); 157 | 158 | return json_decode($content) ?: false; 159 | } 160 | 161 | /** 162 | * Updates the response content 163 | * 164 | * @param Response $response 165 | * @param object $data 166 | * @return JsonResponse|Response 167 | */ 168 | protected function setResponseData(Response $response, $data) 169 | { 170 | if ($response instanceof JsonResponse) { 171 | /** @var $response JsonResponse */ 172 | return $response->setData($data); 173 | } 174 | 175 | $content = json_encode($data, JsonResponse::DEFAULT_ENCODING_OPTIONS); 176 | 177 | return $response->setContent($content); 178 | } 179 | 180 | /** 181 | * Get the current response key 182 | * 183 | * @return string 184 | */ 185 | public function getResponseKey() 186 | { 187 | return $this->responseKey; 188 | } 189 | 190 | /** 191 | * Set response attribute key name. 192 | * 193 | * @param $key 194 | */ 195 | public function setResponseKey($key) 196 | { 197 | $this->responseKey = $key; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Events/StartProfiling.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/StopProfiling.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([$configPath => config_path('api-debugger.php')]); 21 | $this->mergeConfigFrom($configPath, 'api-debugger'); 22 | 23 | // Register collections only for debug environment. 24 | $config = $this->app['config']; 25 | if ($config['api-debugger.enabled']) { 26 | $this->registerCollections($config['api-debugger.collections']); 27 | 28 | $this->setResponseKey($config['api-debugger.response_key']); 29 | } 30 | } 31 | 32 | /** 33 | * Register the service provider. 34 | * 35 | * @return void 36 | */ 37 | public function register() 38 | { 39 | $this->app->singleton(Debugger::class); 40 | } 41 | 42 | /** 43 | * Get the services provided by the provider. 44 | * 45 | * @return array 46 | */ 47 | public function provides() 48 | { 49 | return [ 50 | Debugger::class, 51 | ]; 52 | } 53 | 54 | /** 55 | * Register requested collections within debugger. 56 | * 57 | * @param Collection[] $collections 58 | */ 59 | protected function registerCollections(array $collections) 60 | { 61 | $debugger = $this->app->make(Debugger::class); 62 | 63 | foreach ($collections as $collection) { 64 | $debugger->populateWith($this->app->make($collection)); 65 | } 66 | } 67 | 68 | /** 69 | * Set the response key for the debug object 70 | * (default: debug) 71 | * 72 | * @param string key 73 | */ 74 | protected function setResponseKey($key) 75 | { 76 | $debugger = $this->app->make(Debugger::class); 77 | 78 | if($key && $key !== Debugger::DEFAULT_RESPONSE_KEY){ 79 | $debugger->setResponseKey($key); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Storage.php: -------------------------------------------------------------------------------- 1 | collections[] = $collection; 25 | } 26 | 27 | /** 28 | * Add vars to debug output. 29 | * 30 | * @param array $vars 31 | */ 32 | public function dump($vars) 33 | { 34 | $this->dump[] = count($vars) == 1 35 | ? $vars[0] 36 | : $vars; 37 | } 38 | 39 | /** 40 | * If storage is empty. 41 | * 42 | * @return bool 43 | */ 44 | public function isEmpty() 45 | { 46 | return count($this->collections) == 0 && count($this->dump) == 0; 47 | } 48 | 49 | /** 50 | * Return result debug data. 51 | * 52 | * @return array 53 | */ 54 | public function getData() 55 | { 56 | $return = []; 57 | 58 | foreach ($this->collections as $collection) { 59 | $return[$collection->name()] = $collection->items(); 60 | } 61 | 62 | if (count($this->dump) != 0) { 63 | $return['dump'] = $this->dump; 64 | } 65 | 66 | return $return; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Support/Facade.php: -------------------------------------------------------------------------------- 1 | startProfiling($name); 28 | } 29 | } 30 | 31 | if (! function_exists('lad_pr_stop')) { 32 | /** 33 | * Finish profiling event. 34 | * 35 | * @param string $name 36 | * @return void 37 | */ 38 | function lad_pr_stop($name) 39 | { 40 | app(Debugger::class)->stopProfiling($name); 41 | } 42 | } 43 | 44 | if (! function_exists('lad_pr_me')) { 45 | /** 46 | * Finish profiling event. 47 | * 48 | * @param string $name 49 | * @param Closure|null $action 50 | * @return mixed 51 | */ 52 | function lad_pr_me($name, ?\Closure $action = null) 53 | { 54 | return app(Debugger::class)->profileMe($name, $action); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/DebuggerTest.php: -------------------------------------------------------------------------------- 1 | app['router']->get('foo', function () { 15 | return response()->json(['foo' => 'bar']); 16 | }); 17 | 18 | $this->json('get', '/foo') 19 | ->assertStatus(200) 20 | ->assertJsonStructure([ 21 | 'debug' => [ 22 | 'memory' => [ 23 | 'usage', 24 | 'peak', 25 | ], 26 | ], 27 | ]); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_dump_var_via_helper() 32 | { 33 | $this->app['router']->get('foo', function () { 34 | lad('baz'); 35 | 36 | return response()->json(['foo' => 'bar']); 37 | }); 38 | 39 | $this->json('get', '/foo') 40 | ->assertStatus(200) 41 | ->assertJsonFragment([ 42 | 'dump' => [ 43 | 'baz', 44 | ], 45 | ]); 46 | } 47 | 48 | /** @test */ 49 | public function it_can_dump_multiple_vars() 50 | { 51 | $this->app['router']->get('foo', function () { 52 | lad('baz1', 'baz2'); 53 | 54 | return response()->json(['foo' => 'bar']); 55 | }); 56 | 57 | $this->json('get', '/foo') 58 | ->assertStatus(200) 59 | ->assertJsonFragment([ 60 | 'dump' => [ 61 | ['baz1', 'baz2'], 62 | ], 63 | ]); 64 | } 65 | 66 | /** @test */ 67 | public function it_can_dump_query() 68 | { 69 | $this->app['router']->get('foo', function () { 70 | \Schema::create('users', function (Blueprint $table) { 71 | $table->increments('id'); 72 | }); 73 | 74 | \DB::table('users')->get(); 75 | 76 | return response()->json(['foo' => 'bar']); 77 | }); 78 | 79 | $this->json('get', '/foo') 80 | ->assertStatus(200) 81 | ->assertJsonStructure([ 82 | 'debug' => [ 83 | 'database' => [ 84 | 'items' => [ 85 | '*' => [ 86 | 'connection', 87 | 'query', 88 | 'time', 89 | ], 90 | ], 91 | 'total', 92 | ], 93 | ], 94 | ]); 95 | } 96 | 97 | /** @test */ 98 | public function it_can_profile_custom_events() 99 | { 100 | $this->app['router']->get('foo', function () { 101 | lad_pr_start('test'); 102 | usleep(300); 103 | lad_pr_stop('test'); 104 | 105 | return response()->json(['foo' => 'bar']); 106 | }); 107 | 108 | $this->json('get', '/foo') 109 | ->assertStatus(200) 110 | ->assertJsonStructure([ 111 | 'debug' => [ 112 | 'profiling' => [ 113 | '*' => [ 114 | 'event', 115 | 'time', 116 | ], 117 | ], 118 | ], 119 | ]) 120 | ->assertJsonFragment([ 121 | 'event' => 'test', 122 | ]); 123 | } 124 | 125 | /** @test */ 126 | public function it_can_profile_simple_actions() 127 | { 128 | $this->app['router']->get('foo', function () { 129 | lad_pr_me('test', function () { 130 | usleep(300); 131 | }); 132 | 133 | return response()->json(['foo' => 'bar']); 134 | }); 135 | 136 | $this->json('get', '/foo') 137 | ->assertStatus(200) 138 | ->assertJsonStructure([ 139 | 'debug' => [ 140 | 'profiling' => [ 141 | '*' => [ 142 | 'event', 143 | 'time', 144 | ], 145 | ], 146 | ], 147 | ]) 148 | ->assertJsonFragment([ 149 | 'event' => 'test', 150 | ]); 151 | } 152 | 153 | /** @test */ 154 | public function it_can_show_cache_events() 155 | { 156 | $this->app['router']->get('foo', function () { 157 | $value = Cache::tags('foo')->remember('bar', 60, function () { 158 | return 'bar'; 159 | }); 160 | 161 | $value = Cache::get('bar'); 162 | 163 | return response()->json(['foo' => $value]); 164 | }); 165 | 166 | $this->json('get', '/foo') 167 | ->assertStatus(200) 168 | ->assertJsonStructure([ 169 | 'debug' => [ 170 | 'cache' => [ 171 | 'hit' => [ 172 | 'keys', 173 | 'total', 174 | ], 175 | 'miss' => [ 176 | 'keys', 177 | 'total', 178 | ], 179 | 'write' => [ 180 | 'keys', 181 | 'total', 182 | ], 183 | 'forget' => [ 184 | 'keys', 185 | 'total', 186 | ], 187 | ], 188 | ], 189 | ]) 190 | ->assertJsonFragment([ 191 | 'miss' => [ 192 | 'keys' => [ 193 | [ 194 | 'tags' => ['foo'], 195 | 'key' => 'bar', 196 | ], 197 | 'bar', 198 | ], 199 | 'total' => 2, 200 | ], 201 | ]); 202 | } 203 | 204 | /** @test */ 205 | public function it_preserves_object() 206 | { 207 | $this->app['router']->get('foo', function () { 208 | return response()->json([ 209 | 'foo' => 'bar', 210 | 'baz' => (object)[], 211 | ]); 212 | }); 213 | 214 | $this->json('get', '/foo') 215 | ->assertStatus(200) 216 | ->assertSeeText('"baz":{}'); 217 | } 218 | 219 | /** @test */ 220 | public function it_preserves_array() 221 | { 222 | $this->app['router']->get('foo', function () { 223 | return response()->json([ 224 | 'foo' => 'bar', 225 | 'baz' => [], 226 | ]); 227 | }); 228 | 229 | $this->json('get', '/foo') 230 | ->assertStatus(200) 231 | ->assertSeeText('"baz":[]'); 232 | } 233 | 234 | /** @test */ 235 | public function it_can_set_a_new_response_key() 236 | { 237 | /** @var Debugger $debugger */ 238 | $debugger = $this->app->make(Debugger::class); 239 | 240 | $this->assertEquals(Debugger::DEFAULT_RESPONSE_KEY, $debugger->getResponseKey(), 'Response key is not the default'); 241 | 242 | $new_response_key = 'key123'; 243 | 244 | $debugger->setResponseKey($new_response_key); 245 | 246 | $this->assertEquals($new_response_key, $debugger->getResponseKey(), 'Response key was not changed from "'.$debugger->getResponseKey().'" to "'.$new_response_key.'"'); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /tests/QueriesCollectionTest.php: -------------------------------------------------------------------------------- 1 | 'testing', 15 | 'query' => "select * from `users` where name = %;", 16 | 'attributes' => [ 17 | 'foo', 18 | ], 19 | 'result' => "select * from `users` where name = %;", 20 | ], 21 | [ 22 | 'connection' => 'testing', 23 | 'query' => "select * from `users` where name = ?;", 24 | 'attributes' => [ 25 | 'foo', 26 | ], 27 | 'result' => "select * from `users` where name = 'foo';", 28 | ] 29 | ]; 30 | } 31 | 32 | /** 33 | * @test 34 | * @dataProvider pdoBindingsProvider 35 | * 36 | * @param string $connection 37 | * @param string $query 38 | * @param array $attributes 39 | * @param string $result 40 | */ 41 | public function it_handles_testing_pdo_bindings($connection, $query, $attributes, $result) 42 | { 43 | $collection = $this->factory(); 44 | 45 | $collection->logQuery($connection, $query, $attributes, 1); 46 | $this->assertEquals($result, $collection->items()['items'][0]['query']); 47 | } 48 | 49 | public function mixedAttributesProvider() 50 | { 51 | return [ 52 | [ 53 | 'connection' => 'testing', 54 | 'query' => "select * from `users` where name = ?;", 55 | 'attributes' => [ 56 | 1, 57 | ], 58 | 'result' => "select * from `users` where name = '1';", 59 | ], 60 | [ 61 | 'connection' => 'testing', 62 | 'query' => "select * from `users` where name = ?;", 63 | 'attributes' => [ 64 | true, 65 | ], 66 | 'result' => "select * from `users` where name = '1';", 67 | ], 68 | [ 69 | 'connection' => 'testing', 70 | 'query' => "select * from `users` where name = ?;", 71 | 'attributes' => [ 72 | ['foo'], 73 | ], 74 | 'result' => "select * from `users` where name = '[\"foo\"]';", 75 | ], 76 | [ 77 | 'connection' => 'testing', 78 | 'query' => "select * from `users` where name = ?;", 79 | 'attributes' => [ 80 | function () { 81 | return 'foo'; 82 | }, 83 | ], 84 | 'result' => "select * from `users` where name = 'foo';", 85 | ], 86 | [ 87 | 'connection' => 'testing', 88 | 'query' => "select * from `users` where name = ?;", 89 | 'attributes' => [ 90 | new \DateTime('2017-04-24'), 91 | ], 92 | 'result' => "select * from `users` where name = '2017-04-24 00:00:00';", 93 | ], 94 | [ 95 | 'connection' => 'testing', 96 | 'query' => "select * from `users` where name = ?;", 97 | 'attributes' => [ 98 | new Foo(), 99 | ], 100 | 'result' => "select * from `users` where name = 'Lanin\Laravel\ApiDebugger\Tests\Foo';", 101 | ], 102 | ]; 103 | } 104 | 105 | /** 106 | * @test 107 | * @dataProvider mixedAttributesProvider 108 | * 109 | * @param string $connection 110 | * @param string $query 111 | * @param array $attributes 112 | * @param string $result 113 | */ 114 | public function it_handles_mixed_attributes_types($connection, $query, $attributes, $result) 115 | { 116 | $collection = $this->factory(); 117 | 118 | $collection->logQuery($connection, $query, $attributes, 1); 119 | $this->assertEquals($result, $collection->items()['items'][0]['query']); 120 | } 121 | 122 | public function factory() 123 | { 124 | $connection = \Mockery::mock(Connection::class); 125 | $connection->shouldReceive('enableQueryLog'); 126 | $connection->shouldReceive('listen'); 127 | 128 | return new QueriesCollection($connection); 129 | } 130 | } 131 | 132 | class Foo 133 | { 134 | } 135 | -------------------------------------------------------------------------------- /tests/ServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Debugger::class, $this->app[Debugger::class]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('app.debug', true); 29 | $config->set('database.default', 'testing'); 30 | $config->set( 31 | 'database.connections.testing', 32 | [ 33 | 'driver' => 'sqlite', 34 | 'database' => ':memory:', 35 | 'prefix' => '', 36 | ] 37 | ); 38 | } 39 | 40 | /** 41 | * Get package providers. 42 | * 43 | * @param \Illuminate\Foundation\Application $app 44 | * 45 | * @return array 46 | */ 47 | protected function getPackageProviders($app) 48 | { 49 | return [ 50 | ServiceProvider::class, 51 | ]; 52 | } 53 | } 54 | --------------------------------------------------------------------------------