├── .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 | [](https://github.com/DeGraciaMathieu/php-class-dependencies-analyzer/actions/workflows/testing.yml)
10 | 
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 |
--------------------------------------------------------------------------------