├── .editorconfig ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Application │ ├── Analyze │ │ ├── AnalyzeAction.php │ │ ├── AnalyzeMetric.php │ │ ├── AnalyzePresenter.php │ │ ├── AnalyzeRequest.php │ │ ├── AnalyzeResponse.php │ │ └── AnalyzeResponseMapper.php │ ├── Cyclic │ │ ├── CyclicAction.php │ │ ├── CyclicPresenter.php │ │ ├── CyclicRequest.php │ │ ├── CyclicResponse.php │ │ └── CyclicResponseMapper.php │ └── Weakness │ │ ├── WeaknessAction.php │ │ ├── WeaknessPresenter.php │ │ ├── WeaknessRequest.php │ │ ├── WeaknessResponse.php │ │ └── WeaknessResponseMapper.php ├── Commands │ ├── AbstractCommand.php │ ├── Analyze │ │ ├── Class │ │ │ ├── AnalyzeCommand.php │ │ │ ├── Graph │ │ │ │ ├── GraphPresenterFactory.php │ │ │ │ └── GraphSettingsFactory.php │ │ │ ├── PresenterFactory.php │ │ │ ├── Summary │ │ │ │ ├── SummaryPresenterFactory.php │ │ │ │ └── SummarySettingsFactory.php │ │ │ └── TransformerFactory.php │ │ └── Component │ │ │ ├── ComponentCommand.php │ │ │ ├── Factories │ │ │ ├── PresenterFactory.php │ │ │ └── TransformerFactory.php │ │ │ ├── Graph │ │ │ ├── GraphPresenterFactory.php │ │ │ └── GraphSettingsFactory.php │ │ │ └── Summary │ │ │ ├── SummaryPresenterFactory.php │ │ │ └── SummarySettingsFactory.php │ ├── Cyclic │ │ ├── CyclicCommand.php │ │ ├── SummaryPresenterFactory.php │ │ └── SummarySettingsFactory.php │ └── Weakness │ │ ├── SummaryPresenterFactory.php │ │ ├── SummarySettingsFactory.php │ │ └── WeaknessCommand.php ├── Domain │ ├── Aggregators │ │ └── DependencyAggregator.php │ ├── Entities │ │ └── ClassDependencies.php │ ├── Ports │ │ ├── Aggregators │ │ │ └── FileAggregator.php │ │ └── Repositories │ │ │ └── FileRepository.php │ ├── Services │ │ ├── Cycle.php │ │ ├── CyclicDependency.php │ │ ├── Stack.php │ │ └── Visited.php │ └── ValueObjects │ │ ├── Abstractness.php │ │ ├── Coupling.php │ │ ├── Dependencies.php │ │ ├── Fqcn.php │ │ ├── IsAbstract.php │ │ ├── IsInterface.php │ │ └── SimpleBoolean.php ├── Infrastructure │ ├── Analyze │ │ ├── Adapters │ │ │ ├── Jerowork │ │ │ │ ├── ClassDependenciesParserAdapter.php │ │ │ │ ├── Collectors │ │ │ │ │ └── ClassTypeCollector.php │ │ │ │ ├── DataTransferObjects │ │ │ │ │ └── ClassAnalysisAdapter.php │ │ │ │ ├── NativeDecliner.php │ │ │ │ ├── NodeTraverserFactory.php │ │ │ │ └── Visitors │ │ │ │ │ └── DetectClassTypeVisitor.php │ │ │ └── Services │ │ │ │ └── AnalyzerServiceAdapter.php │ │ └── Ports │ │ │ ├── AnalyzerService.php │ │ │ ├── ClassAnalysis.php │ │ │ └── ClassDependenciesParser.php │ ├── File │ │ ├── Adapters │ │ │ ├── Aggregators │ │ │ │ └── FileAggregatorAdapter.php │ │ │ ├── DataTransferObjects │ │ │ │ └── FileAdapter.php │ │ │ └── Repositories │ │ │ │ └── FileRepositoryAdapter.php │ │ └── Ports │ │ │ └── File.php │ ├── Graph │ │ └── Adapters │ │ │ └── Cytoscape │ │ │ ├── CytoscapeNetwork.php │ │ │ ├── CytoscapeNetworkBuilder.php │ │ │ ├── Edges.php │ │ │ └── Nodes.php │ └── Views │ │ └── Adapters │ │ └── SystemFileLauncherAdapter.php ├── Presenter │ ├── Analyze │ │ ├── Class │ │ │ ├── Graph │ │ │ │ ├── GraphEnums.php │ │ │ │ ├── GraphMapper.php │ │ │ │ ├── GraphPresenter.php │ │ │ │ ├── GraphSettings.php │ │ │ │ ├── GraphView.php │ │ │ │ └── GraphViewModel.php │ │ │ ├── Shared │ │ │ │ ├── Metric.php │ │ │ │ └── MetricMapper.php │ │ │ └── Summary │ │ │ │ ├── SummaryMapper.php │ │ │ │ ├── SummaryPresenter.php │ │ │ │ ├── SummarySettings.php │ │ │ │ ├── SummaryView.php │ │ │ │ └── SummaryViewModel.php │ │ ├── Component │ │ │ ├── Graph │ │ │ │ ├── GraphMapper.php │ │ │ │ ├── GraphPresenter.php │ │ │ │ ├── GraphSettings.php │ │ │ │ ├── GraphView.php │ │ │ │ └── GraphViewModel.php │ │ │ ├── Shared │ │ │ │ ├── Collector.php │ │ │ │ ├── Component.php │ │ │ │ ├── ComponentFactory.php │ │ │ │ └── ComponentMapper.php │ │ │ └── Summary │ │ │ │ ├── SummaryMapper.php │ │ │ │ ├── SummaryPresenter.php │ │ │ │ ├── SummarySettings.php │ │ │ │ ├── SummaryView.php │ │ │ │ └── SummaryViewModel.php │ │ └── Shared │ │ │ ├── Calculators │ │ │ ├── AbstractnessCalculator.php │ │ │ ├── Calculator.php │ │ │ ├── MaintainabilityCalculator.php │ │ │ └── StabilityCalculator.php │ │ │ ├── Filters │ │ │ ├── Collectors │ │ │ │ ├── Components.php │ │ │ │ ├── Depth.php │ │ │ │ └── Metrics.php │ │ │ ├── Contracts │ │ │ │ └── Transformer.php │ │ │ └── Transformers │ │ │ │ ├── ComponentTransformer.php │ │ │ │ ├── NullTransformer.php │ │ │ │ └── TargetTransformer.php │ │ │ ├── Network │ │ │ ├── Network.php │ │ │ ├── NetworkAttribute.php │ │ │ ├── NetworkAttributesMapper.php │ │ │ ├── NetworkBuilder.php │ │ │ └── Networkable.php │ │ │ └── Views │ │ │ └── SystemFileLauncher.php │ ├── ArrayFormatter.php │ ├── Cyclic │ │ └── Summary │ │ │ ├── CycleHelper.php │ │ │ ├── CyclicPresenterMapper.php │ │ │ ├── SummaryPresenter.php │ │ │ ├── SummarySettings.php │ │ │ ├── SummaryView.php │ │ │ └── SummaryViewModel.php │ ├── NameFormatter.php │ └── Weakness │ │ └── Summary │ │ ├── SummaryPresenter.php │ │ ├── SummarySettings.php │ │ ├── SummaryView.php │ │ ├── SummaryViewModel.php │ │ └── WeaknessPresenterMapper.php └── Providers │ ├── AppServiceProvider.php │ └── Custom │ └── CustomNodeTraverserFactory.php ├── arts ├── graph.png └── home.png ├── bootstrap ├── app.php └── providers.php ├── box.json ├── builds └── class-dependencies-analyzer ├── class-dependencies-analyzer ├── composer.json ├── composer.lock ├── config ├── app.php ├── commands.php └── view.php ├── phpunit.xml.dist ├── resources └── views │ ├── class-graph.blade.php │ └── components-graph.blade.php ├── storage ├── app │ └── .gitignore └── framework │ └── views │ └── .gitignore └── tests ├── Builders ├── AnalyzeMetricBuilder.php ├── ClassDependenciesBuilder.php ├── DependencyAggregatorBuilder.php └── WeaknessResponseBuilder.php ├── CreatesApplication.php ├── Feature ├── AnalyzeClassTest.php ├── CyclicTest.php └── WeaknessTest.php ├── Pest.php ├── TestCase.php └── Unit ├── Domain ├── Aggregators │ └── DependencyAggregatorTest.php ├── Entities │ └── ClassDependenciesTest.php ├── Services │ └── CyclicDependencyTest.php └── ValueObjects │ ├── CouplingTest.php │ └── FqcnTest.php ├── Infrastructure └── Services │ ├── AnalyzerServiceAdapterTest.php │ ├── FileStub.php │ └── Stubs │ ├── A.php │ └── Native.php └── Presenter ├── Calculators └── MaintainabilityCalculatorTest.php ├── ComponentMapperTest.php ├── Filters └── TargetFilterTest.php ├── Formatters ├── ArrayFormatterTest.php └── NameFormatterTest.php ├── Helpers └── CycleHelperTest.php ├── WeaknessSummaryPresenterTest.php └── WeaknessSummaryViewSpy.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_size = 2 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | /.github export-ignore 3 | .scrutinizer.yml export-ignore 4 | BACKERS.md export-ignore 5 | CONTRIBUTING.md export-ignore 6 | CHANGELOG.md export-ignore 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode 4 | /.vagrant 5 | .phpunit.result.cache 6 | 7 | graph.html 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | 6 | 7 | # php-class-dependencies-analyzer 8 | 9 | [![testing](https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer/actions/workflows/testing.yml/badge.svg)](https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer/actions/workflows/testing.yml) 10 | ![GitHub Tag](https://img.shields.io/github/v/tag/DeGraciaMathieu/php-class-dependencies-analyzer) 11 | 12 | ## Phar 13 | This tool is distributed as a [PHP Archive (PHAR)](https://www.php.net/phar): 14 | 15 | ``` 16 | wget https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer/raw/main/builds/class-dependencies-analyzer 17 | ``` 18 | 19 | ``` 20 | php class-dependencies-analyzer --version 21 | ``` 22 | 23 | ## Documentation 24 | 25 | Full documentation [here](https://php-quality-tools.com/class-dependencies-analyzer/). 26 | 27 |

28 | 29 |

