├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml ├── release-drafter.yml └── workflows │ ├── format_php.yml │ ├── phpstan.yml │ ├── release-drafter.yml │ ├── tests.yml │ └── update-changelog.yaml ├── .php-cs-fixer.php ├── LICENSE ├── composer.json ├── config └── stats.php ├── phpstan.neon ├── rector.php ├── src ├── ClassesFinder.php ├── Classifier.php ├── Classifiers │ ├── BladeComponentClassifier.php │ ├── CommandClassifier.php │ ├── ControllerClassifier.php │ ├── CustomCastClassifier.php │ ├── DatabaseFactoryClassifier.php │ ├── EventClassifier.php │ ├── EventListenerClassifier.php │ ├── JobClassifier.php │ ├── LivewireComponentClassifier.php │ ├── MailClassifier.php │ ├── MiddlewareClassifier.php │ ├── MigrationClassifier.php │ ├── ModelClassifier.php │ ├── NotificationClassifier.php │ ├── Nova │ │ ├── ActionClassifier.php │ │ ├── DashboardClassifier.php │ │ ├── FilterClassifier.php │ │ ├── LensClassifier.php │ │ └── ResourceClassifier.php │ ├── NullClassifier.php │ ├── ObserverClassifier.php │ ├── PolicyClassifier.php │ ├── RequestClassifier.php │ ├── ResourceClassifier.php │ ├── RuleClassifier.php │ ├── SeederClassifier.php │ ├── ServiceProviderClassifier.php │ └── Testing │ │ ├── BrowserKitTestClassifier.php │ │ ├── DuskClassifier.php │ │ └── PhpUnitClassifier.php ├── Console │ └── StatsListCommand.php ├── Contracts │ ├── Classifier.php │ └── RejectionStrategy.php ├── Outputs │ ├── AsciiTableOutput.php │ └── JsonOutput.php ├── Project.php ├── ReflectionClass.php ├── RejectionStrategies │ ├── RejectInternalClasses.php │ └── RejectVendorClasses.php ├── Statistics │ ├── NumberOfRoutes.php │ └── ProjectStatistic.php ├── StatsServiceProvider.php └── ValueObjects │ ├── ClassifiedClass.php │ └── Component.php └── stubs ├── Pest.php ├── PestTest.php └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: stefanzweifel 2 | custom: ["https://buymeacoff.ee/3oQ64YW"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/stefanzweifel/laravel-stats/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Feature Request 7 | url: https://github.com/stefanzweifel/laravel-stats/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | - name: Bug Report 10 | url: https://github.com/stefanzweifel/laravel-stats/issues/new 11 | about: Report a reproducable bug 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | ame-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | $CHANGES 5 | change-template: '- $TITLE ([#$NUMBER](https://github.com/stefanzweifel/laravel-stats/pull/$NUMBER))' 6 | categories: 7 | - title: Added 8 | labels: 9 | - 'changelog:added' 10 | - title: Changed 11 | labels: 12 | - 'changelog:changed' 13 | - title: Deprecated 14 | labels: 15 | - 'changelog:deprecated' 16 | - title: Removed 17 | labels: 18 | - 'changelog:removed' 19 | - title: Fixed 20 | labels: 21 | - 'changelog:fixed' 22 | - title: Security 23 | labels: 24 | - security 25 | - changelog:security 26 | - title: 'Dependency Updates' 27 | labels: 28 | - dependencies 29 | 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'changelog:removed' 34 | minor: 35 | labels: 36 | - 'changelog:added' 37 | - 'changelog:deprecated' 38 | patch: 39 | labels: 40 | - 'changelog:fixed' 41 | - 'changelog:security' 42 | - 'dependency' 43 | 44 | exclude-labels: 45 | - 'skip-changelog' 46 | -------------------------------------------------------------------------------- /.github/workflows/format_php.yml: -------------------------------------------------------------------------------- 1 | name: Format PHP 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | ref: ${{ github.head_ref }} 12 | 13 | - name: Run php-cs-fixer 14 | uses: docker://oskarstark/php-cs-fixer-ga 15 | 16 | - uses: stefanzweifel/git-auto-commit-action@v4.1.0 17 | with: 18 | commit_message: Apply php-cs-fixer changes 19 | branch: ${{ github.head_ref }} 20 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | phpstan: 8 | uses: stefanzweifel/reusable-workflows/.github/workflows/phpstan.yml@main 9 | with: 10 | php_version: '8.3' 11 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php: [8.2, 8.3, 8.4] 13 | laravel: [11.*, 12.*] 14 | stability: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | - laravel: 12.* 19 | testbench: 10.* 20 | 21 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php }} 31 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 32 | ini-values: pcov.directory=src 33 | coverage: pcov 34 | 35 | - name: Install dependencies 36 | run: | 37 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 38 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-suggest 39 | 40 | - name: Execute tests 41 | run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml 42 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yaml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | ref: main 16 | 17 | - name: Update Changelog 18 | uses: stefanzweifel/changelog-updater-action@v1 19 | with: 20 | release-notes: ${{ github.event.release.body }} 21 | latest-version: ${{ github.event.release.name }} 22 | 23 | - name: Commit updated CHANGELOG 24 | uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | branch: main 27 | commit_message: Update CHANGELOG 28 | file_pattern: CHANGELOG.md 29 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('vendor') 7 | ->notPath('test-stubs-nova') 8 | ->notPath('tests/Stubs') 9 | ->in(__DIR__) 10 | ->name('*.php'); 11 | 12 | $config = new Config(); 13 | 14 | return $config 15 | ->setRiskyAllowed(true) 16 | ->setRules([ 17 | '@PSR2' => true, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'no_unused_imports' => true, 20 | 'declare_strict_types' => true 21 | ]) 22 | ->setFinder($finder); 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2020 Stefan Zweifel 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": "wnx/laravel-stats", 3 | "description": "Get insights about your Laravel Project", 4 | "homepage": "https://github.com/stefanzweifel/laravel-stats", 5 | "keywords": ["laravel", "wnx", "statistics", "stats"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Stefan Zweifel", 10 | "email": "stefan@stefanzweifel.dev", 11 | "homepage": "https://stefanzweifel.dev", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.2", 17 | "ext-json": "*", 18 | "illuminate/console": "^11.0 | ^12.0", 19 | "illuminate/support": "^11.0 | ^12.0", 20 | "stefanzweifel/laravel-stats-phploc": "^7.0 | ^8.0", 21 | "symfony/finder": "^7.0", 22 | "symfony/process": " ^7.0" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^3.2", 26 | "laravel/browser-kit-testing": "^7.1 | ^8.0", 27 | "laravel/dusk": "^8.0", 28 | "livewire/livewire": "^3.0", 29 | "orchestra/testbench": "^9.0 | ^10", 30 | "pestphp/pest": "^2 | ^3", 31 | "phpunit/phpunit": "^10.0 | ^11.0", 32 | "rector/rector": "^1.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Wnx\\LaravelStats\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Wnx\\LaravelStats\\Tests\\": "tests/", 42 | "Laravel\\Nova\\": "test-stubs-nova/" 43 | } 44 | }, 45 | "scripts": { 46 | "format": "php-cs-fixer fix", 47 | "test": "vendor/bin/phpunit" 48 | }, 49 | "config": { 50 | "sort-packages": true, 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Wnx\\LaravelStats\\StatsServiceProvider" 59 | ] 60 | } 61 | }, 62 | "minimum-stability": "dev", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /config/stats.php: -------------------------------------------------------------------------------- 1 | [ 9 | base_path('app'), 10 | base_path('database'), 11 | base_path('tests'), 12 | ], 13 | 14 | /* 15 | * List of files/folders to be excluded from analysis. 16 | */ 17 | 'exclude' => [ 18 | // base_path('app/helpers.php'), 19 | // base_path('app/Services'), 20 | ], 21 | 22 | /* 23 | * List of your custom Classifiers 24 | */ 25 | 'custom_component_classifier' => [ 26 | // \App\Classifiers\CustomerExportClassifier::class 27 | ], 28 | 29 | /* 30 | * The Strategy used to reject Classes from the project statistics. 31 | * 32 | * By default all Classes located in 33 | * the vendor directory are being rejected and don't 34 | * count to the statistics. 35 | * 36 | * The package ships with 2 strategies: 37 | * - \Wnx\LaravelStats\RejectionStrategies\RejectVendorClasses::class 38 | * - \Wnx\LaravelStats\RejectionStrategies\RejectInternalClasses::class 39 | * 40 | * If none of the default strategies fit for your usecase, you can 41 | * write your own class which implements the RejectionStrategy Contract. 42 | */ 43 | 'rejection_strategy' => \Wnx\LaravelStats\RejectionStrategies\RejectVendorClasses::class, 44 | 45 | /* 46 | * Namespaces which should be ignored. 47 | * Laravel Stats uses the `Str::startsWith()` helper to 48 | * check if a Namespace should be ignored. 49 | * 50 | * You can use `Illuminate` to ignore the entire `Illuminate`-namespace 51 | * or `Illuminate\Support` to ignore a subset of the namespace. 52 | */ 53 | 'ignored_namespaces' => [ 54 | 'Wnx\LaravelStats', 55 | 'Illuminate', 56 | 'Symfony', 57 | 'Swoole', 58 | ], 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]) 15 | // uncomment to reach your current PHP version 16 | ->withPhpSets(php82: true) 17 | ->withPreparedSets(deadCode: true, codingStyle: true) 18 | ->withSets([ 19 | PHPUnitSetList::PHPUNIT_100, 20 | ]) 21 | ->withRules([ 22 | AddVoidReturnTypeWhereNoReturnRector::class, 23 | ]) 24 | ->withSkip([ 25 | FirstClassCallableRector::class => [ 26 | __DIR__ . '/tests/Stubs/EventListeners/UserEventSubscriber.php', 27 | ], 28 | ]); 29 | -------------------------------------------------------------------------------- /src/ClassesFinder.php: -------------------------------------------------------------------------------- 1 | findFilesInProjectPath() 23 | ->each(function (SplFileInfo $file) { 24 | try { 25 | // Files that look like to be Pest Tests are ignored as we currently don't support them. 26 | if ($this->isMostLikelyPestTest($file)) { 27 | return true; 28 | } 29 | 30 | require_once $file->getRealPath(); 31 | } catch (Exception) { 32 | // 33 | } 34 | }); 35 | 36 | ob_end_clean(); 37 | 38 | return collect(get_declared_classes()) 39 | ->reject(static fn (string $className) => Str::startsWith($className, ['SwooleLibrary'])) 40 | ->sort(); 41 | } 42 | 43 | /** 44 | * Find PHP Files which should be analyzed. 45 | */ 46 | protected function findFilesInProjectPath(): Collection 47 | { 48 | $excludes = collect(config('stats.exclude', [])); 49 | 50 | $files = (new Finder)->files() 51 | ->in(config('stats.paths', [])) 52 | ->name('*.php'); 53 | 54 | return collect($files) 55 | ->reject(fn ($file) => $this->isExcluded($file, $excludes)); 56 | } 57 | 58 | /** 59 | * Determine if a file has been defined in the exclude configuration. 60 | */ 61 | protected function isExcluded(SplFileInfo $file, Collection $excludes): bool 62 | { 63 | return $excludes->contains(static fn ($exclude) => Str::startsWith($file->getPathname(), $exclude)); 64 | } 65 | 66 | /** 67 | * Determine if a file is a Pest Test. 68 | * Pest Tess are currently not supported as requiring them will throw an exception. 69 | */ 70 | protected function isMostLikelyPestTest(SplFileInfo $file): bool 71 | { 72 | if (str_ends_with($file->getRealPath(), 'Pest.php')) { 73 | return true; 74 | } 75 | 76 | // If the file path does not contain "test" or "Test", then it's probably not a Pest Test. 77 | if (! str_contains($file->getRealPath(), 'test') && ! str_contains($file->getRealPath(), 'Test')) { 78 | return false; 79 | } 80 | 81 | $fileContent = file_get_contents($file->getRealPath()); 82 | 83 | // Check if file contains "class $name" syntax. 84 | // If it does, it's probably a normal PhpUnit Test. 85 | if (preg_match('/class\s/', $fileContent)) { 86 | return false; 87 | } 88 | 89 | // Check if file contains method calls to prominent Pest functions. 90 | // If it does, it's probably a Pest Test. 91 | $methodNames = implode('|', [ 92 | 'describe', 93 | 'test', 94 | 'it', 95 | 'beforeEach', 96 | 'afterEach', 97 | 'beforeAll', 98 | 'afterAll', 99 | ]); 100 | 101 | if (preg_match(sprintf('/%s\s*\(/', $methodNames), $fileContent)) { 102 | return true; 103 | } 104 | 105 | return false; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Classifier.php: -------------------------------------------------------------------------------- 1 | availableClassifiers = array_merge( 83 | self::DEFAULT_CLASSIFIER, 84 | config('stats.custom_component_classifier', []) 85 | ); 86 | } 87 | 88 | public function getClassifierForClassInstance(ReflectionClass $class): ClassifierContract 89 | { 90 | foreach ($this->availableClassifiers as $classifier) { 91 | $classifierInstance = new $classifier(); 92 | 93 | if (! $this->implementsContract($classifier)) { 94 | throw new Exception(sprintf('Classifier %s does not implement ', $classifier).ClassifierContract::class.'.'); 95 | } 96 | 97 | try { 98 | $satisfied = $classifierInstance->satisfies($class); 99 | } catch (Exception) { 100 | $satisfied = false; 101 | } 102 | 103 | if ($satisfied) { 104 | return $classifierInstance; 105 | } 106 | } 107 | 108 | return new NullClassifier; 109 | } 110 | 111 | /** 112 | * Check if a class implements our Classifier Contract. 113 | * 114 | * @throws \ReflectionException 115 | */ 116 | protected function implementsContract(string $classifier): bool 117 | { 118 | return (new NativeReflectionClass($classifier))->implementsInterface(ClassifierContract::class); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Classifiers/BladeComponentClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Component::class); 23 | } 24 | 25 | public function countsTowardsApplicationCode(): bool 26 | { 27 | return false; 28 | } 29 | 30 | public function countsTowardsTests(): bool 31 | { 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Classifiers/CommandClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Command::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/ControllerClassifier.php: -------------------------------------------------------------------------------- 1 | getRoutes()) 21 | ->reject(static function ($route) { 22 | if (method_exists($route, 'getActionName')) { 23 | // Laravel 24 | return $route->getActionName() === 'Closure'; 25 | } 26 | 27 | // Lumen 28 | return data_get($route, 'action.uses') === null; 29 | }) 30 | ->map(static function ($route) { 31 | if (method_exists($route, 'getController')) { 32 | // Laravel 33 | try { 34 | return $route->getController()::class; 35 | } catch (Throwable) { 36 | return; 37 | } 38 | } 39 | 40 | // Lumen 41 | return Str::before(data_get($route, 'action.uses'), '@'); 42 | }) 43 | ->unique() 44 | ->filter() 45 | ->contains($class->getName()); 46 | } 47 | 48 | public function countsTowardsApplicationCode(): bool 49 | { 50 | return true; 51 | } 52 | 53 | public function countsTowardsTests(): bool 54 | { 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Classifiers/CustomCastClassifier.php: -------------------------------------------------------------------------------- 1 | implementsInterface(CastsAttributes::class) || 20 | $class->implementsInterface(CastsInboundAttributes::class); 21 | } 22 | 23 | public function countsTowardsApplicationCode(): bool 24 | { 25 | return true; 26 | } 27 | 28 | public function countsTowardsTests(): bool 29 | { 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Classifiers/DatabaseFactoryClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Factory::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return false; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/EventClassifier.php: -------------------------------------------------------------------------------- 1 | usesTrait(Dispatchable::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/EventListenerClassifier.php: -------------------------------------------------------------------------------- 1 | getEvents()) 26 | ->map(fn (array $listeners) => collect($listeners)->map(fn ($closure) => $this->getEventListener($closure))->toArray()) 27 | ->collapse() 28 | ->flatten() 29 | ->unique() 30 | ->contains($class->getName()); 31 | } 32 | 33 | /** 34 | * @throws \ReflectionException 35 | */ 36 | protected function getEvents(): array 37 | { 38 | /** @var Dispatcher $dispatcher */ 39 | $dispatcher = app('events'); 40 | 41 | if (method_exists($dispatcher, 'getRawListeners')) { 42 | return $dispatcher->getRawListeners(); 43 | } 44 | 45 | $property = new ReflectionProperty($dispatcher, 'listeners'); 46 | $property->setAccessible(true); 47 | 48 | return $property->getValue($dispatcher); 49 | } 50 | 51 | /** 52 | * @param Closure|array|string $closure 53 | * @retrun null|string|object 54 | * @throws \ReflectionException 55 | */ 56 | protected function getEventListener($closure) 57 | { 58 | if (is_string($closure)) { 59 | return $closure; 60 | } 61 | 62 | if (is_array($closure)) { 63 | return head($closure); 64 | } 65 | 66 | $reflection = new ReflectionFunction($closure); 67 | 68 | return Arr::get($reflection->getStaticVariables(), 'listener'); 69 | } 70 | 71 | public function countsTowardsApplicationCode(): bool 72 | { 73 | return true; 74 | } 75 | 76 | public function countsTowardsTests(): bool 77 | { 78 | return false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Classifiers/JobClassifier.php: -------------------------------------------------------------------------------- 1 | usesTrait(Dispatchable::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/LivewireComponentClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Component::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/MailClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Mailable::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/MiddlewareClassifier.php: -------------------------------------------------------------------------------- 1 | httpKernel = $this->getHttpKernelInstance(); 24 | 25 | $middlewares = $this->getMiddlewares(); 26 | 27 | if (in_array($class->getName(), $middlewares)) { 28 | return true; 29 | } 30 | 31 | return collect($middlewares) 32 | ->merge($this->getMiddlewareGroupsFromKernel()) 33 | ->merge($this->getRouteMiddlewares()) 34 | ->flatten() 35 | ->unique() 36 | ->contains($class->getName()); 37 | } 38 | 39 | protected function getMiddlewares(): array 40 | { 41 | $reflection = new ReflectionProperty($this->httpKernel, 'middleware'); 42 | $reflection->setAccessible(true); 43 | 44 | $middleware = $reflection->getValue($this->httpKernel); 45 | 46 | $reflection = new ReflectionProperty($this->httpKernel, 'routeMiddleware'); 47 | $reflection->setAccessible(true); 48 | 49 | $routeMiddlwares = $reflection->getValue($this->httpKernel); 50 | 51 | return array_values(array_unique(array_merge($middleware, $routeMiddlwares))); 52 | } 53 | 54 | protected function getMiddlewareGroupsFromKernel(): array 55 | { 56 | $property = property_exists($this->httpKernel, 'middlewareGroups') 57 | ? 'middlewareGroups' 58 | : 'routeMiddleware'; 59 | 60 | $reflection = new ReflectionProperty($this->httpKernel, $property); 61 | $reflection->setAccessible(true); 62 | 63 | return $reflection->getValue($this->httpKernel); 64 | } 65 | 66 | protected function getHttpKernelInstance() 67 | { 68 | try { 69 | return app(Kernel::class); 70 | } catch (BindingResolutionException) { 71 | // Lumen 72 | return app(); 73 | } 74 | } 75 | 76 | public function countsTowardsApplicationCode(): bool 77 | { 78 | return true; 79 | } 80 | 81 | public function countsTowardsTests(): bool 82 | { 83 | return false; 84 | } 85 | 86 | private function getRouteMiddlewares(): array 87 | { 88 | $reflection = new ReflectionProperty($this->httpKernel, 'router'); 89 | $reflection->setAccessible(true); 90 | 91 | $router = $reflection->getValue($this->httpKernel); 92 | 93 | return collect($router->getRoutes()->getRoutes()) 94 | ->map(static fn (Route $route) => $route->middleware()) 95 | ->flatten() 96 | ->unique() 97 | ->toArray(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Classifiers/MigrationClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Migration::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/ModelClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Model::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/NotificationClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Notification::class); 23 | } 24 | 25 | public function countsTowardsApplicationCode(): bool 26 | { 27 | return true; 28 | } 29 | 30 | public function countsTowardsTests(): bool 31 | { 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Classifiers/Nova/ActionClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(\Laravel\Nova\Actions\Action::class); 18 | } 19 | 20 | public function countsTowardsApplicationCode(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function countsTowardsTests(): bool 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Classifiers/Nova/DashboardClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(\Laravel\Nova\Dashboard::class); 18 | } 19 | 20 | public function countsTowardsApplicationCode(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function countsTowardsTests(): bool 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Classifiers/Nova/FilterClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(\Laravel\Nova\Filters\Filter::class); 18 | } 19 | 20 | public function countsTowardsApplicationCode(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function countsTowardsTests(): bool 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Classifiers/Nova/LensClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(\Laravel\Nova\Lenses\Lens::class); 18 | } 19 | 20 | public function countsTowardsApplicationCode(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function countsTowardsTests(): bool 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Classifiers/Nova/ResourceClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(\Laravel\Nova\Resource::class); 18 | } 19 | 20 | public function countsTowardsApplicationCode(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function countsTowardsTests(): bool 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Classifiers/NullClassifier.php: -------------------------------------------------------------------------------- 1 | getEvents()) 27 | ->filter(static fn ($_listeners, $event) => Str::startsWith($event, 'eloquent.')) 28 | ->map(fn ($listeners) => collect($listeners)->map(fn ($closure) => $this->getEventListener($closure))->toArray()) 29 | ->collapse() 30 | ->unique() 31 | ->filter(static fn ($eventListener) => is_string($eventListener)) 32 | ->filter(static fn (string $eventListenerSignature) => Str::contains($eventListenerSignature, $class->getName())) 33 | ->count() > 0; 34 | } 35 | 36 | /** 37 | * @throws \ReflectionException 38 | */ 39 | protected function getEvents() 40 | { 41 | /** @var Dispatcher $dispatcher */ 42 | $dispatcher = app('events'); 43 | 44 | if (method_exists($dispatcher, 'getRawListeners')) { 45 | return $dispatcher->getRawListeners(); 46 | } 47 | 48 | $property = new ReflectionProperty($dispatcher, 'listeners'); 49 | $property->setAccessible(true); 50 | 51 | return $property->getValue($dispatcher); 52 | } 53 | 54 | /** 55 | * @param Closure|string $closure 56 | * @retrun null|string|object 57 | * @throws \ReflectionException 58 | */ 59 | protected function getEventListener($closure) 60 | { 61 | if (is_string($closure)) { 62 | return $closure; 63 | } 64 | 65 | $reflection = new ReflectionFunction($closure); 66 | 67 | return Arr::get($reflection->getStaticVariables(), 'listener'); 68 | } 69 | 70 | public function countsTowardsApplicationCode(): bool 71 | { 72 | return true; 73 | } 74 | 75 | public function countsTowardsTests(): bool 76 | { 77 | return false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Classifiers/PolicyClassifier.php: -------------------------------------------------------------------------------- 1 | getName(), 22 | $gate->policies() 23 | ); 24 | } 25 | 26 | public function countsTowardsApplicationCode(): bool 27 | { 28 | return true; 29 | } 30 | 31 | public function countsTowardsTests(): bool 32 | { 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Classifiers/RequestClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(FormRequest::class); 23 | } 24 | 25 | public function countsTowardsApplicationCode(): bool 26 | { 27 | return true; 28 | } 29 | 30 | public function countsTowardsTests(): bool 31 | { 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Classifiers/ResourceClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(JsonResource::class)) { 20 | return true; 21 | } 22 | 23 | return $class->isSubclassOf(ResourceCollection::class); 24 | } 25 | 26 | public function countsTowardsApplicationCode(): bool 27 | { 28 | return true; 29 | } 30 | 31 | public function countsTowardsTests(): bool 32 | { 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Classifiers/RuleClassifier.php: -------------------------------------------------------------------------------- 1 | implementsInterface(Rule::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/SeederClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Seeder::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/ServiceProviderClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(ServiceProvider::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/Testing/BrowserKitTestClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(TestCase::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return false; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/Testing/DuskClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(TestCase::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return false; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Classifiers/Testing/PhpUnitClassifier.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(TestCase::class); 19 | } 20 | 21 | public function countsTowardsApplicationCode(): bool 22 | { 23 | return false; 24 | } 25 | 26 | public function countsTowardsTests(): bool 27 | { 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Console/StatsListCommand.php: -------------------------------------------------------------------------------- 1 | } 25 | {--name= : Name used when sharing project statistic} 26 | {--dry-run : Do not make request to share statistic}'; 27 | 28 | /** 29 | * The console command description. 30 | * 31 | * @var string 32 | */ 33 | protected $description = 'Generate statistics for this Laravel project'; 34 | 35 | /** 36 | * Execute the console command. 37 | */ 38 | public function handle(): void 39 | { 40 | $classes = app(ClassesFinder::class)->findAndLoadClasses(); 41 | 42 | // Transform Classes into ReflectionClass instances 43 | // Remove Classes based on the RejectionStrategy 44 | // Remove Classes based on the namespace 45 | $reflectionClasses = $classes 46 | ->map(static fn ($class) => new ReflectionClass($class)) 47 | ->reject(static fn (ReflectionClass $class) => app(config('stats.rejection_strategy', RejectVendorClasses::class)) 48 | ->shouldClassBeRejected($class)) 49 | ->unique(static fn (ReflectionClass $class) => $class->getFileName()) 50 | ->reject(static function (ReflectionClass $class) { 51 | // Never discard anonymous database migrations 52 | if (Str::contains($class->getName(), 'Migration@anonymous')) { 53 | return false; 54 | } 55 | 56 | foreach (config('stats.ignored_namespaces', []) as $namespace) { 57 | if (Str::startsWith($class->getNamespaceName(), $namespace)) { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | }); 64 | 65 | $project = new Project($reflectionClasses); 66 | 67 | $this->renderOutput($project); 68 | 69 | if ($this->option('share') === true) { 70 | $this->warn('The share option has been deprecated and will be removed in a future update.'); 71 | } 72 | } 73 | 74 | private function getArrayOfComponentsToDisplay(): array 75 | { 76 | if (is_null($this->option('components'))) { 77 | return []; 78 | } 79 | 80 | return explode(',', $this->option('components')); 81 | } 82 | 83 | private function renderOutput(Project $project): void 84 | { 85 | if ($this->option('json') === true) { 86 | $json = (new JsonOutput())->render( 87 | $project, 88 | $this->option('verbose'), 89 | $this->getArrayOfComponentsToDisplay() 90 | ); 91 | 92 | $this->output->text(json_encode($json)); 93 | } else { 94 | (new AsciiTableOutput($this->output))->render( 95 | $project, 96 | $this->option('verbose'), 97 | $this->getArrayOfComponentsToDisplay() 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Contracts/Classifier.php: -------------------------------------------------------------------------------- 1 | output = $output; 39 | } 40 | 41 | public function render(Project $project, bool $isVerbose = false, array $filterByComponentName = []): void 42 | { 43 | $this->isVerbose = $isVerbose; 44 | $this->project = $project; 45 | 46 | $groupedByComponent = $project->classifiedClassesGroupedAndFilteredByComponentNames($filterByComponentName); 47 | 48 | $table = new Table($this->output); 49 | $this->rightAlignNumbers($table); 50 | 51 | $table 52 | ->setHeaders(['Name', 'Classes', 'Methods', 'Methods/Class', 'LoC', 'LLoC', 'LLoC/Method']); 53 | 54 | // Render "Core" components 55 | $this->renderComponents($table, $groupedByComponent->filter(static fn ($_, $key) => $key !== 'Other' && ! Str::contains($key, 'Test'))); 56 | 57 | // Render Test components 58 | $this->renderComponents($table, $groupedByComponent->filter(static fn ($_, $key) => Str::contains($key, 'Test'))); 59 | 60 | // Render "Other" component 61 | $this->renderComponents($table, $groupedByComponent->filter(static fn ($_, $key) => $key === 'Other')); 62 | 63 | $table->addRow(new TableSeparator); 64 | $this->addTotalRow($table); 65 | $this->addMetaRow($table); 66 | 67 | $table->render(); 68 | } 69 | 70 | private function renderComponents(Table $table, Collection $groupedByComponent): void 71 | { 72 | foreach ($groupedByComponent as $componentName => $classifiedClasses) { 73 | $component = new Component($componentName, $classifiedClasses); 74 | 75 | $this->addComponentTableRow($table, $component); 76 | 77 | // If the verbose option has been passed, also display each 78 | // classified Class in it's own row 79 | if ($this->isVerbose) { 80 | foreach ($classifiedClasses as $classifiedClass) { 81 | $this->addClassifiedClassTableRow($table, $classifiedClass); 82 | } 83 | 84 | $table->addRow(new TableSeparator); 85 | } 86 | } 87 | } 88 | 89 | private function addComponentTableRow(Table $table, Component $component): void 90 | { 91 | $table->addRow([ 92 | 'name' => $component->name, 93 | 'number_of_classes' => $component->getNumberOfClasses(), 94 | 'number_of_methods' => $component->getNumberOfMethods(), 95 | 'methods_per_class' => $component->getNumberOfMethodsPerClass(), 96 | 'loc' => $component->getLinesOfCode(), 97 | 'lloc' => $component->getLogicalLinesOfCode(), 98 | 'lloc_per_method' => $component->getLogicalLinesOfCodePerMethod(), 99 | ]); 100 | } 101 | 102 | private function addClassifiedClassTableRow(Table $table, ClassifiedClass $classifiedClass): void 103 | { 104 | $table->addRow([ 105 | new TableCell( 106 | '- '.$classifiedClass->reflectionClass->getName(), 107 | ['colspan' => 2] 108 | ), 109 | $classifiedClass->getNumberOfMethods(), 110 | $classifiedClass->getNumberOfMethods(), 111 | $classifiedClass->getLines(), 112 | $classifiedClass->getLogicalLinesOfCode(), 113 | $classifiedClass->getLogicalLinesOfCodePerMethod(), 114 | ]); 115 | } 116 | 117 | private function addTotalRow(Table $table): void 118 | { 119 | $table->addRow([ 120 | 'name' => 'Total', 121 | 'number_of_classes' => $this->project->statistic()->getNumberOfClasses(), 122 | 'number_of_methods' => $this->project->statistic()->getNumberOfMethods(), 123 | 'methods_per_class' => $this->project->statistic()->getNumberOfMethodsPerClass(), 124 | 'loc' => $this->project->statistic()->getLinesOfCode(), 125 | 'lloc' => $this->project->statistic()->getLogicalLinesOfCode(), 126 | 'lloc_per_method' => $this->project->statistic()->getLogicalLinesOfCodePerMethod(), 127 | ]); 128 | } 129 | 130 | private function addMetaRow(Table $table): void 131 | { 132 | $table->setFooterTitle(implode(' • ', [ 133 | 'Code LLoC: ' . $this->project->statistic()->getLogicalLinesOfCodeForApplicationCode(), 134 | 'Test LLoC: ' . $this->project->statistic()->getLogicalLinesOfCodeForTestCode(), 135 | 'Code/Test Ratio: 1:'.$this->project->statistic()->getApplicationCodeToTestCodeRatio(), 136 | 'Routes: '.app(NumberOfRoutes::class)->get(), 137 | ])); 138 | } 139 | 140 | private function rightAlignNumbers(Table $table): void 141 | { 142 | for ($i = 1; $i <= 6; ++$i) { 143 | $table->setColumnStyle($i, (new TableStyle)->setPadType(STR_PAD_LEFT)); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Outputs/JsonOutput.php: -------------------------------------------------------------------------------- 1 | [], 17 | 'total' => $this->getTotalArray($project), 18 | 'meta' => $this->getMetaArray($project), 19 | ]; 20 | 21 | $groupedByComponent = $project->classifiedClassesGroupedAndFilteredByComponentNames($filterByComponentName); 22 | 23 | foreach ($groupedByComponent as $componentName => $classifiedClasses) { 24 | $singleComponent = $this->getStatisticsArrayComponent($componentName, $classifiedClasses); 25 | 26 | if ($isVerbose) { 27 | $arrayOfClasses = []; 28 | 29 | foreach ($classifiedClasses as $classifiedClass) { 30 | $arrayOfClasses[] = $this->getStatisticsArrayForSingleClass($classifiedClass); 31 | } 32 | 33 | $singleComponent['classes'] = $arrayOfClasses; 34 | } 35 | 36 | $jsonStructure['components'][] = $singleComponent; 37 | } 38 | 39 | return $jsonStructure; 40 | } 41 | 42 | private function getTotalArray(Project $project): array 43 | { 44 | return [ 45 | 'number_of_classes' => $project->statistic()->getNumberOfClasses(), 46 | 'number_of_methods' => $project->statistic()->getNumberOfMethods(), 47 | 'methods_per_class' => $project->statistic()->getNumberOfMethodsPerClass(), 48 | 'loc' => $project->statistic()->getLinesOfCode(), 49 | 'lloc' => $project->statistic()->getLogicalLinesOfCode(), 50 | 'lloc_per_method' => $project->statistic()->getLogicalLinesOfCodePerMethod(), 51 | ]; 52 | } 53 | 54 | private function getMetaArray(Project $project): array 55 | { 56 | return [ 57 | 'code_lloc' => $project->statistic()->getLogicalLinesOfCodeForApplicationCode(), 58 | 'test_lloc' => $project->statistic()->getLogicalLinesOfCodeForTestCode(), 59 | 'code_to_test_ratio' => $project->statistic()->getApplicationCodeToTestCodeRatio(), 60 | 'number_of_routes' => app(NumberOfRoutes::class)->get(), 61 | ]; 62 | } 63 | 64 | private function getStatisticsArrayComponent(string $componentName, Collection $classifiedClasses): array 65 | { 66 | $component = new Component($componentName, $classifiedClasses); 67 | 68 | return [ 69 | 'name' => $component->name, 70 | 'number_of_classes' => $component->getNumberOfClasses(), 71 | 'number_of_methods' => $component->getNumberOfMethods(), 72 | 'methods_per_class' => $component->getNumberOfMethodsPerClass(), 73 | 'loc' => $component->getLinesOfCode(), 74 | 'lloc' => $component->getLogicalLinesOfCode(), 75 | 'lloc_per_method' => $component->getLogicalLinesOfCodePerMethod(), 76 | ]; 77 | } 78 | 79 | private function getStatisticsArrayForSingleClass(ClassifiedClass $classifiedClass): array 80 | { 81 | return [ 82 | 'name' => $classifiedClass->reflectionClass->getName(), 83 | 'methods' => $classifiedClass->getNumberOfMethods(), 84 | 'methods_per_class' => $classifiedClass->getNumberOfMethods(), 85 | 'loc' => $classifiedClass->getLines(), 86 | 'lloc' => $classifiedClass->getLogicalLinesOfCode(), 87 | 'lloc_per_method' => $classifiedClass->getLogicalLinesOfCodePerMethod(), 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Project.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private $classifiedClasses; 17 | 18 | public function __construct( 19 | private readonly Collection $classes 20 | ) { 21 | // Loop through ReflectionClasses and classify them. 22 | $this->classifiedClasses = $this->classes->map(static fn (ReflectionClass $reflectionClass) => new ClassifiedClass( 23 | $reflectionClass, 24 | app(Classifier::class)->getClassifierForClassInstance($reflectionClass) 25 | )); 26 | } 27 | 28 | public function classifiedClasses(): Collection 29 | { 30 | return $this->classifiedClasses; 31 | } 32 | 33 | public function classifiedClassesGroupedByComponentName(): Collection 34 | { 35 | return $this->classifiedClasses() 36 | ->groupBy(static fn (ClassifiedClass $classifiedClass) => $classifiedClass->classifier->name()) 37 | ->sortBy(static fn ($_, string $componentName) => $componentName); 38 | } 39 | 40 | public function classifiedClassesGroupedAndFilteredByComponentNames(array $componentNamesToFilter = []): Collection 41 | { 42 | $shouldCollectionBeFiltered = ! empty(array_filter($componentNamesToFilter)); 43 | 44 | return $this->classifiedClassesGroupedByComponentName() 45 | ->when( 46 | $shouldCollectionBeFiltered, 47 | static fn (Collection $components) => $components->filter(static fn ($_item, string $key) => in_array($key, $componentNamesToFilter)) 48 | ); 49 | } 50 | 51 | public function statistic(): ProjectStatistic 52 | { 53 | return new ProjectStatistic($this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ReflectionClass.php: -------------------------------------------------------------------------------- 1 | getFileName(), '/vendor/'); 18 | } 19 | 20 | /** 21 | * Determine whether the class uses the given trait. 22 | * 23 | * 24 | */ 25 | public function usesTrait(string $name): bool 26 | { 27 | return collect($this->getTraits()) 28 | ->contains(static fn (NativeReflectionClass $trait) => $trait->name == $name); 29 | } 30 | 31 | /** 32 | * Return a collection of methods defined on the given class. 33 | * This ignores methods defined in parent class, traits etc. 34 | */ 35 | public function getDefinedMethods(): Collection 36 | { 37 | return collect($this->getMethods()) 38 | ->filter(fn (ReflectionMethod $method) => $method->getFileName() === $this->getFileName()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/RejectionStrategies/RejectInternalClasses.php: -------------------------------------------------------------------------------- 1 | isInternal(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RejectionStrategies/RejectVendorClasses.php: -------------------------------------------------------------------------------- 1 | isInternal() || 13 | $class->isVendorProvided(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Statistics/NumberOfRoutes.php: -------------------------------------------------------------------------------- 1 | getRoutes())->count(); 14 | } catch (Exception) { 15 | return 0; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Statistics/ProjectStatistic.php: -------------------------------------------------------------------------------- 1 | numberOfClasses === null) { 47 | $this->numberOfClasses = $this->project->classifiedClasses()->count(); 48 | } 49 | 50 | return $this->numberOfClasses; 51 | } 52 | 53 | public function getNumberOfMethods(): int 54 | { 55 | if ($this->numberOfMethods === null) { 56 | $this->numberOfMethods = $this->project->classifiedClasses()->sum(static fn (ClassifiedClass $class) => $class->getNumberOfMethods()); 57 | } 58 | 59 | return $this->numberOfMethods; 60 | } 61 | 62 | public function getNumberOfMethodsPerClass(): float 63 | { 64 | if ($this->numberOfMethodsPerClass === null) { 65 | $this->numberOfMethodsPerClass = round($this->getNumberOfMethods() / $this->getNumberOfClasses(), 2); 66 | } 67 | 68 | return $this->numberOfMethodsPerClass; 69 | } 70 | 71 | public function getLinesOfCode(): int 72 | { 73 | if ($this->linesOfCode === null) { 74 | $this->linesOfCode = $this->project->classifiedClasses()->sum(static fn (ClassifiedClass $class) => $class->getLines()); 75 | } 76 | 77 | return $this->linesOfCode; 78 | } 79 | 80 | public function getLogicalLinesOfCode(): float 81 | { 82 | if ($this->logicalLinesOfCode === null) { 83 | $this->logicalLinesOfCode = $this->project->classifiedClasses()->sum(static fn (ClassifiedClass $class) => $class->getLogicalLinesOfCode()); 84 | } 85 | 86 | return $this->logicalLinesOfCode; 87 | } 88 | 89 | public function getLogicalLinesOfCodePerMethod(): float 90 | { 91 | if ($this->logicalLinesOfCodePerMethod === null) { 92 | if ($this->getNumberOfMethods() === 0) { 93 | $this->logicalLinesOfCodePerMethod = 0; 94 | } else { 95 | $this->logicalLinesOfCodePerMethod = round($this->getLogicalLinesOfCode() / $this->getNumberOfMethods(), 2); 96 | } 97 | } 98 | 99 | return $this->logicalLinesOfCodePerMethod; 100 | } 101 | 102 | public function getLogicalLinesOfCodeForApplicationCode(): float 103 | { 104 | return $this 105 | ->project 106 | ->classifiedClasses() 107 | ->filter(static fn (ClassifiedClass $classifiedClass) => $classifiedClass->classifier->countsTowardsApplicationCode()) 108 | ->sum(static fn (ClassifiedClass $class) => $class->getLogicalLinesOfCode()); 109 | } 110 | 111 | public function getLogicalLinesOfCodeForTestCode(): float 112 | { 113 | return $this 114 | ->project 115 | ->classifiedClasses() 116 | ->filter(static fn (ClassifiedClass $classifiedClass) => $classifiedClass->classifier->countsTowardsTests()) 117 | ->sum(static fn (ClassifiedClass $class) => $class->getLogicalLinesOfCode()); 118 | } 119 | 120 | public function getApplicationCodeToTestCodeRatio(): float 121 | { 122 | return round( 123 | $this->getLogicalLinesOfCodeForTestCode() / $this->getLogicalLinesOfCodeForApplicationCode(), 124 | 1 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/StatsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 21 | $this->publishes([ 22 | $this->config => base_path('config/stats.php'), 23 | ], 'config'); 24 | } 25 | } 26 | 27 | /** 28 | * Register the service provider. 29 | */ 30 | public function register(): void 31 | { 32 | $this->mergeConfigFrom($this->config, 'stats'); 33 | 34 | $this->commands([ 35 | StatsListCommand::class, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ValueObjects/ClassifiedClass.php: -------------------------------------------------------------------------------- 1 | reflectionClass = $reflectionClass; 57 | $this->classifier = $classifier; 58 | } 59 | 60 | /** 61 | * Return the total number of Methods declared in all declared classes. 62 | */ 63 | public function getNumberOfMethods(): int 64 | { 65 | if ($this->numberOfMethods === null) { 66 | $this->numberOfMethods = $this->reflectionClass->getDefinedMethods()->count(); 67 | } 68 | 69 | return $this->numberOfMethods; 70 | } 71 | 72 | public function getNumberOfPublicMethods(): int 73 | { 74 | if ($this->numberOfPublicMethods === null) { 75 | $this->numberOfPublicMethods = $this->reflectionClass->getDefinedMethods() 76 | ->filter(static fn (ReflectionMethod $method) => $method->isPublic())->count(); 77 | } 78 | 79 | return $this->numberOfPublicMethods; 80 | } 81 | 82 | public function getNumberOfNonPublicMethods(): int 83 | { 84 | if ($this->numberOfNonPublicMethods === null) { 85 | $this->numberOfNonPublicMethods = $this->reflectionClass->getDefinedMethods() 86 | ->filter(static fn (ReflectionMethod $method) => ! $method->isPublic())->count(); 87 | } 88 | 89 | return $this->numberOfNonPublicMethods; 90 | } 91 | 92 | /** 93 | * Return the total number of lines. 94 | */ 95 | public function getLines(): int 96 | { 97 | if ($this->linesOfCode === null) { 98 | $this->linesOfCode = app(Analyser::class) 99 | ->countFiles([$this->reflectionClass->getFileName()], false)['loc']; 100 | } 101 | 102 | return $this->linesOfCode; 103 | } 104 | 105 | /** 106 | * Return the total number of lines of code. 107 | */ 108 | public function getLogicalLinesOfCode(): float 109 | { 110 | if ($this->logicalLinesOfCode === null) { 111 | $this->logicalLinesOfCode = app(Analyser::class) 112 | ->countFiles([$this->reflectionClass->getFileName()], false)['lloc']; 113 | } 114 | 115 | return $this->logicalLinesOfCode; 116 | } 117 | 118 | /** 119 | * Return the average number of lines of code per method. 120 | */ 121 | public function getLogicalLinesOfCodePerMethod(): float 122 | { 123 | if ($this->logicalLinesOfCodePerMethod === null) { 124 | if ($this->getNumberOfMethods() === 0) { 125 | $this->logicalLinesOfCodePerMethod = 0; 126 | } else { 127 | $this->logicalLinesOfCodePerMethod = round($this->getLogicalLinesOfCode() / $this->getNumberOfMethods(), 2); 128 | } 129 | } 130 | 131 | return $this->logicalLinesOfCodePerMethod; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/ValueObjects/Component.php: -------------------------------------------------------------------------------- 1 | numberOfClasses === null) { 58 | $this->numberOfClasses = $this->classifiedClasses->count(); 59 | } 60 | 61 | return $this->numberOfClasses; 62 | } 63 | 64 | public function getNumberOfMethods(): int 65 | { 66 | if ($this->numberOfMethods === null) { 67 | $this->numberOfMethods = $this->classifiedClasses->sum(static fn (ClassifiedClass $class) => $class->getNumberOfMethods()); 68 | } 69 | 70 | return $this->numberOfMethods; 71 | } 72 | 73 | public function getNumberOfPublicMethods(): int 74 | { 75 | if ($this->numberOfPublicMethods === null) { 76 | $this->numberOfPublicMethods = $this->classifiedClasses->sum(static fn (ClassifiedClass $class) => $class->getNumberOfPublicMethods()); 77 | } 78 | 79 | return $this->numberOfPublicMethods; 80 | } 81 | 82 | public function getNumberOfNonPublicMethods(): int 83 | { 84 | if ($this->numberOfNonPublicMethods === null) { 85 | $this->numberOfNonPublicMethods = $this->classifiedClasses->sum(static fn (ClassifiedClass $class) => $class->getNumberOfNonPublicMethods()); 86 | } 87 | 88 | return $this->numberOfNonPublicMethods; 89 | } 90 | 91 | public function getNumberOfMethodsPerClass(): float 92 | { 93 | if ($this->numberOfMethodsPerClass === null) { 94 | $this->numberOfMethodsPerClass = round($this->getNumberOfMethods() / $this->getNumberOfClasses(), 2); 95 | } 96 | 97 | return $this->numberOfMethodsPerClass; 98 | } 99 | 100 | public function getLinesOfCode(): int 101 | { 102 | if ($this->linesOfCode === null) { 103 | $this->linesOfCode = $this->classifiedClasses->sum(static fn (ClassifiedClass $class) => $class->getLines()); 104 | } 105 | 106 | return $this->linesOfCode; 107 | } 108 | 109 | public function getLogicalLinesOfCode(): float 110 | { 111 | if ($this->logicalLinesOfCode === null) { 112 | $this->logicalLinesOfCode = $this->classifiedClasses->sum(static fn (ClassifiedClass $class) => $class->getLogicalLinesOfCode()); 113 | } 114 | 115 | return $this->logicalLinesOfCode; 116 | } 117 | 118 | public function getLogicalLinesOfCodePerMethod(): float 119 | { 120 | if ($this->logicalLinesOfCodePerMethod === null) { 121 | if ($this->getNumberOfMethods() === 0) { 122 | $this->logicalLinesOfCodePerMethod = 0; 123 | } else { 124 | $this->logicalLinesOfCodePerMethod = round($this->getLogicalLinesOfCode() / $this->getNumberOfMethods(), 2); 125 | } 126 | } 127 | 128 | return $this->logicalLinesOfCodePerMethod; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /stubs/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Expectations 23 | |-------------------------------------------------------------------------- 24 | | 25 | | When you're writing tests, you often need to check that values meet certain conditions. The 26 | | "expect()" function gives you access to a set of "expectations" methods that you can use 27 | | to assert different things. Of course, you may extend the Expectation API at any time. 28 | | 29 | */ 30 | 31 | expect()->extend('toBeOne', function () { 32 | return $this->toBe(1); 33 | }); 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Functions 38 | |-------------------------------------------------------------------------- 39 | | 40 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 41 | | project that you don't want to repeat in every file. Here you can also expose helpers as 42 | | global functions to help you to reduce the number of lines of code in your test files. 43 | | 44 | */ 45 | 46 | function something() 47 | { 48 | // .. 49 | } 50 | 51 | function getFixture($path) 52 | { 53 | return File::get(base_path("tests/fixtures/{$path}")); 54 | } 55 | -------------------------------------------------------------------------------- /stubs/PestTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | 7 | it('asserts that true is true', function () { 8 | expect(true)->toBeTrue(); 9 | }); 10 | -------------------------------------------------------------------------------- /stubs/README.md: -------------------------------------------------------------------------------- 1 | # Not autoloaded Stubs 2 | 3 | This directory contains stubs that are not autoloaded by composer. 4 | --------------------------------------------------------------------------------