├── .gitignore ├── src ├── BladeAuditServiceProvider.php ├── Commands │ └── BladeAudit.php └── Analyze.php ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.phar 3 | /composer.lock 4 | .DS_Store 5 | .vscode -------------------------------------------------------------------------------- /src/BladeAuditServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 17 | $this->commands([ 18 | BladeAudit::class 19 | ]); 20 | } 21 | } 22 | 23 | // public function register() 24 | // { 25 | // } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Awssat Developers 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awssat/laravel-blade-audit", 3 | "type": "library", 4 | "description": "Extensive information about any blade view in laravel's project.", 5 | "keywords": ["laravel", "blade", "audit", "analysis", "check", "view"], 6 | "license": "MIT", 7 | "homepage": "https://github.com/awssat/laravel-blade-audit", 8 | "authors": [ 9 | { 10 | "name": "Awssat developers", 11 | "email": "hello@awssat.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "~7.2", 16 | "illuminate/console": "~5.5|^6.0|^7.0|^8.0", 17 | "illuminate/view": "~5.5|^6.0|^7.0|^8.0", 18 | "illuminate/filesystem": "~5.5|^6.0|^7.0|^8.0", 19 | "illuminate/support": "~5.5|^6.0|^7.0|^8.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "~6.5|^8.5" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Awssat\\BladeAudit\\": "src" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Awssat\\BladeAudit\\BladeAuditServiceProvider" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # No longer maintained. Please use other packages: (https://github.com/enlightn/enlightn, or https://github.com/stefanzweifel/laravel-stats) 2 | 3 | 4 | 5 | # Laravel Blade Audit 6 | 7 | 8 | ![laravel-blade-audit](https://i.imgur.com/i0Xj0ZL.jpg) 9 | 10 | 11 | [![Latest Version on Packagist][ico-version]][link-packagist] 12 | [![Software License][ico-license]](LICENSE.md) 13 | 14 | 15 | ## Introduction 16 | Laravel's artisan command to show extensive information about any blade view in laravel's project. 17 | 18 | 19 | ## Features 20 | - General information about the view (size, lines, longest line, blade's directives number ...) 21 | - Blade directives information (repetitions, type: custom or built-in) 22 | - Blade directives nesting level. 23 | - Audit notes (recommendations and best practice notes) 24 | 25 | 26 | ## Install 27 | 28 | Via Composer 29 | ``` bash 30 | composer require awssat/laravel-blade-audit --dev 31 | ``` 32 | 33 | 34 | ## Usage 35 | ```console 36 | php artisan blade:audit view.name 37 | ``` 38 | 39 | All views: 40 | ```console 41 | php artisan blade:audit 42 | ``` 43 | 44 | ## Output Example 45 | #### All Views Audit 46 | ![output example](https://i.imgur.com/cwPtlfw.jpg) 47 | 48 | #### One view audit shows same as all views and extra more information 49 | ![output example2](https://i.imgur.com/JSM8UT4.jpg) 50 | 51 | 52 | 53 | ## License 54 | 55 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 56 | 57 | ## Credits 58 | - [All Contributors][link-contributors] 59 | 60 | 61 | [ico-version]: https://img.shields.io/packagist/v/awssat/laravel-blade-audit.svg?style=flat-square 62 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 63 | [link-packagist]: https://packagist.org/packages/awssat/laravel-blade-audit 64 | [link-contributors]: ../../contributors 65 | 66 | -------------------------------------------------------------------------------- /src/Commands/BladeAudit.php: -------------------------------------------------------------------------------- 1 | viewsPaths = array_map(function($path) { 32 | return realpath($path) ?: $path; 33 | }, $app->config['view']['paths']); 34 | 35 | $this->filesystem = $app->make(Filesystem::class); 36 | } 37 | 38 | /** 39 | * Execute the console command. 40 | * 41 | * @return mixed 42 | */ 43 | public function handle() 44 | { 45 | $view = $this->argument('view'); 46 | 47 | $allViews = empty($view); 48 | 49 | if ($allViews) { 50 | $views = $this->getAllViews(); 51 | $this->allViewsResult = Collection::make(); 52 | } else { 53 | $views = Collection::wrap($view); 54 | } 55 | 56 | $views->each(function ($view) use ($allViews) { 57 | $result = Analyze::view($view); 58 | 59 | if (! $result) { 60 | $this->error('view ['.$view.'] not found!'); 61 | 62 | return; 63 | } 64 | 65 | if (! $allViews) { 66 | $this->outputOneView($result); 67 | } else { 68 | $this->allViewsResult->push([$result, $view]); 69 | } 70 | }); 71 | 72 | if ($allViews) { 73 | $this->outputAllViews(); 74 | } 75 | } 76 | 77 | protected function outputOneView($result) 78 | { 79 | (new Table($this->output)) 80 | ->setHeaders([ 81 | [new TableCell('View Information', ['colspan' => 2])], 82 | ]) 83 | ->setRows($result->getViewInfo()->toArray()) 84 | ->render(); 85 | 86 | $this->output->newLine(); 87 | 88 | (new Table($this->output)) 89 | ->setHeaders([ 90 | [new TableCell('Directives Information', ['colspan' => 3])], 91 | ['Directive', 'Repetition', 'Type'] 92 | ]) 93 | ->setRows( 94 | $result->getDirectivesInfo()->map(function ($item) { 95 | $item[2] = ''.$item[2].''; 96 | 97 | return $item; 98 | })->toArray() 99 | ) 100 | ->render(); 101 | 102 | $this->output->newLine(); 103 | 104 | $lastLevel = $result->getNestedLevels()->max(1); 105 | 106 | (new Table($this->output)) 107 | ->setHeaders([ 108 | [new TableCell('Directives Nesting Levels', ['colspan' => $lastLevel + 1])], 109 | range(1, $lastLevel + 1), 110 | ]) 111 | ->setRows( 112 | $result->getNestedLevels()->map(function ($item) { 113 | $level = $item[1]; 114 | $items = array_pad([], $level, ' |---'); 115 | $items[$level] = ''.$item[0].''; 116 | 117 | return $items; 118 | })->toArray() 119 | ) 120 | ->render(); 121 | 122 | if ($result->getWarnings()->isNotEmpty()) { 123 | $this->output->newLine(); 124 | 125 | (new Table($this->output)) 126 | ->setHeaders([ 127 | [new TableCell('Notes', ['colspan' => 2])], 128 | ]) 129 | ->setStyle('compact') 130 | ->setRows( 131 | $result->getWarnings()->map(function ($item) { 132 | $item[0] = ''.$item[0].':'; 133 | 134 | return $item; 135 | })->toArray() 136 | ) 137 | ->render(); 138 | } 139 | } 140 | 141 | protected function outputAllViews() 142 | { 143 | $result = $this->allViewsResult->reduce(function ($carry, $item) { 144 | [$result, $view] = $item; 145 | 146 | if (empty($carry)) { 147 | $carry = ['info' => [], 'directives' => [], 'warnings' => []]; 148 | } 149 | 150 | $carry['info'] = $result->getViewInfo()->mapWithKeys(function ($item) use ($carry) { 151 | return [$item[0] => isset($carry['info'][$item[0]]) 152 | ? $carry['info'][$item[0]] + $item[1] : $item[1] 153 | ]; 154 | }); 155 | 156 | $carry['directives'] = $result->getDirectivesInfo()->mapWithKeys(function ($item) use ($carry) { 157 | $item[1] = ! empty($carry['directives'][$item[0]]) 158 | ? $carry['directives'][$item[0]][1] + $item[1] 159 | : $item[1]; 160 | 161 | return [$item[0] => $item]; 162 | }); 163 | 164 | $carry['warnings'][$view] = $result->getWarnings(); 165 | 166 | return $carry; 167 | }); 168 | 169 | (new Table($this->output)) 170 | ->setHeaders([ 171 | [new TableCell('All Views Information', ['colspan' => 2])], 172 | ]) 173 | ->setRows( 174 | Collection::wrap($result['info']) 175 | ->map(function ($v, $k) { 176 | return [$k, $v]; 177 | }) 178 | ->filter(function ($v, $k) { 179 | return $k != 'Longest Line (chars)'; 180 | }) 181 | ->values() 182 | ->toArray() 183 | ) 184 | ->render(); 185 | 186 | $this->output->newLine(); 187 | 188 | (new Table($this->output)) 189 | ->setHeaders([ 190 | [new TableCell('Directives Information', ['colspan' => 3])], 191 | ['Directive', 'Repetition', 'Type'] 192 | ]) 193 | ->setRows( 194 | Collection::wrap($result['directives'])->map(function ($item) { 195 | $item[2] = ''.$item[2].''; 196 | 197 | return $item; 198 | })->toArray() 199 | ) 200 | ->render(); 201 | 202 | foreach ($result['warnings'] ?? [] as $view => $warnings) { 203 | if ($warnings->isEmpty()) { 204 | continue; 205 | } 206 | 207 | $this->output->newLine(); 208 | 209 | (new Table($this->output)) 210 | ->setHeaders([ 211 | [new TableCell('Notes: ('.$view.')', ['colspan' => 2])], 212 | ]) 213 | ->setStyle('compact') 214 | ->setRows( 215 | $warnings->map(function ($item) { 216 | $item[0] = ''.$item[0].':'; 217 | 218 | return $item; 219 | })->toArray() 220 | ) 221 | ->render(); 222 | } 223 | } 224 | 225 | /** 226 | * @return Illuminate\Support\Collection 227 | */ 228 | protected function getAllViews() 229 | { 230 | return Collection::wrap($this->viewsPaths) 231 | ->map(function($path) { 232 | return $this->filesystem->allFiles($path) ?? []; 233 | }) 234 | ->flatten() 235 | ->map(function ($file) { 236 | if (Str::endsWith($file, '.blade.php')) { 237 | return str_replace( 238 | [DIRECTORY_SEPARATOR, '.blade.php'], 239 | ['.', ''], 240 | Str::after($file, resource_path('views') . DIRECTORY_SEPARATOR) 241 | ); 242 | } 243 | return null; 244 | }) 245 | ->filter(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Analyze.php: -------------------------------------------------------------------------------- 1 | compiler = $app->make(BladeCompiler::class); 33 | $this->viewsPaths = array_map(function($path) { 34 | return realpath($path) ?: $path; 35 | }, $app->config['view']['paths']); 36 | } 37 | 38 | /** 39 | * @return self 40 | */ 41 | public static function view($viewName) 42 | { 43 | return (new static())->analyze($viewName); 44 | } 45 | 46 | public function analyze($viewName) 47 | { 48 | $viewPath = ''; 49 | $viewName = str_replace('.', '/', $viewName).'.blade.php'; 50 | 51 | foreach ((array) $this->viewsPaths as $path) { 52 | if (file_exists($path.'/'.$viewName)) { 53 | $viewPath = $path.'/'.$viewName; 54 | } 55 | } 56 | 57 | if(empty($viewPath)) { 58 | return false; 59 | } 60 | 61 | $this->code = file_get_contents($viewPath); 62 | 63 | preg_match_all( 64 | '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', 65 | $this->code, 66 | $m, 67 | PREG_SET_ORDER 68 | ); 69 | 70 | $this->directives = Collection::wrap($m) 71 | ->map(function ($v) { 72 | return [$v[0], strtolower($v[1])]; 73 | }); 74 | 75 | $this->warnings = Collection::make(); 76 | 77 | $this->calculateNestingLevel(); 78 | $this->fetchViewInfo(); 79 | $this->fetchDirectivesInfo(); 80 | $this->detectWarnings(); 81 | 82 | return $this; 83 | } 84 | 85 | protected function calculateNestingLevel() 86 | { 87 | $currentNestingLevel = 0; 88 | $nestedDirectives = Collection::make(); 89 | 90 | foreach ($this->directives as $item) { 91 | 92 | [$code, $directive] = $item; 93 | 94 | if (Str::startsWith($directive, ['end', 'stop'])) { 95 | if ($currentNestingLevel > 0) { 96 | $currentNestingLevel--; 97 | } 98 | 99 | $nestedDirectives->push([$directive, $currentNestingLevel]); 100 | } elseif (Str::startsWith($directive, 'else')) { 101 | $nestedDirectives->push([$directive, $currentNestingLevel > 0 ? $currentNestingLevel - 1 : 0]); 102 | } else { 103 | $nestedDirectives->push([$directive, $currentNestingLevel]); 104 | 105 | if ($this->isBlockDirective($code)) { 106 | $currentNestingLevel++; 107 | } 108 | } 109 | } 110 | 111 | $this->nestedLevels = $nestedDirectives; 112 | } 113 | 114 | protected function fetchDirectivesInfo() 115 | { 116 | $this->directivesInfo = Collection::wrap(array_count_values($this->directives->pluck(1)->toArray())); 117 | 118 | $this->directivesInfo = $this->directivesInfo->map(function ($v, $k) { 119 | $customDirective = !method_exists(BladeCompiler::class, 'compile' . $k); 120 | 121 | return [$k, $v, $customDirective ? 'custom' : 'built-in']; 122 | }); 123 | } 124 | 125 | protected function fetchViewInfo() 126 | { 127 | $this->viewInfo = Collection::make(); 128 | 129 | //file info 130 | $this->viewInfo->push(['Size (bytes)', strlen($this->code)]); 131 | 132 | $linesNumber = substr_count($this->code, "\n"); 133 | 134 | $this->viewInfo->push(['Lines', $linesNumber]); 135 | 136 | if ($linesNumber > 300) { 137 | $this->warnings->push(['lines > 300', sprintf('View has %d lines, it\'s a good idea to seperate & @include codes.', $linesNumber)]); 138 | } 139 | 140 | $lines = array_map('\\Illuminate\\Support\\Str::length', explode("\n", $this->code)); 141 | 142 | $this->viewInfo->push(['Longest Line (chars)', max($lines)]); 143 | 144 | //directives number 145 | $directivesNumber = $this->directives 146 | ->filter(function ($item) { 147 | return !Str::startsWith($item[0], ['end', 'stop', 'else']); 148 | })->count(); 149 | 150 | $this->viewInfo->push(['Directives', $directivesNumber]); 151 | 152 | //html & css 153 | if (class_exists(\DOMDocument::class) && !empty($this->code)) { 154 | $dom = new \DOMDocument(); 155 | $dom->loadHTML($this->code, LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING); 156 | $allElements = $dom->getElementsByTagName('*'); 157 | 158 | $this->viewInfo->push(['HTML elements', $allElements->length]); 159 | } 160 | } 161 | 162 | protected function detectWarnings() 163 | { 164 | //php directive? 165 | if (strpos($this->code, '@php') !== false) { 166 | $this->warnings->push(['@php', 'Is not recommended to use php codes directly in your view.']); 167 | } 168 | 169 | //not a good idea things 170 | if (strpos($this->code, '__DIR__') !== false) { 171 | $this->warnings->push(['__DIR__', 'Avoid using __DIR__ because it refers to the location of cache folder, not the view.']); 172 | } 173 | 174 | if (strpos($this->code, '__FILE__') !== false) { 175 | $this->warnings->push(['__FILE__', 'Avoid using __FILE__ because it\'s cached file\'s location, not the view.']); 176 | } 177 | 178 | //new stuff 179 | $laravelVersion = Container::getInstance()::VERSION; 180 | 181 | if (version_compare($laravelVersion, '5.6', '>=')) { 182 | if (preg_match('/\{\{\s+csrf_field/', $this->code)) { 183 | $this->warnings->push(['csrf_field', 'You could use @csrf instead of {{ csrf_field() }}']); 184 | } 185 | 186 | if (preg_match('/\{\{\s+method_field/', $this->code)) { 187 | $this->warnings->push(['method_field', 'You could use @method(..) instead of {{ method_field(..) }}']); 188 | } 189 | } 190 | 191 | if (version_compare($laravelVersion, '5.7', '>=')) { 192 | if ( 193 | preg_match('/@?{{(((?!}})(?