30 | 31 | > 🇫🇷 For French developers, an [article](https://laravel-france.com/posts/des-dependances-stables-pour-une-architecture-de-qualite) on Laravel France explores these stable dependency concepts. 32 | 33 | > [!TIP] 34 | > Other analysis [tools](https://github.com/DeGraciaMathieu) are available. 35 | -------------------------------------------------------------------------------- /app/Application/Analyze/AnalyzeAction.php: -------------------------------------------------------------------------------- 1 | hello(); 23 | 24 | $fileAggregator = $this->fileRepository->find($request->path); 25 | 26 | $dependencyAggregator = $fileAggregator->getAllDependencies(); 27 | 28 | $dependencyAggregator->calculateInstability(); 29 | 30 | $dependencyAggregator->calculateAbstractness(); 31 | 32 | $dependencyAggregator->filter($request->only, $request->exclude); 33 | 34 | $presenter->present( 35 | $this->mapper->from($dependencyAggregator), 36 | ); 37 | 38 | } catch (Throwable $e) { 39 | $presenter->error($e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Application/Analyze/AnalyzeMetric.php: -------------------------------------------------------------------------------- 1 | metric['name']; 14 | } 15 | 16 | public function dependencies(): array 17 | { 18 | return $this->metric['dependencies']; 19 | } 20 | 21 | public function abstract(): bool 22 | { 23 | return $this->metric['abstract']; 24 | } 25 | 26 | public function efferentCoupling(): float 27 | { 28 | return $this->metric['coupling']['efferent']; 29 | } 30 | 31 | public function afferentCoupling(): float 32 | { 33 | return $this->metric['coupling']['afferent']; 34 | } 35 | 36 | public function instability(): float 37 | { 38 | return $this->metric['coupling']['instability']; 39 | } 40 | 41 | public function numberOfAbstractDependencies(): int 42 | { 43 | return $this->metric['abstractness']['numberOfAbstractDependencies']; 44 | } 45 | 46 | public function abstractnessRatio(): float 47 | { 48 | return $this->metric['abstractness']['ratio']; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Application/Analyze/AnalyzePresenter.php: -------------------------------------------------------------------------------- 1 | count(), 14 | metrics: $this->map($dependencyAggregator), 15 | ); 16 | } 17 | 18 | private function map(DependencyAggregator $dependencyAggregator): array 19 | { 20 | return array_map(function (array $metric) { 21 | return new AnalyzeMetric($metric); 22 | }, $dependencyAggregator->toArray()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Application/Cyclic/CyclicAction.php: -------------------------------------------------------------------------------- 1 | hello(); 23 | 24 | $fileAggregator = $this->fileRepository->find($request->path); 25 | 26 | $dependencyAggregator = $fileAggregator->getAllDependencies(); 27 | 28 | $dependencyAggregator->filter($request->only, $request->exclude); 29 | 30 | $cycles = $dependencyAggregator->detectCycles(); 31 | 32 | $presenter->present( 33 | $this->mapper->from($cycles), 34 | ); 35 | 36 | } catch (Throwable $e) { 37 | $presenter->error($e); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Application/Cyclic/CyclicPresenter.php: -------------------------------------------------------------------------------- 1 | count(), 14 | cycles: $cycles->all(), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Application/Weakness/WeaknessAction.php: -------------------------------------------------------------------------------- 1 | hello(); 22 | 23 | $fileAggregator = $this->fileRepository->find($request->path); 24 | 25 | $dependencyAggregator = $fileAggregator->getAllDependencies(); 26 | 27 | $dependencyAggregator->calculateInstability(); 28 | 29 | $dependencyAggregator->filter($request->only, $request->exclude); 30 | 31 | $presenter->present( 32 | $this->mapper->from($dependencyAggregator), 33 | ); 34 | 35 | } catch (Throwable $e) { 36 | $presenter->error($e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Application/Weakness/WeaknessPresenter.php: -------------------------------------------------------------------------------- 1 | toArray(); 12 | 13 | return new WeaknessResponse( 14 | count: $dependencyAggregator->count(), 15 | metrics: $this->formatClassesMetrics($classes), 16 | ); 17 | } 18 | 19 | private function formatClassesMetrics(array $classes): array 20 | { 21 | $metrics = []; 22 | 23 | foreach ($classes as $class) { 24 | 25 | $instability = $class['coupling']['instability']; 26 | 27 | foreach ($class['dependencies'] as $dependency) { 28 | 29 | $dependency = $classes[$dependency] ?? null; 30 | 31 | if ($dependency && $dependency['coupling']['instability'] > $instability) { 32 | 33 | $metrics[] = [ 34 | 'class' => $class['name'], 35 | 'class_instability' => $instability, 36 | 'dependency' => $dependency['name'], 37 | 'dependency_instability' => $dependency['coupling']['instability'], 38 | 'delta' => $dependency['coupling']['instability'] - $instability, 39 | ]; 40 | } 41 | } 42 | } 43 | 44 | return $metrics; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Commands/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | option($key); 12 | 13 | return $this->stringToList($value); 14 | } 15 | 16 | public function argumentToList(string $key): array 17 | { 18 | $value = $this->argument($key); 19 | 20 | return $this->stringToList($value); 21 | } 22 | 23 | private function stringToList(?string $value): array 24 | { 25 | return $value === null ? [] : explode(',', $value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/AnalyzeCommand.php: -------------------------------------------------------------------------------- 1 | execute( 28 | request: $this->makeRequest(), 29 | presenter: $this->makePresenter(), 30 | ); 31 | } 32 | 33 | private function makeRequest(): AnalyzeRequest 34 | { 35 | return new AnalyzeRequest( 36 | path: $this->argument('path'), 37 | only: $this->optionToList('only'), 38 | exclude: $this->optionToList('exclude'), 39 | ); 40 | } 41 | 42 | private function makePresenter(): AnalyzePresenter 43 | { 44 | return app(PresenterFactory::class)->make($this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/Graph/GraphPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 25 | mapper: $this->mapper, 26 | transformer: $this->transformerFactory->make($command), 27 | settings: $this->settingsFactory->make($command), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/Graph/GraphSettingsFactory.php: -------------------------------------------------------------------------------- 1 | option('debug'), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/PresenterFactory.php: -------------------------------------------------------------------------------- 1 | option('graph') 20 | ? $this->makeGraphPresenter($command) 21 | : $this->makeSummaryPresenter($command); 22 | } 23 | 24 | private function makeGraphPresenter(Command $command): AnalyzePresenter 25 | { 26 | return $this->graphPresenterFactory->make($command); 27 | } 28 | 29 | private function makeSummaryPresenter(Command $command): AnalyzePresenter 30 | { 31 | return $this->summaryPresenterFactory->make($command); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/Summary/SummaryPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 25 | mapper: $this->mapper, 26 | transformer: $this->transformerFactory->make($command), 27 | settings: $this->settingsFactory->make($command), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/Summary/SummarySettingsFactory.php: -------------------------------------------------------------------------------- 1 | option('debug'), 14 | info: $command->option('info'), 15 | humanReadable: $command->option('human-readable'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Class/TransformerFactory.php: -------------------------------------------------------------------------------- 1 | option('target')) { 15 | default => self::makeTargetTransformer($command), 16 | null => self::makeNullTransformer(), 17 | }; 18 | } 19 | 20 | private static function makeTargetTransformer(Command $command): Transformer 21 | { 22 | return app(TargetTransformer::class, [ 23 | 'target' => $command->option('target'), 24 | 'depthLimit' => $command->option('depth-limit'), 25 | ]); 26 | } 27 | 28 | private static function makeNullTransformer(): Transformer 29 | { 30 | return app(NullTransformer::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/ComponentCommand.php: -------------------------------------------------------------------------------- 1 | execute( 25 | request: $this->makeRequest(), 26 | presenter: $this->makePresenter(), 27 | ); 28 | } 29 | 30 | private function makeRequest(): AnalyzeRequest 31 | { 32 | return new AnalyzeRequest( 33 | path: $this->argument('path'), 34 | ); 35 | } 36 | 37 | private function makePresenter(): AnalyzePresenter 38 | { 39 | return app(PresenterFactory::class)->make($this); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Factories/PresenterFactory.php: -------------------------------------------------------------------------------- 1 | option('graph') 20 | ? $this->makeGraphPresenter($command) 21 | : $this->makeSummaryPresenter($command); 22 | } 23 | 24 | private function makeGraphPresenter(Command $command): AnalyzePresenter 25 | { 26 | return $this->graphPresenterFactory->make($command); 27 | } 28 | 29 | private function makeSummaryPresenter(Command $command): AnalyzePresenter 30 | { 31 | return $this->summaryPresenterFactory->make($command); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Factories/TransformerFactory.php: -------------------------------------------------------------------------------- 1 | $command->argumentToList('components'), 15 | ]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Graph/GraphPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 26 | mapper: $this->mapper, 27 | transformer: $this->transformerFactory->make($command), 28 | settings: $this->settingsFactory->make($command), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Graph/GraphSettingsFactory.php: -------------------------------------------------------------------------------- 1 | argumentToList('components'), 14 | info: $command->option('info'), 15 | debug: $command->option('debug'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Summary/SummaryPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 26 | mapper: $this->mapper, 27 | transformer: $this->transformerFactory->make($command), 28 | settings: $this->settingsFactory->make($command), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Commands/Analyze/Component/Summary/SummarySettingsFactory.php: -------------------------------------------------------------------------------- 1 | argumentToList('components'), 14 | info: $command->option('info'), 15 | debug: $command->option('debug'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Commands/Cyclic/CyclicCommand.php: -------------------------------------------------------------------------------- 1 | execute( 24 | request: $this->makeRequest(), 25 | presenter: $this->makePresenter(), 26 | ); 27 | } 28 | 29 | private function makeRequest(): CyclicRequest 30 | { 31 | return new CyclicRequest( 32 | path: $this->argument('path'), 33 | only: $this->optionToList('only'), 34 | exclude: $this->optionToList('exclude'), 35 | ); 36 | } 37 | 38 | private function makePresenter(): CyclicPresenter 39 | { 40 | return app(SummaryPresenterFactory::class)->make($this); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Commands/Cyclic/SummaryPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 23 | mapper: $this->mapper, 24 | settings: $this->settingsFactory->make($command), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Commands/Cyclic/SummarySettingsFactory.php: -------------------------------------------------------------------------------- 1 | option('debug'), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Commands/Weakness/SummaryPresenterFactory.php: -------------------------------------------------------------------------------- 1 | view, 21 | settings: $this->settingsFactory->make($command), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Commands/Weakness/SummarySettingsFactory.php: -------------------------------------------------------------------------------- 1 | option('limit') ?? null, 14 | minDelta: $command->option('min-delta') ?? null, 15 | debug: $command->option('debug') ?? false, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Commands/Weakness/WeaknessCommand.php: -------------------------------------------------------------------------------- 1 | execute( 26 | request: $this->makeRequest(), 27 | presenter: $this->makePresenter(), 28 | ); 29 | } 30 | 31 | private function makeRequest(): WeaknessRequest 32 | { 33 | return new WeaknessRequest( 34 | path: $this->argument('path'), 35 | only: $this->optionToList('only'), 36 | exclude: $this->optionToList('exclude'), 37 | ); 38 | } 39 | 40 | private function makePresenter(): WeaknessPresenter 41 | { 42 | return app(SummaryPresenterFactory::class)->make($this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Domain/Aggregators/DependencyAggregator.php: -------------------------------------------------------------------------------- 1 | classes[$classDependencies->getName()] = $classDependencies; 20 | } 21 | 22 | public function count(): int 23 | { 24 | return count($this->classes); 25 | } 26 | 27 | public function classes(): array 28 | { 29 | return $this->classes; 30 | } 31 | 32 | public function get(string $fqcn): ?ClassDependencies 33 | { 34 | return $this->classes[$fqcn] ?? null; 35 | } 36 | 37 | public function calculateInstability(): void 38 | { 39 | foreach ($this->classes as $givenClass) { 40 | 41 | foreach ($this->classes as $otherClass) { 42 | 43 | if ($givenClass->is($otherClass)) { 44 | continue; 45 | } 46 | 47 | if ($otherClass->isDependentOn($givenClass)) { 48 | $givenClass->incrementAfferent(); 49 | } 50 | } 51 | 52 | $givenClass->calculateInstability(); 53 | } 54 | } 55 | 56 | public function calculateAbstractness(): void 57 | { 58 | foreach ($this->classes as $givenClass) { 59 | 60 | foreach ($givenClass->getDependencies() as $dependency) { 61 | 62 | $dependencyClass = $this->get($dependency); 63 | 64 | if ($dependencyClass && $dependencyClass->isAbstract()) { 65 | $givenClass->incrementNumberOfAbstractDependencies(); 66 | } 67 | } 68 | 69 | $givenClass->calculateAbstractness(); 70 | } 71 | } 72 | 73 | public function detectCycles(): Cycle 74 | { 75 | return $this->cyclicDependency->detect($this->classes); 76 | } 77 | 78 | public function filter(array $only = [], array $exclude = []): void 79 | { 80 | $this->classes = array_filter($this->classes, function ($givenClass) use ($only, $exclude) { 81 | 82 | if ($only) { 83 | return $givenClass->looksLike($only); 84 | } 85 | 86 | if ($exclude) { 87 | return ! $givenClass->looksLike($exclude); 88 | } 89 | 90 | return true; 91 | }); 92 | } 93 | 94 | public function toArray(): array 95 | { 96 | return array_map(function (ClassDependencies $class) { 97 | return $class->toArray(); 98 | }, $this->classes); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/Domain/Entities/ClassDependencies.php: -------------------------------------------------------------------------------- 1 | initializeCouplings(); 24 | } 25 | 26 | private function initializeCouplings(): void 27 | { 28 | $this->coupling = new Coupling(efferent: $this->dependencies->count()); 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->fqcn->getValue(); 34 | } 35 | 36 | public function looksLike(array $filters): bool 37 | { 38 | return $this->fqcn->looksLike($filters); 39 | } 40 | 41 | public function is(ClassDependencies $otherClass): bool 42 | { 43 | return $this->fqcn->is($otherClass->fqcn); 44 | } 45 | 46 | public function incrementAfferent(): void 47 | { 48 | $this->coupling->incrementAfferent(); 49 | } 50 | 51 | public function isDependentOn(ClassDependencies $otherClass): bool 52 | { 53 | return $this->dependencies->knows($otherClass->fqcn); 54 | } 55 | 56 | public function calculateInstability(): void 57 | { 58 | $this->coupling->calculateInstability(); 59 | } 60 | 61 | public function isAbstract(): bool 62 | { 63 | return $this->isAbstract->isTrue() || $this->isInterface->isTrue(); 64 | } 65 | 66 | public function getDependencies(): array 67 | { 68 | return $this->dependencies->getValues(); 69 | } 70 | 71 | public function toArray(): array 72 | { 73 | return [ 74 | 'name' => $this->getName(), 75 | 'dependencies' => $this->getDependencies(), 76 | 'abstract' => $this->isAbstract(), 77 | 'coupling' => $this->coupling->toArray(), 78 | 'abstractness' => $this->abstractness->toArray(), 79 | ]; 80 | } 81 | 82 | public function hasNoDependencies(): bool 83 | { 84 | return $this->coupling->nobodyUsesThis(); 85 | } 86 | 87 | public function incrementNumberOfAbstractDependencies(): void 88 | { 89 | $this->abstractness->increment(); 90 | } 91 | 92 | public function calculateAbstractness(): void 93 | { 94 | $this->abstractness->calculate($this->coupling->efferent); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/Domain/Ports/Aggregators/FileAggregator.php: -------------------------------------------------------------------------------- 1 | cycles[] = $cycle; 12 | } 13 | 14 | public function isEmpty(): bool 15 | { 16 | return empty($this->cycles); 17 | } 18 | 19 | public function count(): int 20 | { 21 | return count($this->cycles); 22 | } 23 | 24 | public function all(): array 25 | { 26 | return $this->cycles; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Domain/Services/CyclicDependency.php: -------------------------------------------------------------------------------- 1 | $_) { 20 | 21 | if ($this->visited->unknown($givenClass)) { 22 | $this->deepDive($givenClass, $classes); 23 | } 24 | } 25 | 26 | return $this->cycle; 27 | } 28 | 29 | private function deepDive(string $class, array $classes): void 30 | { 31 | /** 32 | * Duplicate class can exist in the array. 33 | * If the class is already visited, we can switch to another class. 34 | */ 35 | if ($this->visited->isMarked($class)) { 36 | return; 37 | } 38 | 39 | /** 40 | * This stack is used to detect cycles. 41 | * Progressively, we push dependencies in the stack. 42 | * If the class is already in the stack, we have a cycle. 43 | */ 44 | if ($this->stack->contains($class)) { 45 | 46 | $cycle = $this->stack->extractCycle($class); 47 | 48 | $this->cycle->add($cycle); 49 | 50 | /** 51 | * Stop the recursion, we can switch to another class. 52 | */ 53 | return; 54 | } 55 | 56 | $this->stack->push($class); 57 | 58 | $this->visited->mark($class, false); 59 | 60 | foreach ($classes[$class]->getDependencies() as $dependency) { 61 | if (isset($classes[$dependency])) { 62 | $this->deepDive($dependency, $classes); 63 | } 64 | } 65 | 66 | /** 67 | * Remove the class from the stack. 68 | */ 69 | $this->stack->pop(); 70 | 71 | /** 72 | * Mark the class as fully explored to avoid infinite loops. 73 | */ 74 | $this->visited->mark($class, true); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Domain/Services/Stack.php: -------------------------------------------------------------------------------- 1 | stack, true); 12 | } 13 | 14 | public function push(string $class): void 15 | { 16 | $this->stack[] = $class; 17 | } 18 | 19 | public function extractCycle(string $class): array 20 | { 21 | /** 22 | * Get the index of the class in the stack. 23 | */ 24 | $index = array_search($class, $this->stack); 25 | 26 | /** 27 | * Get the cycle by slicing the stack from the index of the class to the end. 28 | */ 29 | return array_slice($this->stack, $index); 30 | } 31 | 32 | public function pop(): void 33 | { 34 | array_pop($this->stack); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Domain/Services/Visited.php: -------------------------------------------------------------------------------- 1 | visited[$key]); 12 | } 13 | 14 | public function mark(string $key, bool $value): void 15 | { 16 | $this->visited[$key] = $value; 17 | } 18 | 19 | public function isMarked(string $key): bool 20 | { 21 | return $this->visited[$key] ?? false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Domain/ValueObjects/Abstractness.php: -------------------------------------------------------------------------------- 1 | numberOfAbstractDependencies; 15 | } 16 | 17 | public function increment(): void 18 | { 19 | $this->numberOfAbstractDependencies++; 20 | } 21 | 22 | public function calculate(int $totalDependencies): void 23 | { 24 | $this->ratio = $totalDependencies === 0 25 | ? 0 26 | : number_format($this->numberOfAbstractDependencies / $totalDependencies, 2); 27 | } 28 | 29 | public function toArray(): array 30 | { 31 | return [ 32 | 'ratio' => $this->ratio, 33 | 'numberOfAbstractDependencies' => $this->numberOfAbstractDependencies, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Domain/ValueObjects/Coupling.php: -------------------------------------------------------------------------------- 1 | afferent++; 16 | } 17 | 18 | public function nobodyUsesThis(): bool 19 | { 20 | return $this->afferent === 0; 21 | } 22 | 23 | public function calculateInstability(): void 24 | { 25 | $instability = $this->efferent / (($this->afferent + $this->efferent) ?: 1); 26 | 27 | $this->instability = number_format($instability, 2); 28 | } 29 | 30 | public function toArray(): array 31 | { 32 | return [ 33 | 'afferent' => $this->afferent, 34 | 'efferent' => $this->efferent, 35 | 'instability' => $this->instability, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Domain/ValueObjects/Dependencies.php: -------------------------------------------------------------------------------- 1 | getValue(), $this->values); 16 | } 17 | 18 | public function count(): int 19 | { 20 | return count($this->values); 21 | } 22 | 23 | public function getValues(): array 24 | { 25 | return $this->values; 26 | } 27 | 28 | public static function fromArray(array $values): self 29 | { 30 | return new self($values); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Domain/ValueObjects/Fqcn.php: -------------------------------------------------------------------------------- 1 | value); 16 | } 17 | 18 | public function getValue(): string 19 | { 20 | return $this->value; 21 | } 22 | 23 | public function is(Fqcn $otherFqcn): bool 24 | { 25 | return $this->getValue() === $otherFqcn->getValue(); 26 | } 27 | 28 | public static function fromString(string $value): self 29 | { 30 | return new self($value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Domain/ValueObjects/IsAbstract.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | 16 | public static function fromBool(bool $value): self 17 | { 18 | return new static($value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/ClassDependenciesParserAdapter.php: -------------------------------------------------------------------------------- 1 | getFileContent($filePath); 23 | 24 | $ast = $this->parseContent($fileContent); 25 | 26 | $collectors = $this->makeCollectors($filePath); 27 | 28 | $this->traverse($ast, $collectors); 29 | 30 | return ClassAnalysisAdapter::fromArray($collectors); 31 | } 32 | 33 | private function getFileContent(string $filePath): ?string 34 | { 35 | return file_get_contents($filePath); 36 | } 37 | 38 | private function parseContent(string $content): ?array 39 | { 40 | return $this->parser->parse($content); 41 | } 42 | 43 | private function makeCollectors(string $filePath): array 44 | { 45 | return [ 46 | 'dependencies' => new ClassDependenciesCollector($filePath), 47 | 'type' => new ClassTypeCollector($filePath), 48 | ]; 49 | } 50 | 51 | private function traverse(array $ast, array &$collectors): void 52 | { 53 | $traverser = $this->traverserFactory->createTraverser($collectors); 54 | 55 | $traverser->traverse($ast); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/Collectors/ClassTypeCollector.php: -------------------------------------------------------------------------------- 1 | isInterface; 19 | } 20 | 21 | public function isAbstract(): bool 22 | { 23 | return $this->isAbstract; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/DataTransferObjects/ClassAnalysisAdapter.php: -------------------------------------------------------------------------------- 1 | classDependencies->getFqn() ?? ''; 19 | } 20 | 21 | public function dependencies(): array 22 | { 23 | return $this->classDependencies->getDependencyList(); 24 | } 25 | 26 | public function isInterface(): bool 27 | { 28 | return $this->classType->isInterface(); 29 | } 30 | 31 | public function isAbstract(): bool 32 | { 33 | return $this->classType->isAbstract(); 34 | } 35 | 36 | public static function fromArray(array $attributes): self 37 | { 38 | return new self( 39 | $attributes['dependencies'], 40 | $attributes['type'], 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/NativeDecliner.php: -------------------------------------------------------------------------------- 1 | toString(); 36 | 37 | return $this->isNativePrimitiveType($fqn) || $this->isNativePhpClass($fqn); 38 | } 39 | 40 | private function isNativePrimitiveType(string $fqn): bool 41 | { 42 | return in_array(strtolower($fqn), $this->primitiveTypes, true); 43 | } 44 | 45 | private function isNativePhpClass(string $fqn): bool 46 | { 47 | return function_exists($fqn) || 48 | class_exists($fqn, false) || 49 | interface_exists($fqn, false); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/NodeTraverserFactory.php: -------------------------------------------------------------------------------- 1 | addVisitor(new ParentConnectingVisitor()); 29 | $traverser->addVisitor(new ParseClassFqnNodeVisitor($collectors['dependencies'])); 30 | $traverser->addVisitor(new ParseImportedFqnNodeVisitor($collectors['dependencies'])); 31 | $traverser->addVisitor(new DetectClassTypeVisitor($collectors['type'])); 32 | $traverser->addVisitor(new ParseInlineFqnNodeVisitor( 33 | $collectors['dependencies'], 34 | [ 35 | new NamespaceDecliner(), 36 | new ImportedFqnDecliner(), 37 | new PhpNativeAccessorDecliner(), 38 | new NativeDecliner(), 39 | ], 40 | [ 41 | new FullyQualifiedNameProcessor(), 42 | new RootLevelFunctionProcessor(), 43 | new InlineFqnIsImportedProcessor(), 44 | new InlineFqnIsImportedAsAliasProcessor(), 45 | new InlineFqnWithinSameNamespaceProcessor(), 46 | ], 47 | )); 48 | 49 | 50 | return $traverser; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Jerowork/Visitors/DetectClassTypeVisitor.php: -------------------------------------------------------------------------------- 1 | collector->namespace = (string) $node->name; 34 | } 35 | 36 | if ($this->nodeAreClassOrEquivalent($node)) { 37 | 38 | $this->collector->className = (string) $node->name; 39 | 40 | if ($node instanceof Class_) { 41 | $this->collector->isAbstract = $node->isAbstract(); 42 | } 43 | 44 | $this->collector->isInterface = $node instanceof Interface_; 45 | } 46 | } 47 | 48 | public function leaveNode(Node $node): void 49 | { 50 | // 51 | } 52 | 53 | private function nodeAreClassOrEquivalent(Node $node): bool 54 | { 55 | $parts = [Class_::class, Trait_::class, Interface_::class, Enum_::class]; 56 | 57 | return in_array(get_class($node), $parts, true); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Adapters/Services/AnalyzerServiceAdapter.php: -------------------------------------------------------------------------------- 1 | classDependenciesParser->parse($file->fullPath()); 23 | 24 | return new ClassDependencies( 25 | fqcn: Fqcn::fromString($classAnalysis->fqcn()), 26 | dependencies: Dependencies::fromArray($classAnalysis->dependencies()), 27 | isInterface: IsInterface::fromBool($classAnalysis->isInterface()), 28 | isAbstract: IsAbstract::fromBool($classAnalysis->isAbstract()), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Infrastructure/Analyze/Ports/AnalyzerService.php: -------------------------------------------------------------------------------- 1 | files = $files; 23 | } 24 | 25 | public function getAllDependencies(): DependencyAggregator 26 | { 27 | foreach ($this->files as $file) { 28 | 29 | $classCoupling = $this->analyzerService->getDependencies( 30 | new FileAdapter($file), 31 | ); 32 | 33 | $this->dependencyAggregator->aggregate($classCoupling); 34 | } 35 | 36 | return $this->dependencyAggregator; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Infrastructure/File/Adapters/DataTransferObjects/FileAdapter.php: -------------------------------------------------------------------------------- 1 | file->fullPath; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Infrastructure/File/Adapters/Repositories/FileRepositoryAdapter.php: -------------------------------------------------------------------------------- 1 | getFiles($path); 22 | 23 | $this->fileAggregator->aggregate($files); 24 | 25 | return $this->fileAggregator; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Infrastructure/File/Ports/File.php: -------------------------------------------------------------------------------- 1 | nodes->add($name, $instability); 19 | } 20 | 21 | public function countNodes(): int 22 | { 23 | return $this->nodes->count(); 24 | } 25 | 26 | public function missingNode(string $name): bool 27 | { 28 | return $this->nodes->miss($name); 29 | } 30 | 31 | public function addEdge(string $source, string $target): void 32 | { 33 | $this->edges->add($source, $target); 34 | } 35 | 36 | public function nodes(): array 37 | { 38 | return $this->nodes->toArray(); 39 | } 40 | 41 | public function edges(): array 42 | { 43 | return $this->edges->toArray(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Infrastructure/Graph/Adapters/Cytoscape/CytoscapeNetworkBuilder.php: -------------------------------------------------------------------------------- 1 | mapNodes($metrics); 19 | $this->mapEdges($metrics); 20 | 21 | return $this->network; 22 | } 23 | 24 | private function mapNodes(array $metrics): void 25 | { 26 | foreach ($metrics as $item) { 27 | $this->network->addNode($item->name(), $item->instability()); 28 | } 29 | } 30 | 31 | private function mapEdges(array $metrics): void 32 | { 33 | foreach ($metrics as $item) { 34 | 35 | foreach ($item->dependencies() as $dependency) { 36 | 37 | if ($this->isSelfDependency($dependency, $item)) { 38 | continue; 39 | } 40 | 41 | if ($this->network->missingNode($dependency)) { 42 | $this->network->addNode($dependency); 43 | } 44 | 45 | $this->network->addEdge(source: $item->name(), target: $dependency); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * We remove self dependency from graph for readability reasons 52 | */ 53 | private function isSelfDependency(string $dependency, NetworkAttribute $item): bool 54 | { 55 | return $dependency === $item->name(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Infrastructure/Graph/Adapters/Cytoscape/Edges.php: -------------------------------------------------------------------------------- 1 | edges[] = [ 14 | 'data' => [ 15 | 'source' => $source, 16 | 'target' => $target, 17 | ], 18 | ]; 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return $this->edges; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Infrastructure/Graph/Adapters/Cytoscape/Nodes.php: -------------------------------------------------------------------------------- 1 | nodeNames[] = $name; 15 | 16 | $this->nodes[] = [ 17 | 'data' => [ 18 | 'id' => $name, 19 | 'instability' => $instability, 20 | ], 21 | ]; 22 | } 23 | 24 | public function miss(string $name): bool 25 | { 26 | return ! in_array($name, $this->nodeNames, true); 27 | } 28 | 29 | public function count(): int 30 | { 31 | return count($this->nodes); 32 | } 33 | 34 | public function toArray(): array 35 | { 36 | return $this->nodes; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Infrastructure/Views/Adapters/SystemFileLauncherAdapter.php: -------------------------------------------------------------------------------- 1 | getExecCommand(); 14 | 15 | exec("$command $this->file"); 16 | } 17 | 18 | public function save(string $html): void 19 | { 20 | file_put_contents('graph.html', $html); 21 | } 22 | 23 | private function getExecCommand(): string 24 | { 25 | return match (PHP_OS_FAMILY) { 26 | 'Windows' => 'start', 27 | 'Darwin' => 'open', 28 | default => 'xdg-open', 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Graph/GraphEnums.php: -------------------------------------------------------------------------------- 1 | $metrics 21 | */ 22 | public function from(array $metrics): Network 23 | { 24 | $metrics = $this->metricMapper->from($metrics); 25 | 26 | $networkAttributes = $this->networkAttributesMapper->map($metrics); 27 | 28 | $network = $this->networkBuilder->build($networkAttributes); 29 | 30 | return $network; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Graph/GraphPresenter.php: -------------------------------------------------------------------------------- 1 | settings->debug) { 36 | alert($e); 37 | } 38 | 39 | alert($e->getMessage()); 40 | } 41 | 42 | public function present(AnalyzeResponse $response): void 43 | { 44 | $metrics = $response->metrics; 45 | 46 | $metrics = $this->transformer->apply($metrics); 47 | 48 | $network = $this->mapper->from($metrics); 49 | 50 | $this->view->show(new GraphViewModel($network)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Graph/GraphSettings.php: -------------------------------------------------------------------------------- 1 | render($viewModel); 21 | 22 | $this->save($html); 23 | 24 | $this->open(); 25 | 26 | $this->showInfo($viewModel); 27 | } 28 | 29 | private function render(GraphViewModel $viewModel): string 30 | { 31 | $view = $this->view->make('class-graph', [ 32 | 'nodes' => $viewModel->nodes(), 33 | 'edges' => $viewModel->edges(), 34 | ]); 35 | 36 | return $view->render(); 37 | } 38 | 39 | private function open(): void 40 | { 41 | $this->systemFileLauncher->open(); 42 | } 43 | 44 | private function save(string $html): void 45 | { 46 | $this->systemFileLauncher->save($html); 47 | } 48 | 49 | private function showInfo(GraphViewModel $viewModel): void 50 | { 51 | info('Graph successfully generated in graph.html'); 52 | 53 | if ($viewModel->hasManyNodes()) { 54 | outro('Graph is quickly bloated with many dependencies, do not hesitate to use the --only= and --exclude= options for better readability'); 55 | outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Graph/GraphViewModel.php: -------------------------------------------------------------------------------- 1 | network->countNodes() > GraphEnums::READABILITY_THRESHOLD->value; 17 | } 18 | 19 | public function nodes(): array 20 | { 21 | return $this->network->nodes(); 22 | } 23 | 24 | public function edges(): array 25 | { 26 | return $this->network->edges(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Shared/Metric.php: -------------------------------------------------------------------------------- 1 | name; 18 | } 19 | 20 | public function instability(): float 21 | { 22 | return $this->instability; 23 | } 24 | 25 | public function dependencies(): array 26 | { 27 | return $this->dependencies; 28 | } 29 | } -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Shared/MetricMapper.php: -------------------------------------------------------------------------------- 1 | makeClass($metric); 13 | }, $metrics); 14 | } 15 | 16 | private function makeClass(AnalyzeMetric $metric): Metric 17 | { 18 | return new Metric( 19 | name: $metric->name(), 20 | instability: $metric->instability(), 21 | dependencies: $metric->dependencies(), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Summary/SummaryMapper.php: -------------------------------------------------------------------------------- 1 | $metric->name(), 24 | 'Ec' => $metric->efferentCoupling(), 25 | 'Ac' => $metric->afferentCoupling(), 26 | 'I' => $metric->instability(), 27 | 'Na' => $metric->numberOfAbstractDependencies(), 28 | 'A' => $metric->abstractnessRatio(), 29 | ]; 30 | }, $metrics); 31 | } 32 | 33 | private static function formatHumanReadableMetrics(array $metrics): array 34 | { 35 | return array_map(function ($metric) { 36 | return [ 37 | 'name' => $metric->name(), 38 | 'stability' => StabilityCalculator::calculate($metric), 39 | 'abstractness' => AbstractnessCalculator::calculate($metric), 40 | 'maintainability' => MaintainabilityCalculator::calculate($metric), 41 | ]; 42 | }, $metrics); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Summary/SummaryPresenter.php: -------------------------------------------------------------------------------- 1 | settings->debug) { 35 | alert($e); 36 | } 37 | 38 | alert($e->getMessage()); 39 | } 40 | 41 | public function present(AnalyzeResponse $response): void 42 | { 43 | $metrics = $response->metrics; 44 | 45 | $metrics = $this->transformer->apply($metrics); 46 | 47 | $metrics = $this->mapper->from($metrics, $this->settings->humanReadable); 48 | 49 | $viewModel = new SummaryViewModel($metrics, $response->count, $this->settings); 50 | 51 | $this->view->show($viewModel); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Summary/SummarySettings.php: -------------------------------------------------------------------------------- 1 | displayMetrics($viewModel); 15 | $this->displayInfo($viewModel); 16 | } 17 | 18 | private function displayMetrics(SummaryViewModel $viewModel): void 19 | { 20 | $viewModel->hasMetrics() 21 | ? $this->showMetrics($viewModel) 22 | : warning('No classes found'); 23 | } 24 | 25 | private function showMetrics(SummaryViewModel $viewModel): void 26 | { 27 | table( 28 | headers: $viewModel->headers(), 29 | rows: $viewModel->metrics(), 30 | ); 31 | } 32 | 33 | private function displayInfo(SummaryViewModel $viewModel): void 34 | { 35 | $viewModel->needInfo() 36 | ? $this->showInfo($viewModel) 37 | : $this->showOutro($viewModel); 38 | } 39 | 40 | private function showInfo(SummaryViewModel $viewModel): void 41 | { 42 | $viewModel->isHumanReadable() 43 | ? $this->showHumanReadableInfo() 44 | : $this->showMetricsInfo(); 45 | } 46 | 47 | private function showHumanReadableInfo(): void 48 | { 49 | outro('A stable and concrete class is heavily used by the application and has few abstractions.'); 50 | outro('It is probably not open to extension and it will be necessary to modify it to add behaviors.'); 51 | outro('This class has a risky maintainability because a modification will impact many classes.'); 52 | 53 | outro('An unstable and concrete class uses many concrete dependencies, without going through abstractions, making it harder to test.'); 54 | outro('This class has a suffering maintainability because it will suffer from side effects of its dependencies.'); 55 | 56 | outro('For more information, see the documentation: https://php-quality-tools.com/class-dependencies-analyzer'); 57 | } 58 | 59 | private function showMetricsInfo(): void 60 | { 61 | table( 62 | headers: ['Metric', 'Description'], 63 | rows: [ 64 | ['Ac (Afferent Coupling)', 'number of classes that depend on the class.'], 65 | ['Ec (Efferent Coupling)', 'number of classes that the class depends on.'], 66 | ['I (Instability)', 'instability of the class.'], 67 | ['Na (Number of abstractions)', 'number of abstractions (interface, abstract class) contained in the class.'], 68 | ['A (Abstractness)', 'ratio of abstractions to the total number of methods in the class.'], 69 | ], 70 | ); 71 | 72 | outro('Class with a low instability (I close to 0) is strongly used and probably critical for the application, its business logic must be tested.'); 73 | outro('Class with a high instability (I close to 1) can suffer from side effects of its dependencies and must favor abstractions.'); 74 | outro('Class with a high abstractness (A close to 1) is totally abstract and should not have any concrete code.'); 75 | outro('Try --human-readable to get a more human readable output.'); 76 | outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); 77 | } 78 | 79 | private function showOutro(SummaryViewModel $viewModel): void 80 | { 81 | outro('Add --info to get more information on metrics.'); 82 | outro(sprintf('Found %d classes in the given path', $viewModel->count())); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Class/Summary/SummaryViewModel.php: -------------------------------------------------------------------------------- 1 | metrics); 18 | 19 | return array_keys($metrics[0]); 20 | } 21 | 22 | public function isHumanReadable(): bool 23 | { 24 | return $this->settings->humanReadable; 25 | } 26 | 27 | public function needInfo(): bool 28 | { 29 | return $this->settings->info; 30 | } 31 | 32 | public function metrics(): array 33 | { 34 | return $this->metrics; 35 | } 36 | 37 | public function count(): int 38 | { 39 | return $this->count; 40 | } 41 | 42 | public function hasMetrics(): bool 43 | { 44 | return count($this->metrics) > 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Graph/GraphMapper.php: -------------------------------------------------------------------------------- 1 | componentMapper->from($metrics); 21 | 22 | $networkAttributes = $this->networkAttributesMapper->map($components); 23 | 24 | return $this->networkBuilder->build($networkAttributes); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Graph/GraphPresenter.php: -------------------------------------------------------------------------------- 1 | settings->debug) { 37 | alert($e); 38 | } 39 | 40 | alert($e->getMessage()); 41 | } 42 | 43 | public function present(AnalyzeResponse $response): void 44 | { 45 | $metrics = $response->metrics; 46 | 47 | $metrics = $this->transformer->apply($metrics); 48 | 49 | $network = $this->mapper->from($metrics); 50 | 51 | $this->view->show(new GraphViewModel($network)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Graph/GraphSettings.php: -------------------------------------------------------------------------------- 1 | render($viewModel); 20 | 21 | $this->save($html); 22 | 23 | $this->open(); 24 | 25 | $this->showInfo($viewModel); 26 | } 27 | 28 | private function render(GraphViewModel $viewModel): string 29 | { 30 | $view = $this->view->make('components-graph', [ 31 | 'nodes' => $viewModel->nodes(), 32 | 'edges' => $viewModel->edges(), 33 | ]); 34 | 35 | return $view->render(); 36 | } 37 | 38 | private function open(): void 39 | { 40 | $this->systemFileLauncher->open(); 41 | } 42 | 43 | private function save(string $html): void 44 | { 45 | $this->systemFileLauncher->save($html); 46 | } 47 | 48 | private function showInfo(GraphViewModel $viewModel): void 49 | { 50 | info('Graph successfully generated in graph.html'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Graph/GraphViewModel.php: -------------------------------------------------------------------------------- 1 | network->nodes(); 16 | } 17 | 18 | public function edges(): array 19 | { 20 | return $this->network->edges(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Shared/Collector.php: -------------------------------------------------------------------------------- 1 | name = $name; 18 | } 19 | 20 | public function collect(AnalyzeMetric $metric): void 21 | { 22 | $this->totalInstability += $metric->instability(); 23 | 24 | if ($metric->abstract()) { 25 | $this->countAbstractions++; 26 | } 27 | 28 | $this->countClasses++; 29 | } 30 | 31 | public function addDependency(string $dependency): void 32 | { 33 | $this->dependencies[] = $dependency; 34 | } 35 | 36 | public function hasDependency(string $dependency): bool 37 | { 38 | return in_array($dependency, $this->dependencies, true); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Shared/Component.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | 22 | public function countClasses(): int 23 | { 24 | return $this->countClasses; 25 | } 26 | 27 | public function countAbstractions(): int 28 | { 29 | return $this->countAbstractions; 30 | } 31 | 32 | public function abstractness(): float 33 | { 34 | return number_format($this->countAbstractions / $this->countClasses, 2); 35 | } 36 | 37 | public function instability(): float 38 | { 39 | return number_format($this->totalInstability / $this->countClasses, 2); 40 | } 41 | 42 | public function dependencies(): array 43 | { 44 | return $this->dependencies; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Shared/ComponentFactory.php: -------------------------------------------------------------------------------- 1 | name, 11 | countClasses: $collector->countClasses, 12 | countAbstractions: $collector->countAbstractions, 13 | totalInstability: $collector->totalInstability, 14 | dependencies: $collector->dependencies, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Shared/ComponentMapper.php: -------------------------------------------------------------------------------- 1 | > $metrics 15 | */ 16 | public function from(array $metrics): array 17 | { 18 | $components = array_keys($metrics); 19 | 20 | $items = []; 21 | 22 | foreach ($metrics as $component => $componentMetrics) { 23 | 24 | $collector = new Collector(); 25 | 26 | $collector->setName($component); 27 | 28 | foreach ($componentMetrics as $metric) { 29 | 30 | $collector->collect($metric); 31 | 32 | foreach ($metric->dependencies() as $dependency) { 33 | 34 | foreach ($components as $otherComponent) { 35 | 36 | if (Str::startsWith($dependency, $otherComponent)) { 37 | 38 | if ($otherComponent === $component) { 39 | continue; 40 | } 41 | 42 | if (! $collector->hasDependency($otherComponent)) { 43 | $collector->addDependency($otherComponent); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | $items[] = $this->componentFactory->make($collector); 51 | } 52 | 53 | return $items; 54 | } 55 | } -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Summary/SummaryMapper.php: -------------------------------------------------------------------------------- 1 | componentMapper->from($metrics); 16 | 17 | return $this->format($components); 18 | } 19 | 20 | private function format(array $components): array 21 | { 22 | return array_map(function ($component) { 23 | return [ 24 | 'name' => $component->name(), 25 | 'Nc' => $component->countClasses(), 26 | 'Na' => $component->countAbstractions(), 27 | 'A' => $component->abstractness(), 28 | 'I' => $component->instability(), 29 | ]; 30 | }, $components); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Summary/SummaryPresenter.php: -------------------------------------------------------------------------------- 1 | settings->debug) { 36 | alert($e); 37 | } 38 | 39 | alert($e->getMessage()); 40 | } 41 | 42 | public function present(AnalyzeResponse $response): void 43 | { 44 | $metrics = $response->metrics; 45 | 46 | $metrics = $this->transformer->apply($metrics); 47 | 48 | $metrics = $this->mapper->from($metrics); 49 | 50 | $this->view->show(new SummaryViewModel($metrics)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Summary/SummarySettings.php: -------------------------------------------------------------------------------- 1 | displayMetrics($viewModel); 15 | $this->displayInfo($viewModel); 16 | } 17 | 18 | private function displayMetrics(SummaryViewModel $viewModel): void 19 | { 20 | $viewModel->hasComponents() 21 | ? $this->showComponents($viewModel) 22 | : warning('No classes found'); 23 | } 24 | 25 | private function showComponents(SummaryViewModel $viewModel): void 26 | { 27 | table( 28 | headers: $viewModel->headers(), 29 | rows: $viewModel->components(), 30 | ); 31 | } 32 | 33 | private function displayInfo(SummaryViewModel $viewModel): void 34 | { 35 | $viewModel->needInfo() 36 | ? $this->showInfo($viewModel) 37 | : $this->showOutro($viewModel); 38 | } 39 | 40 | private function showInfo(SummaryViewModel $viewModel): void 41 | { 42 | $viewModel->isHumanReadable() 43 | ? $this->showHumanReadableInfo() 44 | : $this->showMetricsInfo(); 45 | } 46 | 47 | private function showHumanReadableInfo(): void 48 | { 49 | outro('For more information, see the documentation: https://php-quality-tools.com/class-dependencies-analyzer'); 50 | } 51 | 52 | private function showMetricsInfo(): void 53 | { 54 | outro('Try --human-readable to get a more human readable output.'); 55 | outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); 56 | } 57 | 58 | private function showOutro(SummaryViewModel $viewModel): void 59 | { 60 | outro('Add --info to get more information on metrics.'); 61 | outro(sprintf('Found %d classes in the given path', $viewModel->count())); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Component/Summary/SummaryViewModel.php: -------------------------------------------------------------------------------- 1 | components; 14 | } 15 | 16 | public function headers(): array 17 | { 18 | return array_keys(array_values($this->components)[0]); 19 | } 20 | 21 | public function count(): int 22 | { 23 | return count($this->components); 24 | } 25 | 26 | public function hasComponents(): bool 27 | { 28 | return $this->count() > 0; 29 | } 30 | 31 | public function needInfo(): bool 32 | { 33 | return false; 34 | } 35 | 36 | public function isHumanReadable(): bool 37 | { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Calculators/AbstractnessCalculator.php: -------------------------------------------------------------------------------- 1 | abstractnessRatio(); 13 | 14 | if ($ratio > 0.7) { 15 | return 'abstract'; 16 | } 17 | 18 | if ($ratio < 0.3) { 19 | return 'concrete'; 20 | } 21 | 22 | return 'balanced'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Calculators/Calculator.php: -------------------------------------------------------------------------------- 1 | abstractnessRatio() < 0.3 && $metric->instability() > 0.7; 32 | } 33 | 34 | private static function isLowlyAbstractAndHighlyStable(AnalyzeMetric $metric): bool 35 | { 36 | return $metric->abstractnessRatio() < 0.3 && $metric->instability() < 0.3; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Calculators/StabilityCalculator.php: -------------------------------------------------------------------------------- 1 | instability(); 13 | 14 | if ($instability > 0.7) { 15 | return 'unstable'; 16 | } 17 | 18 | if ($instability < 0.3) { 19 | return 'stable'; 20 | } 21 | 22 | return 'flexible'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Collectors/Components.php: -------------------------------------------------------------------------------- 1 | components = $components; 14 | } 15 | 16 | public function get(): array 17 | { 18 | return $this->components; 19 | } 20 | 21 | public function add(string $component, array $metric): void 22 | { 23 | $this->components[$component][] = $metric; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Collectors/Depth.php: -------------------------------------------------------------------------------- 1 | depth[$class->name()] = $class; 14 | } 15 | 16 | public function has(string $name): bool 17 | { 18 | return isset($this->depth[$name]); 19 | } 20 | 21 | public function toArray(): array 22 | { 23 | return $this->depth; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Collectors/Metrics.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 14 | } 15 | 16 | public function unknown(string $name): bool 17 | { 18 | return ! isset($this->metrics[$name]); 19 | } 20 | 21 | public function get(string $name): AnalyzeMetric 22 | { 23 | return $this->metrics[$name]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Contracts/Transformer.php: -------------------------------------------------------------------------------- 1 | $metrics 14 | * @return array 15 | */ 16 | public function apply(array $metrics): array; 17 | } 18 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Transformers/ComponentTransformer.php: -------------------------------------------------------------------------------- 1 | targetedComponents as $component) { 21 | 22 | $name = $metric->name(); 23 | 24 | if ($this->isTargetedComponent($name, $component)) { 25 | 26 | $components[$component][] = $metric; 27 | 28 | break; 29 | } 30 | } 31 | } 32 | 33 | return $components; 34 | } 35 | 36 | private function isTargetedComponent(string $name, string $component): bool 37 | { 38 | return Str::startsWith($name, $component); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Filters/Transformers/NullTransformer.php: -------------------------------------------------------------------------------- 1 | metrics->set($metrics); 27 | 28 | $this->checkTargetPresence(); 29 | 30 | $targetClass = $this->metrics->get($this->target); 31 | 32 | $this->depth->add($targetClass); 33 | 34 | foreach ($targetClass->dependencies() as $dependency) { 35 | $this->deepDive($dependency); 36 | } 37 | 38 | return $this->depth->toArray(); 39 | } 40 | 41 | private function checkTargetPresence(): void 42 | { 43 | if ($this->metrics->unknown($this->target)) { 44 | throw new Exception('Target ' . $this->target . ' not found on metrics, try verify the target name.'); 45 | } 46 | } 47 | 48 | private function deepDive(string $dependency): void 49 | { 50 | if ($this->shouldStop($dependency)) { 51 | return; 52 | } 53 | 54 | $targetClass = $this->metrics->get($dependency); 55 | 56 | $this->depth->add($targetClass); 57 | 58 | $this->incrementDeep(); 59 | 60 | foreach ($targetClass->dependencies() as $innerDependency) { 61 | $this->deepDive($innerDependency); 62 | } 63 | 64 | $this->decrementDeep(); 65 | } 66 | 67 | private function shouldStop(string $dependency): bool 68 | { 69 | return $this->dependencyIsAlreadyAnalyzed($dependency) 70 | || $this->dependencyIsUnknown($dependency) 71 | || $this->dependencyIsTooDeep(); 72 | } 73 | 74 | private function dependencyIsAlreadyAnalyzed(string $dependency): bool 75 | { 76 | return $this->depth->has($dependency); 77 | } 78 | 79 | private function dependencyIsUnknown(string $dependency): bool 80 | { 81 | return $this->metrics->unknown($dependency); 82 | } 83 | 84 | private function dependencyIsTooDeep(): bool 85 | { 86 | return $this->depthLimit !== null && $this->deep >= ($this->depthLimit - 1) ; 87 | } 88 | 89 | private function incrementDeep(): void 90 | { 91 | $this->deep++; 92 | } 93 | 94 | private function decrementDeep(): void 95 | { 96 | $this->deep--; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Network/Network.php: -------------------------------------------------------------------------------- 1 | name; 16 | } 17 | 18 | public function instability(): float 19 | { 20 | return $this->instability; 21 | } 22 | 23 | public function dependencies(): array 24 | { 25 | return $this->dependencies; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Network/NetworkAttributesMapper.php: -------------------------------------------------------------------------------- 1 | makeNetworkAttribute($item); 16 | } 17 | 18 | return $attributes; 19 | } 20 | 21 | public function makeNetworkAttribute(Networkable $item): NetworkAttribute 22 | { 23 | return new NetworkAttribute( 24 | $item->name(), 25 | $item->instability(), 26 | $item->dependencies(), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Network/NetworkBuilder.php: -------------------------------------------------------------------------------- 1 | $attributes 12 | */ 13 | public function build(array $attributes): Network; 14 | } 15 | -------------------------------------------------------------------------------- /app/Presenter/Analyze/Shared/Network/Networkable.php: -------------------------------------------------------------------------------- 1 | $a[$key]; 11 | }); 12 | 13 | return $items; 14 | } 15 | 16 | public static function cut(?int $limit, array $items): array 17 | { 18 | if ($limit === null) { 19 | return $items; 20 | } 21 | 22 | return array_slice($items, 0, $limit); 23 | } 24 | 25 | public static function filterByMinValue(string $key, ?float $minValue, array $items): array 26 | { 27 | if ($minValue === null) { 28 | return $items; 29 | } 30 | 31 | $items = array_filter($items, function ($item) use ($key, $minValue) { 32 | return $item[$key] >= $minValue; 33 | }); 34 | 35 | return array_values($items); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Presenter/Cyclic/Summary/CycleHelper.php: -------------------------------------------------------------------------------- 1 | ', $names); 28 | } 29 | 30 | private static function completeCircularPath(string $path, array $names): string 31 | { 32 | return $path . ' -> ' . NameFormatter::humanReadable($names[0]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Presenter/Cyclic/Summary/CyclicPresenterMapper.php: -------------------------------------------------------------------------------- 1 | settings->debug) { 33 | alert($e); 34 | } 35 | 36 | alert($e->getMessage()); 37 | } 38 | 39 | public function present(CyclicResponse $response): void 40 | { 41 | $metrics = $this->mapper->from($response->cycles); 42 | 43 | $viewModel = new SummaryViewModel($metrics, $response->count); 44 | 45 | $this->view->show($viewModel); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Presenter/Cyclic/Summary/SummarySettings.php: -------------------------------------------------------------------------------- 1 | metrics) === 0 15 | ? warning('Good work! No cycles found') 16 | : $this->showTable($viewModel); 17 | } 18 | 19 | private function showTable(SummaryViewModel $viewModel): void 20 | { 21 | table( 22 | ['Cyclic Path'], 23 | $viewModel->metrics, 24 | ); 25 | 26 | outro('A cycle is a class that depends on itself through its dependencies.'); 27 | outro('It can be a sign of a bad design and reveal an ensemble of components difficult to maintain and evolve.'); 28 | 29 | outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); 30 | 31 | outro('Cycles found: ' . $viewModel->totalClasses); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Presenter/Cyclic/Summary/SummaryViewModel.php: -------------------------------------------------------------------------------- 1 | explode('\\')->last(); 13 | } 14 | 15 | public static function humanReadable(string $className): string 16 | { 17 | $exploded = Str::of($className)->explode('\\'); 18 | 19 | if (self::isShortName($exploded)) { 20 | return $className; 21 | } 22 | 23 | $exploded = self::keepLastTwoNamespaces($exploded); 24 | 25 | return $exploded->implode('\\'); 26 | } 27 | 28 | private static function isShortName(Collection $exploded): bool 29 | { 30 | return $exploded->count() <= 2; 31 | } 32 | 33 | private static function keepLastTwoNamespaces(Collection $exploded): Collection 34 | { 35 | return $exploded->reverse()->take(2)->reverse(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Presenter/Weakness/Summary/SummaryPresenter.php: -------------------------------------------------------------------------------- 1 | settings->debug()) { 33 | alert($exception); 34 | } 35 | 36 | alert($exception->getMessage()); 37 | } 38 | 39 | public function present(WeaknessResponse $response): void 40 | { 41 | $metrics = $this->applyFiltersOnMetrics($response); 42 | 43 | $metrics = WeaknessPresenterMapper::from($metrics); 44 | 45 | $viewModel = new SummaryViewModel($metrics, $response->count, $this->settings->minDelta()); 46 | 47 | $this->view->show($viewModel); 48 | } 49 | 50 | private function applyFiltersOnMetrics(WeaknessResponse $response): array 51 | { 52 | $metrics = $response->metrics; 53 | 54 | $metrics = ArrayFormatter::sort('delta', $metrics); 55 | 56 | $metrics = ArrayFormatter::cut($this->settings->limit(), $metrics); 57 | 58 | $metrics = ArrayFormatter::filterByMinValue('delta', $this->settings->minDelta(), $metrics); 59 | 60 | return $metrics; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Presenter/Weakness/Summary/SummarySettings.php: -------------------------------------------------------------------------------- 1 | limit; 16 | } 17 | 18 | public function minDelta(): float 19 | { 20 | return $this->minDelta ?? 0.0; 21 | } 22 | 23 | public function debug(): bool 24 | { 25 | return $this->debug; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Presenter/Weakness/Summary/SummaryView.php: -------------------------------------------------------------------------------- 1 | metrics) === 0 15 | ? warning('Good work! No weaknesses found') 16 | : $this->showTable($viewModel); 17 | } 18 | 19 | protected function showTable(SummaryViewModel $viewModel): void 20 | { 21 | table( 22 | headers: ['Class', 'Instability', 'Dependency', 'Instability', 'Delta'], 23 | rows: $viewModel->metrics, 24 | ); 25 | 26 | $this->showHowManyWeaknessesFound($viewModel); 27 | } 28 | 29 | protected function showHowManyWeaknessesFound(SummaryViewModel $viewModel): void 30 | { 31 | if ($viewModel->delta) { 32 | outro(sprintf('Showing weaknesses with delta greater than %s', $viewModel->delta)); 33 | } 34 | 35 | outro('A weakness is a class that depends on a class that is more unstable than itself. More the delta is high, more the dependency is unstable.'); 36 | outro('It can be a sign of a bad design and an indicator of a class that can suffer from side effects of its dependencies.'); 37 | 38 | outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); 39 | 40 | outro(sprintf('Found %d weaknesses in %d classes', count($viewModel->metrics), $viewModel->totalClasses)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Presenter/Weakness/Summary/SummaryViewModel.php: -------------------------------------------------------------------------------- 1 | NameFormatter::humanReadable($metric['class']), 14 | 'instability' => $metric['class_instability'], 15 | 'dependency' => NameFormatter::humanReadable($metric['dependency']), 16 | 'dependency_instability' => $metric['dependency_instability'], 17 | 'delta' => $metric['delta'], 18 | ]; 19 | }, $metrics); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(FileRepository::class, FileRepositoryAdapter::class); 39 | 40 | $this->app->bind(FileAggregator::class, FileAggregatorAdapter::class); 41 | 42 | $this->app->bind(AnalyzerService::class, AnalyzerServiceAdapter::class); 43 | 44 | $this->app->bind(NetworkBuilder::class, CytoscapeNetworkBuilder::class); 45 | 46 | $this->app->bind(SystemFileLauncher::class, SystemFileLauncherAdapter::class); 47 | 48 | $this->app->bind(ClassDependenciesParser::class, function () { 49 | return new ClassDependenciesParserAdapter( 50 | (new ParserFactory())->create(ParserFactory::PREFER_PHP7), 51 | new NodeTraverserFactory(), 52 | ); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Providers/Custom/CustomNodeTraverserFactory.php: -------------------------------------------------------------------------------- 1 | addVisitor(new ParentConnectingVisitor()); 21 | $traverser->addVisitor(new ParseClassFqnNodeVisitor($classDependencies)); 22 | $traverser->addVisitor(new ParseImportedFqnNodeVisitor($classDependencies)); 23 | 24 | return $traverser; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /arts/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeGraciaMathieu/php-class-dependencies-analyzer/aad21eaa299adcecea8d9013e56d9ccb2e9f9594/arts/graph.png -------------------------------------------------------------------------------- /arts/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeGraciaMathieu/php-class-dependencies-analyzer/aad21eaa299adcecea8d9013e56d9ccb2e9f9594/arts/home.png -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | create(); 6 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "degraciamathieu/php-class-dependencies-analyzer", 3 | "description": "This tool allows you to monitor the dependencies and instability of your classes.", 4 | "keywords": ["php", "quality", "analysis", "static-analysis", "coupling", "instability"], 5 | "homepage": "https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer", 6 | "type": "project", 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer/issues", 10 | "source": "https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Mathieu De Gracia", 15 | "email": "work@degracia-mathieu.fr" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2.0", 20 | "degraciamathieu/php-file-explorer": "^0.4.1", 21 | "illuminate/view": "^11.5", 22 | "jerowork/class-dependencies-parser": "^0.5.1", 23 | "laravel-zero/framework": "^11.0.0", 24 | "laravel/prompts": "^0.1.25", 25 | "nikic/php-parser": "4.19.*" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.15.2", 29 | "mockery/mockery": "^1.6.11", 30 | "pestphp/pest": "^2.34.7" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "App\\": "app/", 35 | "Database\\Factories\\": "database/factories/", 36 | "Database\\Seeders\\": "database/seeders/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\": "tests/" 42 | } 43 | }, 44 | "config": { 45 | "preferred-install": "dist", 46 | "sort-packages": true, 47 | "optimize-autoloader": true, 48 | "allow-plugins": { 49 | "pestphp/pest-plugin": true 50 | } 51 | }, 52 | "scripts": { 53 | "test": "vendor/bin/pest -p", 54 | "healthcheck": [ 55 | "php class-dependencies-analyzer weakness app", 56 | "php class-dependencies-analyzer weakness app --min-delta=0.1", 57 | "php class-dependencies-analyzer weakness app --limit=10", 58 | "php class-dependencies-analyzer weakness app --only='App\\Application'", 59 | "php class-dependencies-analyzer weakness app --exclude='App\\Infrastructure'", 60 | "php class-dependencies-analyzer cyclic app", 61 | "php class-dependencies-analyzer cyclic app --only='App\\Application'", 62 | "php class-dependencies-analyzer cyclic app --exclude='App\\Infrastructure'", 63 | "php class-dependencies-analyzer analyze:class app", 64 | "php class-dependencies-analyzer analyze:class app --only='App\\Application'", 65 | "php class-dependencies-analyzer analyze:class app --exclude='App\\Infrastructure'", 66 | "php class-dependencies-analyzer analyze:class app --target='App\\Application\\Analyze\\AnalyzeAction'", 67 | "php class-dependencies-analyzer analyze:class app --target='App\\Application\\Analyze\\AnalyzeAction' --depth-limit=2", 68 | "php class-dependencies-analyzer analyze:class app --graph", 69 | "php class-dependencies-analyzer analyze:component app 'App\\Application'", 70 | "php class-dependencies-analyzer analyze:component app 'App\\Application' --graph", 71 | "vendor/bin/pest -p" 72 | ], 73 | "coverage": "php -d xdebug.mode=coverage vendor/bin/pest --coverage -p" 74 | }, 75 | "minimum-stability": "stable", 76 | "prefer-stable": true, 77 | "bin": ["class-dependencies-analyzer"] 78 | } 79 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 'Class-dependencies-analyzer', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Version 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "version" your application is currently running 24 | | in. You may want to follow the "Semantic Versioning" - Given a version 25 | | number MAJOR.MINOR.PATCH when an update happens: https://semver.org. 26 | | 27 | */ 28 | 29 | 'version' => app('git.version'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Environment 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This value determines the "environment" your application is currently 37 | | running in. This may determine how you prefer to configure various 38 | | services the application utilizes. This can be overridden using 39 | | the global command line "--env" option when calling commands. 40 | | 41 | */ 42 | 43 | 'env' => 'development', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Autoloaded Service Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | The service providers listed here will be automatically loaded on the 51 | | request to your application. Feel free to add your own services to 52 | | this array to grant expanded functionality to your applications. 53 | | 54 | */ 55 | 56 | 'providers' => [ 57 | App\Providers\AppServiceProvider::class, 58 | ], 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /config/commands.php: -------------------------------------------------------------------------------- 1 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Commands Paths 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "paths" that should be loaded by the console's 24 | | kernel. Foreach "path" present on the array provided below the kernel 25 | | will extract all "Illuminate\Console\Command" based class commands. 26 | | 27 | */ 28 | 29 | 'paths' => [app_path('Commands')], 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Added Commands 34 | |-------------------------------------------------------------------------- 35 | | 36 | | You may want to include a single command class without having to load an 37 | | entire folder. Here you can specify which commands should be added to 38 | | your list of commands. The console's kernel will try to load them. 39 | | 40 | */ 41 | 42 | 'add' => [ 43 | // 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Hidden Commands 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Your application commands will always be visible on the application list 52 | | of commands. But you can still make them "hidden" specifying an array 53 | | of commands below. All "hidden" commands can still be run/executed. 54 | | 55 | */ 56 | 57 | 'hidden' => [ 58 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 59 | Symfony\Component\Console\Command\DumpCompletionCommand::class, 60 | Symfony\Component\Console\Command\HelpCommand::class, 61 | Illuminate\Console\Scheduling\ScheduleRunCommand::class, 62 | Illuminate\Console\Scheduling\ScheduleListCommand::class, 63 | Illuminate\Console\Scheduling\ScheduleFinishCommand::class, 64 | Illuminate\Foundation\Console\VendorPublishCommand::class, 65 | LaravelZero\Framework\Commands\StubPublishCommand::class, 66 | ], 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Removed Commands 71 | |-------------------------------------------------------------------------- 72 | | 73 | | Do you have a service provider that loads a list of commands that 74 | | you don't need? No problem. Laravel Zero allows you to specify 75 | | below a list of commands that you don't to see in your app. 76 | | 77 | */ 78 | 79 | 'remove' => [ 80 | // 81 | ], 82 | 83 | ]; 84 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 5 | resource_path('views'), 6 | ], 7 | 'compiled' => \Phar::running() 8 | ? getcwd() 9 | : env('VIEW_COMPILED_PATH', realpath(storage_path('framework/views'))), 10 | ]; 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Feature 10 | 11 | 12 | ./tests/Unit 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/views/class-graph.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dependency Graph 7 | 8 | 55 | 56 | 57 | 58 |
59 |
60 |
61 |
High Instability (> 0.8) 62 |
63 |
64 |
Medium Instability (> 0.4) 65 |
66 |
67 |
Low Instability (≤ 0.4) 68 |
69 |
70 |
Unstable Dependency (red arrow) 71 |
72 |
73 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /resources/views/components-graph.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Module Dependency Graph 7 | 8 | 9 | 10 | 57 | 58 | 59 | 60 |
61 |
62 |
63 |
High Instability (> 0.8) 64 |
65 |
66 |
Medium Instability (> 0.4) 67 |
68 |
69 |
Low Instability (≤ 0.4) 70 |
71 |
72 |
Unstable Dependency (red arrow) 73 |
74 |
75 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/Builders/AnalyzeMetricBuilder.php: -------------------------------------------------------------------------------- 1 | name = $value; 21 | 22 | return $this; 23 | } 24 | 25 | public function isAbstract(): self 26 | { 27 | $this->abstract = true; 28 | 29 | return $this; 30 | } 31 | 32 | public function withDependencies(array $value): self 33 | { 34 | $this->dependencies = $value; 35 | 36 | return $this; 37 | } 38 | 39 | public function withInstability(float $value): self 40 | { 41 | $this->instability = $value; 42 | 43 | return $this; 44 | } 45 | 46 | public function withAbstractnessRatio(float $value): self 47 | { 48 | $this->ratio = $value; 49 | 50 | return $this; 51 | } 52 | 53 | public function build(): AnalyzeMetric 54 | { 55 | return new AnalyzeMetric([ 56 | 'name' => $this->name, 57 | 'dependencies' => $this->dependencies, 58 | 'abstract' => $this->abstract, 59 | 'coupling' => [ 60 | 'efferent' => $this->efferent, 61 | 'afferent' => $this->afferent, 62 | 'instability' => $this->instability, 63 | ], 64 | 'abstractness' => [ 65 | 'numberOfAbstractDependencies' => $this->numberOfAbstractDependencies, 66 | 'ratio' => $this->ratio, 67 | ], 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Builders/ClassDependenciesBuilder.php: -------------------------------------------------------------------------------- 1 | fqcn = $fqcn; 21 | 22 | return $this; 23 | } 24 | 25 | public function withDependencies(array $dependencies): self 26 | { 27 | $this->dependencies = $dependencies; 28 | 29 | return $this; 30 | } 31 | 32 | public function isInterface(): self 33 | { 34 | $this->isInterface = true; 35 | 36 | return $this; 37 | } 38 | 39 | public function isAbstract(): self 40 | { 41 | $this->isAbstract = true; 42 | 43 | return $this; 44 | } 45 | 46 | public function build(): ClassDependencies 47 | { 48 | return new ClassDependencies( 49 | new Fqcn($this->fqcn), 50 | new Dependencies($this->dependencies), 51 | new IsInterface($this->isInterface), 52 | new IsAbstract($this->isAbstract), 53 | ); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /tests/Builders/DependencyAggregatorBuilder.php: -------------------------------------------------------------------------------- 1 | classDependenciesBuilder->build(); 18 | 19 | return $this->addClassDependencies($classDependencies); 20 | 21 | return $this; 22 | } 23 | 24 | public function addClassDependencies(ClassDependencies $classDependencies): self 25 | { 26 | $this->dependencyAggregator->aggregate($classDependencies); 27 | 28 | return $this; 29 | } 30 | 31 | public function withManyClassDependencies(array $classDependencies): self 32 | { 33 | foreach ($classDependencies as $classDependency) { 34 | $this->addClassDependencies($classDependency); 35 | } 36 | 37 | return $this; 38 | } 39 | 40 | public function build(): DependencyAggregator 41 | { 42 | return $this->dependencyAggregator; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Builders/WeaknessResponseBuilder.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Feature/AnalyzeClassTest.php: -------------------------------------------------------------------------------- 1 | artisan('analyze:class app')->assertSuccessful(); 5 | }); 6 | 7 | it('can run the analyze:class command with a custom target', function () { 8 | $this->artisan('analyze:class app --target=App\Application\Analyze\AnalyzeAction')->assertSuccessful(); 9 | }); 10 | 11 | it('can run the analyze:class command with only a specific namespace', function () { 12 | $this->artisan('analyze:class app --only=App\Application')->assertSuccessful(); 13 | }); 14 | 15 | it('can run the analyze:class command with exclude a specific namespace', function () { 16 | $this->artisan('analyze:class app --exclude=App\Application')->assertSuccessful(); 17 | }); 18 | 19 | it('can run the analyze:class command with graph', function () { 20 | $this->artisan('analyze:class app --graph')->assertSuccessful(); 21 | })->skip('need to find a way to bypass graph generation'); 22 | -------------------------------------------------------------------------------- /tests/Feature/CyclicTest.php: -------------------------------------------------------------------------------- 1 | artisan('cyclic app')->assertSuccessful(); 5 | }); 6 | 7 | it('can run the cyclic command with only option', function () { 8 | $this->artisan("cyclic app --only='App\Application'")->assertSuccessful(); 9 | }); 10 | 11 | it('can run the cyclic command with exclude option', function () { 12 | $this->artisan("cyclic app --exclude='App\Application'")->assertSuccessful(); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/Feature/WeaknessTest.php: -------------------------------------------------------------------------------- 1 | artisan('weakness app')->assertSuccessful(); 5 | }); 6 | 7 | it('can run the weakness command with only option', function () { 8 | $this->artisan("weakness app --only='App\Application'")->assertSuccessful(); 9 | }); 10 | 11 | it('can run the weakness command with exclude option', function () { 12 | $this->artisan("weakness app --exclude='App\Application'")->assertSuccessful(); 13 | }); 14 | 15 | it('can run the weakness command with limit option', function () { 16 | $this->artisan("weakness app --limit=10")->assertSuccessful(); 17 | }); 18 | 19 | it('can run the weakness command with min-delta option', function () { 20 | $this->artisan("weakness app --min-delta=10")->assertSuccessful(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 17 | uses(Tests\TestCase::class)->in('Unit'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something(): void 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | oneDependencyAggregator() 6 | ->withManyClassDependencies([ 7 | $this->oneClassDependencies()->withFqcn('A')->withDependencies(['B', 'C'])->build(), 8 | $this->oneClassDependencies()->withFqcn('C')->withDependencies(['D'])->build(), 9 | ]) 10 | ->build(); 11 | 12 | $dependencyAggregator->calculateInstability(); 13 | 14 | $metrics = $dependencyAggregator->toArray(); 15 | 16 | expect($metrics['A'])->toMatchArray([ 17 | 'name' => 'A', 18 | 'coupling' => [ 19 | 'afferent' => 0, 20 | 'efferent' => 2, 21 | 'instability' => 1.0, 22 | ], 23 | ]); 24 | 25 | expect($metrics['C'])->toMatchArray([ 26 | 'name' => 'C', 27 | 'coupling' => [ 28 | 'afferent' => 1, 29 | 'efferent' => 1, 30 | 'instability' => 0.5, 31 | ], 32 | ]); 33 | }); 34 | 35 | test('it keeps only classes by given filters', function () { 36 | 37 | $dependencyAggregator = $this->oneDependencyAggregator() 38 | ->withManyClassDependencies([ 39 | $this->oneClassDependencies()->withFqcn('App\Domain\Aggregators\DependencyAggregator')->build(), 40 | ]) 41 | ->build(); 42 | 43 | $dependencyAggregator->filter(only: ['Domain']); 44 | 45 | $dependencies = $dependencyAggregator->toArray(); 46 | 47 | expect($dependencies)->toHaveLength(1); 48 | expect($dependencies)->toHaveKey('App\Domain\Aggregators\DependencyAggregator'); 49 | }); 50 | 51 | test('it filters classes by given exclude filters', function () { 52 | 53 | $dependencyAggregator = $this->oneDependencyAggregator() 54 | ->withManyClassDependencies([ 55 | $this->oneClassDependencies()->withFqcn('App\Application\Analyze\AnalyzeAction')->build(), 56 | ]) 57 | ->build(); 58 | 59 | $dependencyAggregator->filter(exclude: ['Application']); 60 | 61 | $dependencies = $dependencyAggregator->toArray(); 62 | 63 | expect($dependencies)->toHaveLength(0); 64 | }); 65 | 66 | test('it calculates the abstractness correctly when class has no dependencies', function () { 67 | 68 | $dependencyAggregator = $this->oneDependencyAggregator() 69 | ->withManyClassDependencies([ 70 | $this->oneClassDependencies()->withFqcn('A')->build(), 71 | ]) 72 | ->build(); 73 | 74 | $dependencyAggregator->calculateAbstractness(); 75 | 76 | $metrics = $dependencyAggregator->toArray(); 77 | 78 | expect($metrics['A'])->toMatchArray([ 79 | 'name' => 'A', 80 | 'abstractness' => [ 81 | 'ratio' => 0.0, 82 | 'numberOfAbstractDependencies' => 0, 83 | ], 84 | ]); 85 | }); 86 | 87 | test('it calculates the abstractness correctly when class has abstract dependencies', function () { 88 | 89 | $dependencyAggregator = $this->oneDependencyAggregator() 90 | ->withManyClassDependencies([ 91 | $this->oneClassDependencies()->withFqcn('A')->withDependencies(['B', 'C'])->build(), 92 | $this->oneClassDependencies()->withFqcn('B')->isAbstract()->build(), 93 | $this->oneClassDependencies()->withFqcn('C')->build(), 94 | ]) 95 | ->build(); 96 | 97 | $dependencyAggregator->calculateAbstractness(); 98 | 99 | $metrics = $dependencyAggregator->toArray(); 100 | 101 | expect($metrics['A'])->toMatchArray([ 102 | 'name' => 'A', 103 | 'abstractness' => [ 104 | 'ratio' => 0.5, 105 | 'numberOfAbstractDependencies' => 1, 106 | ], 107 | ]); 108 | }); -------------------------------------------------------------------------------- /tests/Unit/Domain/Entities/ClassDependenciesTest.php: -------------------------------------------------------------------------------- 1 | oneClassDependencies() 6 | ->withDependencies([ 7 | 'A', 8 | 'B', 9 | 'C', 10 | ]) 11 | ->build(); 12 | 13 | $dependencies = $classDependencies->getDependencies(); 14 | 15 | expect($dependencies)->toBe([ 16 | 'A', 17 | 'B', 18 | 'C', 19 | ]); 20 | }); 21 | 22 | test('it returns the correct FQCN', function () { 23 | 24 | $classDependencies = $this->oneClassDependencies() 25 | ->withFqcn('A') 26 | ->build(); 27 | 28 | $fqcn = $classDependencies->getName(); 29 | 30 | expect($fqcn)->toBe('A'); 31 | }); 32 | 33 | test('it correctly checks if a class is not a dependency', function () { 34 | 35 | $classDependencies = $this->oneClassDependencies() 36 | ->withFqcn('A') 37 | ->withDependencies([ 38 | 'B', 39 | ]) 40 | ->build(); 41 | 42 | $c = $this->oneClassDependencies() 43 | ->withFqcn('C') 44 | ->build(); 45 | 46 | expect($classDependencies->isDependentOn($c))->toBeFalse(); 47 | }); 48 | 49 | test('it correctly checks if a class is a dependency', function () { 50 | 51 | $classDependencies = $this->oneClassDependencies() 52 | ->withFqcn('A') 53 | ->withDependencies([ 54 | 'B', 55 | ]) 56 | ->build(); 57 | 58 | $b = $this->oneClassDependencies() 59 | ->withFqcn('B') 60 | ->build(); 61 | 62 | expect($classDependencies->isDependentOn($b))->toBeTrue(); 63 | }); 64 | 65 | test('it calculates the abstractness correctly', function () { 66 | 67 | $classDependencies = $this->oneClassDependencies() 68 | ->withFqcn('A') 69 | ->withDependencies([ 70 | 'B', 71 | 'C', 72 | 'D', 73 | ]) 74 | ->build(); 75 | 76 | $classDependencies->incrementNumberOfAbstractDependencies(); 77 | $classDependencies->incrementNumberOfAbstractDependencies(); 78 | 79 | $classDependencies->calculateAbstractness(); 80 | 81 | $abstractness = $classDependencies->toArray()['abstractness']; 82 | 83 | expect($abstractness)->toBe([ 84 | 'ratio' => 0.67, 85 | 'numberOfAbstractDependencies' => 2, 86 | ]); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/Unit/Domain/Services/CyclicDependencyTest.php: -------------------------------------------------------------------------------- 1 | oneDependencyAggregator() 8 | ->withManyClassDependencies([ 9 | $this->oneClassDependencies()->withFqcn('A')->withDependencies(['B'])->build(), 10 | $this->oneClassDependencies()->withFqcn('B')->withDependencies(['C'])->build(), 11 | $this->oneClassDependencies()->withFqcn('C')->withDependencies(['A'])->build(), 12 | ]) 13 | ->build(); 14 | 15 | $cycles = app(CyclicDependency::class)->detect($dependencyAggregator->classes()); 16 | 17 | $this->assertFalse($cycles->isEmpty()); 18 | 19 | expect($cycles->all())->toBe([ 20 | ['A', 'B', 'C'], 21 | ]); 22 | }); 23 | 24 | test('it detects classes with multiple cycles', function () { 25 | 26 | $dependencyAggregator = $this->oneDependencyAggregator() 27 | ->withManyClassDependencies([ 28 | $this->oneClassDependencies()->withFqcn('A')->withDependencies(['B', 'C'])->build(), 29 | $this->oneClassDependencies()->withFqcn('B')->withDependencies(['A'])->build(), 30 | $this->oneClassDependencies()->withFqcn('C')->withDependencies(['A'])->build(), 31 | ]) 32 | ->build(); 33 | 34 | $cycles = app(CyclicDependency::class)->detect($dependencyAggregator->classes()); 35 | 36 | $this->assertFalse($cycles->isEmpty()); 37 | 38 | expect($cycles->all())->toBe([ 39 | ['A', 'B'], 40 | ['A', 'C'], 41 | ]); 42 | }); -------------------------------------------------------------------------------- /tests/Unit/Domain/ValueObjects/CouplingTest.php: -------------------------------------------------------------------------------- 1 | incrementAfferent(); 10 | $coupling->incrementAfferent(); 11 | $coupling->incrementAfferent(); 12 | 13 | expect($coupling->toArray())->toBe([ 14 | 'afferent' => 3, 15 | 'efferent' => 0, 16 | 'instability' => 0.0, 17 | ]); 18 | }); 19 | 20 | 21 | it('it calculates instability', function (int $afferent, int $efferent, float $instability) { 22 | 23 | $coupling = new Coupling(afferent: $afferent, efferent: $efferent); 24 | 25 | $coupling->calculateInstability(); 26 | 27 | expect($coupling->toArray())->toBe([ 28 | 'afferent' => $afferent, 29 | 'efferent' => $efferent, 30 | 'instability' => $instability, 31 | ]); 32 | 33 | })->with([ 34 | [0, 0, 0.0], 35 | [1, 0, 0.0], 36 | [1, 1, 0.5], 37 | [2, 1, 0.33], 38 | [3, 1, 0.25], 39 | [4, 1, 0.2], 40 | [5, 1, 0.17], 41 | [1, 2, 0.67], 42 | [1, 3, 0.75], 43 | [1, 4, 0.8], 44 | [1, 5, 0.83], 45 | ]); 46 | -------------------------------------------------------------------------------- /tests/Unit/Domain/ValueObjects/FqcnTest.php: -------------------------------------------------------------------------------- 1 | looksLike([ 10 | 'App\Domain\ValueObjects\Coupling', 11 | 'App\Domain\ValueObjects\Instability', 12 | ]); 13 | 14 | expect($looked)->toBeTrue(); 15 | }); 16 | 17 | test('it able of not identifying himself', function () { 18 | 19 | $fqcn = new Fqcn('App\Domain\ValueObjects\Coupling'); 20 | 21 | $looked = $fqcn->looksLike([ 22 | 'App\Domain\ValueObjects\Other', 23 | ]); 24 | 25 | expect($looked)->toBeFalse(); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Unit/Infrastructure/Services/AnalyzerServiceAdapterTest.php: -------------------------------------------------------------------------------- 1 | getDependencies(new FileStub('A.php')); 12 | 13 | expect($dependencies)->toBeInstanceOf(ClassDependencies::class); 14 | 15 | expect($dependencies->toArray()['dependencies'])->toBe( 16 | [ 17 | 'App\Infrastructure\Services\Stubs\B', 18 | 'App\Infrastructure\Services\Stubs\C', 19 | 'App\Infrastructure\Services\Stubs\D', 20 | 'App\Infrastructure\Services\Stubs\E', 21 | 'F', 22 | 'G', 23 | ] 24 | ); 25 | }); 26 | 27 | it('exclude native PHP dependencies', function () { 28 | 29 | $analyzerServiceAdapter = app(AnalyzerServiceAdapter::class); 30 | 31 | $dependencies = $analyzerServiceAdapter->getDependencies(new FileStub('Native.php')); 32 | 33 | expect($dependencies->hasNoDependencies())->toBeTrue(); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/Unit/Infrastructure/Services/FileStub.php: -------------------------------------------------------------------------------- 1 | path; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Unit/Infrastructure/Services/Stubs/A.php: -------------------------------------------------------------------------------- 1 | oneAnalyzeMetric() 10 | ->withInstability($instability) 11 | ->withAbstractnessRatio($abstractness) 12 | ->build(); 13 | 14 | $maintainability = $calculator->calculate($analyzeMetric); 15 | 16 | expect($maintainability)->toBe($expected); 17 | 18 | })->with([ 19 | [0, 0, 'risky'], // lowly abstract and highly unstable 20 | [1, 1, 'good'], // highly abstract and highly unstable 21 | [0, 1, 'suffering'], // lowly abstract and highly unstable 22 | [1, 0, 'good'], // highly abstract and stable 23 | ]); 24 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/ComponentMapperTest.php: -------------------------------------------------------------------------------- 1 | componentMapper = new ComponentMapper(new ComponentFactory()); 9 | }); 10 | 11 | it('should map metrics to components', function () { 12 | 13 | $metrics = [ 14 | 'A' => [ 15 | $this->oneAnalyzeMetric()->withName('A\Class1')->build(), 16 | $this->oneAnalyzeMetric()->withName('A\Class2')->build(), 17 | $this->oneAnalyzeMetric()->withName('A\Class3')->build(), 18 | ] 19 | ]; 20 | 21 | $components = $this->componentMapper->from($metrics); 22 | 23 | expect($components)->toHaveCount(1); 24 | expect($components[0])->toBeInstanceOf(Component::class); 25 | expect($components[0]->name())->toBe('A'); 26 | }); 27 | 28 | it('should map metrics to components with dependencies', function () { 29 | 30 | $metrics = [ 31 | 'A' => [ 32 | $this->oneAnalyzeMetric()->withName('A\Class1')->withDependencies(['B\Class2'])->build(), 33 | ], 34 | 'B' => [ 35 | // 36 | ] 37 | ]; 38 | 39 | $components = $this->componentMapper->from($metrics); 40 | 41 | expect($components)->toHaveCount(2); 42 | expect($components[0]->dependencies())->toBe(['B']); 43 | }); 44 | 45 | it('should not keep dependencies from unwanted namespaces', function () { 46 | 47 | $metrics = [ 48 | 'A' => [ 49 | /** 50 | * This dependency is in an unwanted namespace C 51 | */ 52 | $this->oneAnalyzeMetric()->withName('A\Class1')->withDependencies(['C\Class2'])->build(), 53 | ], 54 | 'B' => [ 55 | // 56 | ] 57 | ]; 58 | 59 | $components = $this->componentMapper->from($metrics); 60 | 61 | expect($components)->toHaveCount(2); 62 | expect($components[0]->dependencies())->toBe([]); 63 | }); 64 | 65 | it('should calculate the average abstractness', function () { 66 | 67 | $metrics = [ 68 | 'A' => [ 69 | $this->oneAnalyzeMetric()->build(), 70 | $this->oneAnalyzeMetric()->isAbstract()->build(), 71 | $this->oneAnalyzeMetric()->isAbstract()->build(), 72 | $this->oneAnalyzeMetric()->isAbstract()->build(), 73 | ], 74 | ]; 75 | 76 | $components = $this->componentMapper->from($metrics); 77 | 78 | expect($components[0]->countClasses())->toBe(4); 79 | expect($components[0]->countAbstractions())->toBe(3); 80 | expect($components[0]->abstractness())->toBe(0.75); 81 | }); 82 | 83 | it('should calculate the average instability', function () { 84 | 85 | $metrics = [ 86 | 'A' => [ 87 | $this->oneAnalyzeMetric()->withInstability(0.3)->build(), 88 | $this->oneAnalyzeMetric()->withInstability(0.7)->build(), 89 | $this->oneAnalyzeMetric()->withInstability(1)->build(), 90 | ], 91 | ]; 92 | 93 | $components = $this->componentMapper->from($metrics); 94 | 95 | expect($components[0]->instability())->toBe(0.67); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/Filters/TargetFilterTest.php: -------------------------------------------------------------------------------- 1 | apply([ 16 | 'A' => $this->oneAnalyzeMetric()->withName('A')->withDependencies(['B'])->build(), 17 | 'B' => $this->oneAnalyzeMetric()->withName('B')->build(), 18 | 'C' => $this->oneAnalyzeMetric()->withName('C')->build(), 19 | ]); 20 | 21 | expect($result)->toHaveLength(2); 22 | expect($result)->toHaveKeys(['A', 'B']); 23 | }); 24 | 25 | it('should throw an exception if the target is not found', function () { 26 | 27 | $filter = new TargetTransformer( 28 | new Depth(), 29 | new Metrics(), 30 | 'D', 31 | ); 32 | 33 | $filter->apply([]); 34 | 35 | })->throws(Exception::class, 'Target D not found on metrics, try verify the target name.'); 36 | 37 | it('should stop if the depth limit is reached', function () { 38 | 39 | $filter = new TargetTransformer( 40 | new Depth(), 41 | new Metrics(), 42 | 'A', 43 | 3, 44 | ); 45 | 46 | $result = $filter->apply([ 47 | 'A' => $this->oneAnalyzeMetric()->withName('A')->withDependencies(['B'])->build(), 48 | 'B' => $this->oneAnalyzeMetric()->withName('B')->withDependencies(['C'])->build(), 49 | 'C' => $this->oneAnalyzeMetric()->withName('C')->withDependencies(['D'])->build(), 50 | 'D' => $this->oneAnalyzeMetric()->withName('D')->build(), 51 | ]); 52 | 53 | expect($result)->toHaveLength(3); 54 | expect($result)->toHaveKeys(['A', 'B', 'C']); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/Formatters/ArrayFormatterTest.php: -------------------------------------------------------------------------------- 1 | 1], 9 | ['foo' => 2], 10 | ]; 11 | 12 | $expected = [ 13 | ['foo' => 2], 14 | ['foo' => 1], 15 | ]; 16 | 17 | expect(ArrayFormatter::sort('foo', $items))->toBe($expected); 18 | }); 19 | 20 | test('it cuts items', function () { 21 | 22 | $items = [1, 2, 3, 4, 5]; 23 | 24 | expect(ArrayFormatter::cut(3, $items))->toBe([1, 2, 3]); 25 | }); 26 | 27 | test('it keep items when limit is null', function () { 28 | 29 | $items = [1, 2, 3, 4, 5]; 30 | 31 | expect(ArrayFormatter::cut(null, $items))->toBe($items); 32 | }); 33 | 34 | test('it filters items by min value', function () { 35 | 36 | $items = [ 37 | ['foo' => 1], 38 | ['foo' => 2], 39 | ['foo' => 3], 40 | ]; 41 | 42 | $expected = [ 43 | ['foo' => 2], 44 | ['foo' => 3], 45 | ]; 46 | 47 | expect(ArrayFormatter::filterByMinValue('foo', 2, $items))->toBe($expected); 48 | }); 49 | 50 | test('it keeps items when min value is null', function () { 51 | 52 | $items = [ 53 | ['foo' => 1], 54 | ['foo' => 2], 55 | ['foo' => 3], 56 | ]; 57 | 58 | expect(ArrayFormatter::filterByMinValue('foo', null, $items))->toBe($items); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/Formatters/NameFormatterTest.php: -------------------------------------------------------------------------------- 1 | toBe('CyclicDependency'); 7 | }); 8 | 9 | test('it formats human readable names', function () { 10 | expect(NameFormatter::humanReadable('App\Domain\Services\CyclicDependency'))->toBe('Services\\CyclicDependency'); 11 | }); 12 | 13 | test('it keeps short names', function () { 14 | expect(NameFormatter::humanReadable('App\Foo'))->toBe('App\Foo'); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/Helpers/CycleHelperTest.php: -------------------------------------------------------------------------------- 1 | toBe('A -> B -> C -> A'); 12 | }); 13 | 14 | test('it formats a cycle with readable names', function () { 15 | 16 | $cycle = [ 17 | 'App\Domain\Services\BarService', 18 | 'App\Domain\Services\FooService', 19 | ]; 20 | 21 | $formatted = CycleHelper::through($cycle); 22 | 23 | expect($formatted)->toBe('Services\BarService -> Services\FooService -> Services\BarService'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/WeaknessSummaryPresenterTest.php: -------------------------------------------------------------------------------- 1 | present(new WeaknessResponse( 16 | 1, 17 | [ 18 | [ 19 | 'class' => 'ClassA', 20 | 'class_instability' => 0.5, 21 | 'dependency' => 'ClassB', 22 | 'dependency_instability' => 0.8, 23 | 'delta' => 0.3, 24 | ], 25 | ], 26 | )); 27 | 28 | expect($summarySpy->showTableHasBeenCalled)->toBeTrue(); 29 | expect($summarySpy->totalClasses)->toBe(1); 30 | expect($summarySpy->delta)->toBe(0.0); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/Unit/Presenter/WeaknessSummaryViewSpy.php: -------------------------------------------------------------------------------- 1 | showTableHasBeenCalled = true; 17 | $this->totalClasses = $viewModel->totalClasses; 18 | $this->delta = $viewModel->delta; 19 | } 20 | } 21 | --------------------------------------------------------------------------------