Skip to content

Commit 701479b

Browse files
Amoifrclaude
andcommitted
release: v2.0.0 — eliminate Laravel false positives
End-to-end validation of the four PR2.0 changes (A1, A2, B1, B2, D1, D2) on a real Laravel project (Partoo storeloc-saas, 136 files, 110 classes): Layer violations: 12+ → 1 (the one remaining is a real Domain → Infrastructure violation) SOLID violations: 100+ → 6 (all legitimate; biggest controller is 10/10 instead of 104/104) AppServiceProvider: flagged → exempt (Wiring layer) Architecture score: B (73) Add an integration test under tests/Integration/ that pins those behaviours on a self-contained laravel-mini fixture (Action, Job, Provider, Controller, Repository, Model, ignore-annotated Service): - testActionIsApplicationNotController (A1) - testServiceProviderIsWiring (A2) - testNoLayerViolationsOnCommonLaravelStructure (A2) - testJobUsingQueueableHasNoDipViolation (B1+B2) - testIgnoreAnnotationSuppressesBillingServiceDip (D1) - testBaselineRoundTripSuppressesAllRemainingViolations (D1) A standalone PHP smoke runner is also included (run-partoo-smoke.php) for ad-hoc execution against any local Laravel/Symfony project, no Symfony kernel required. Documentation: - CHANGELOG.md gets a 2.0.0 section listing every breaking change with rationale and a "Migrating from 1.x" subsection. - README.md adds a "Migrating from 1.x" preamble and documents the new options (--config, --baseline, --generate-baseline, --wizard), the phpquality.json schema, the @phpquality-ignore annotation, and a baseline-based CI workflow. - DOCKERHUB.md mirrors the new options + a "What's new in 2.0" blurb pointing at the changelog. - composer.json branch-alias bumped to 2.0.x-dev. Total test count: 510 → 544. All green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7e14434 commit 701479b

14 files changed

Lines changed: 610 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.0] - 2026-05-06
11+
12+
Major release focused on **dramatically reducing false positives** in
13+
architectural / SOLID analysis on framework projects (Laravel and Symfony).
14+
The headline change is that `--type=laravel` is now a real, opinionated preset
15+
instead of a near-no-op flag.
16+
17+
### Breaking changes
18+
19+
- **Layer classification is now rule-based.** The previous
20+
`(layer ⇒ namespace[] + suffix[])` lookup is replaced by an ordered list of
21+
glob `LayerRule` objects (first match wins). `array_merge_recursive` is gone.
22+
Custom `ProjectTypeInterface` implementations must implement the new
23+
`getLayerRules()` method (defaults provided in `AbstractProjectType`).
24+
- **`*Action` is no longer classified as `Controller` in Laravel.** It maps to
25+
`Application` (use case). This eliminates ~12 spurious
26+
"Infrastructure → Controller" violations on a typical Laravel app using the
27+
Action pattern.
28+
- **`Wiring` is now a layer.** `ServiceProvider`, container extensions and
29+
files under `**\\Providers\\**` are flagged as `Wiring`, exempt from layer
30+
violation reporting and from DIP scoring. Their job is precisely to bind
31+
concrete classes from any layer, so the previous violations were noise.
32+
- **DIP scope reduced to injected dependencies.** Only `type_hint_param`,
33+
`type_hint_return`, and `type_hint_property` count toward the abstraction
34+
ratio. `extends`, `trait`, `new`, `static_call`, `instanceof`, `catch`,
35+
`implements`, `const`, `use` no longer pollute the metric. Old DIP scores
36+
are not comparable.
37+
- **DIP whitelist by default.** The Laravel preset excludes Eloquent
38+
primitives, Carbon, Closure, facades, queue/bus traits, and OpenAPI
39+
attributes from the DIP ratio. The Symfony preset excludes Doctrine ORM,
40+
HttpFoundation, Form, Validator, Security, Twig, etc. This eliminates the
41+
pathological case of e.g. `OrganizationController` flagged with
42+
`104/104 concrete dependencies`.
43+
- **`ProjectTypeInterface` gains 3 methods**: `getLayerRules()`,
44+
`getDipIgnoreList()`, `getWiringPatterns()`. `AbstractProjectType` provides
45+
generic defaults so existing custom types still compile, but presets that
46+
want framework-aware behaviour must override them.
47+
- **`SolidAnalyzer::analyze()` and `ArchitectureAnalyzer::analyze()` signatures
48+
changed.** `SolidAnalyzer::analyze(array $fileResults, array $dipIgnoreList = [], array $wiringClasses = [], array $classIgnores = [])`; `ArchitectureAnalyzer::analyze(array $fileResults, ?ProjectTypeInterface $projectType = null, ?ProjectConfig $config = null)`.
49+
- **`ProjectAnalyzer` constructor now requires `ProjectConfigLoader` and
50+
`BaselineManager`.** Symfony autowiring picks them up automatically.
51+
52+
### Added
53+
54+
- **`phpquality.json` project configuration file** at the project root.
55+
Schema (all keys optional):
56+
```json
57+
{
58+
"layers": { "rules": [{ "match": "App\\Foo\\**", "layer": "Application" }] },
59+
"wiring": { "patterns": ["**ServiceProvider"] },
60+
"abstractionRatio": { "ignore": ["App\\Models\\**"] },
61+
"ignore": { "violations": ["solid.dip:App\\Foo\\Bar"] },
62+
"baseline": "phpquality.baseline.json"
63+
}
64+
```
65+
`layers.rules` is **prepended** to the framework preset (project rules win).
66+
`wiring.patterns`, `abstractionRatio.ignore`, `ignore.violations` are
67+
**unioned** with framework defaults.
68+
- **`@phpquality-ignore` docblock annotation** on classes and interfaces:
69+
```php
70+
/** @phpquality-ignore solid.dip — wiring intentionnel */
71+
final class FooService { … }
72+
```
73+
Codes: `solid.srp`, `solid.dip`, `solid.isp`, `architecture.layer`.
74+
- **Baseline generation and application**.
75+
- `--generate-baseline=phpquality.baseline.json` writes a hash-keyed list of
76+
all current violations and exits successfully (does not fail-on-violation).
77+
- `--baseline=phpquality.baseline.json` filters violations whose hash is in
78+
the baseline. The summary exposes `suppressedByBaseline` and warns about
79+
`obsoleteBaselineEntries` (entries that no longer match anything →
80+
regenerate).
81+
- New CLI options: `--config`, `--baseline`, `--generate-baseline`.
82+
- New services: `PhpQuality\Config\ProjectConfigLoader`,
83+
`PhpQuality\Config\BaselineManager`, `PhpQuality\Config\ProjectConfig`,
84+
`PhpQuality\Config\LayerRule`,
85+
`PhpQuality\Analyzer\Ast\IgnoreAnnotationParser`.
86+
87+
### Migrating from 1.x
88+
89+
Most users only need to run the new version: false positives drop on their
90+
own. If you previously had a CI policy on the violation counts, expect those
91+
counts to **decrease**.
92+
93+
If you have a custom `ProjectTypeInterface` implementation:
94+
1. Have it extend `AbstractProjectType` (gets the 3 new methods for free), OR
95+
2. Implement `getLayerRules()`, `getDipIgnoreList()`, `getWiringPatterns()`.
96+
97+
If you want to keep some violations in your reports while you migrate, pin
98+
them via `--generate-baseline` and commit the baseline file. Subsequent runs
99+
with `--baseline=…` will only show new violations.
100+
10101
## [1.6.0] - 2026-03-26
11102

12103
### Added
@@ -107,7 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
107198
### Fixed
108199
- Allow running Docker container with any user (`--user` flag)
109200

110-
[Unreleased]: https://github.com/amoifr/PhpQuality/compare/v1.6.0...HEAD
201+
[Unreleased]: https://github.com/amoifr/PhpQuality/compare/v2.0.0...HEAD
202+
[2.0.0]: https://github.com/amoifr/PhpQuality/compare/v1.6.0...v2.0.0
111203
[1.6.0]: https://github.com/amoifr/PhpQuality/compare/v1.5.0...v1.6.0
112204
[1.5.0]: https://github.com/amoifr/PhpQuality/compare/v1.4.1...v1.5.0
113205
[1.4.1]: https://github.com/amoifr/PhpQuality/compare/v1.4.0...v1.4.1

DOCKERHUB.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ docker run --rm \
7777
| `--fail-on-violation` | Exit with error if violations found (CI mode) |
7878
| `--list-types` | List available project types |
7979
| `--list-langs` | List available languages |
80+
| `--config` | Path to a project configuration file (default: `./phpquality.json`) |
81+
| `--baseline` | Filter out violations listed in the baseline file |
82+
| `--generate-baseline` | Write a baseline of current violations and exit successfully |
83+
84+
---
85+
86+
## What's new in 2.0
87+
88+
- The Laravel preset (`--type=laravel`) is now a real preset: `*Action` is
89+
classified as `Application`, `ServiceProvider` as `Wiring` (exempt from
90+
layer violations), and Eloquent / Carbon / facades / queue traits are
91+
excluded from the DIP ratio.
92+
- DIP only counts type-hinted, injectable dependencies — no more false
93+
positives from `use Trait;` or PHP attributes.
94+
- New `phpquality.json` for project-level overrides, `@phpquality-ignore`
95+
docblock annotation, and a `--baseline` workflow to adopt the tool on
96+
existing projects without rewriting them.
97+
98+
See [CHANGELOG.md](https://github.com/Amoifr/phpquality/blob/main/CHANGELOG.md) for the full migration guide.
8099

81100
---
82101

README.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,33 @@
33
> PHP static analyzer available as a Symfony Bundle and Docker image, designed to replace `phpmetrics/phpmetrics` (now unmaintained).
44
> Analyzes your PHP code and generates detailed reports on complexity, maintainability, coupling, architecture, and test coverage.
55
6-
**Version:** 1.2.0
6+
**Version:** 2.0.0
77
**Author:** [Pascal CESCON](https://moi.ruedesjasses.fr)
88
**GitHub:** [amoifr/PhpQuality](https://github.com/amoifr/PhpQuality)
99

1010
---
1111

12+
## Migrating from 1.x
13+
14+
Version **2.0** sharply reduces false positives in architectural and SOLID
15+
analysis on framework projects. If you are upgrading from 1.x:
16+
17+
- The Laravel preset (`--type=laravel`) is now a real preset: `*Action` is
18+
`Application` (not Controller), `ServiceProvider` is `Wiring` (exempt from
19+
layer-violation reporting), and Eloquent / Carbon / facades / queue traits
20+
are excluded from the DIP ratio.
21+
- DIP only counts type-hinted, injectable dependencies. Old DIP scores are
22+
not directly comparable.
23+
- A `phpquality.json` file at the project root can override layer rules,
24+
add to whitelists, and pin a baseline.
25+
- `--generate-baseline=phpquality.baseline.json` then `--baseline=…` lets
26+
you adopt the tool on existing projects without rewriting them.
27+
28+
See [`CHANGELOG.md`](CHANGELOG.md) for the full breaking changes list and
29+
detailed migration notes.
30+
31+
---
32+
1233
## Installation
1334

1435
### As a Symfony Bundle
@@ -247,6 +268,55 @@ docker run --rm \
247268
| `--lang`, `-l` | Report language (en, fr, de, es, it, pt, nl, pl, ru, ja, zh, ko...) |
248269
| `--list-types` | List all available project types |
249270
| `--list-langs` | List all available languages |
271+
| `--wizard`, `-w` | Interactive wizard for guided configuration |
272+
| `--config` | Path to a project configuration file (default: `./phpquality.json`) |
273+
| `--baseline` | Path to a baseline file. Violations listed in the baseline are filtered out. |
274+
| `--generate-baseline` | Write a baseline file with all current violations and exit successfully. |
275+
276+
### Project configuration file (`phpquality.json`, since 2.0)
277+
278+
Drop a `phpquality.json` at the root of the project being analyzed to extend
279+
the framework preset:
280+
281+
```json
282+
{
283+
"layers": {
284+
"rules": [
285+
{ "match": "App\\Custom\\**", "layer": "Application" }
286+
]
287+
},
288+
"wiring": { "patterns": ["**ServiceProvider"] },
289+
"abstractionRatio": { "ignore": ["App\\Models\\**"] },
290+
"ignore": { "violations": ["solid.dip:App\\Foo\\Bar"] },
291+
"baseline": "phpquality.baseline.json"
292+
}
293+
```
294+
295+
Project rules under `layers.rules` are **prepended** to the framework preset
296+
(first match wins). The other lists are **unioned** with the preset.
297+
298+
### Suppressing a violation locally
299+
300+
```php
301+
/**
302+
* @phpquality-ignore solid.dip — this service intentionally wires concretes.
303+
*/
304+
final class FooService { /* … */ }
305+
```
306+
307+
Codes: `solid.srp`, `solid.dip`, `solid.isp`, `architecture.layer`.
308+
309+
### Baseline workflow (CI-friendly)
310+
311+
```bash
312+
# 1. Pin existing violations as accepted (do NOT fail the build)
313+
phpquality:analyze --source=app --type=laravel \
314+
--generate-baseline=phpquality.baseline.json
315+
316+
# 2. Subsequent runs only report NEW violations
317+
phpquality:analyze --source=app --type=laravel \
318+
--baseline=phpquality.baseline.json --fail-on-violation
319+
```
250320

251321
---
252322

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "MIT",
66
"extra": {
77
"branch-alias": {
8-
"dev-dev": "1.6.x-dev"
8+
"dev-dev": "2.0.x-dev"
99
}
1010
},
1111
"keywords": [
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpQuality\Tests\Integration;
6+
7+
use PhpQuality\Analyzer\ArchitectureAnalyzer;
8+
use PhpQuality\Analyzer\Architecture\LayerDetector;
9+
use PhpQuality\Analyzer\Architecture\SolidAnalyzer;
10+
use PhpQuality\Analyzer\Ast\AstParser;
11+
use PhpQuality\Analyzer\CoverageAnalyzer;
12+
use PhpQuality\Analyzer\DependenciesAnalyzer;
13+
use PhpQuality\Analyzer\FileAnalyzer;
14+
use PhpQuality\Analyzer\Metric\MaintainabilityIndex;
15+
use PhpQuality\Analyzer\ProjectAnalyzer;
16+
use PhpQuality\Analyzer\ProjectType\LaravelProjectType;
17+
use PhpQuality\Analyzer\ProjectType\PhpProjectType;
18+
use PhpQuality\Analyzer\ProjectType\ProjectTypeDetector;
19+
use PhpQuality\Analyzer\Result\SolidViolation;
20+
use PhpQuality\Config\BaselineManager;
21+
use PhpQuality\Config\ProjectConfigLoader;
22+
use PHPUnit\Framework\TestCase;
23+
24+
/**
25+
* Anti-regression for the Laravel false-positive class. Runs the full pipeline
26+
* against fixtures under tests/Integration/fixtures/laravel-mini/ and asserts:
27+
*
28+
* - App\Actions\PublishOrganizationAction is Application (not Controller) → A1
29+
* - App\Providers\AppServiceProvider is Wiring → A2
30+
* - The Job using Queueable / Dispatchable / Eloquent triggers no DIP → B1+B2
31+
* - The @phpquality-ignore solid.dip annotation suppresses the BillingService DIP
32+
* - The baseline round-trip (generate then re-apply) suppresses everything
33+
*/
34+
class LaravelEndToEndTest extends TestCase
35+
{
36+
private string $sourcePath;
37+
private ProjectAnalyzer $analyzer;
38+
39+
protected function setUp(): void
40+
{
41+
$this->sourcePath = __DIR__ . '/fixtures/laravel-mini';
42+
$this->analyzer = $this->buildAnalyzer();
43+
}
44+
45+
public function testActionIsApplicationNotController(): void
46+
{
47+
$result = $this->analyzer->analyze($this->sourcePath, 'laravel');
48+
49+
$this->assertSame(
50+
'Application',
51+
$result->architecture->layerAssignments['App\\Actions\\PublishOrganizationAction'] ?? null,
52+
'PublishOrganizationAction must be classified as Application.'
53+
);
54+
}
55+
56+
public function testServiceProviderIsWiring(): void
57+
{
58+
$result = $this->analyzer->analyze($this->sourcePath, 'laravel');
59+
60+
$this->assertSame(
61+
'Wiring',
62+
$result->architecture->layerAssignments['App\\Providers\\AppServiceProvider'] ?? null,
63+
);
64+
}
65+
66+
public function testNoLayerViolationsOnCommonLaravelStructure(): void
67+
{
68+
$result = $this->analyzer->analyze($this->sourcePath, 'laravel');
69+
70+
$sources = array_map(
71+
fn($v) => $v->sourceClass,
72+
$result->architecture->layerViolations,
73+
);
74+
75+
$this->assertNotContains(
76+
'App\\Providers\\AppServiceProvider',
77+
$sources,
78+
'AppServiceProvider must not appear as a violation source (it is wiring).',
79+
);
80+
}
81+
82+
public function testJobUsingQueueableHasNoDipViolation(): void
83+
{
84+
$result = $this->analyzer->analyze($this->sourcePath, 'laravel');
85+
86+
$jobDip = array_filter(
87+
$result->architecture->solidViolations,
88+
fn(SolidViolation $v) => $v->principle === SolidViolation::DIP
89+
&& $v->className === 'App\\Jobs\\SendWelcomeEmailJob',
90+
);
91+
$this->assertEmpty(
92+
$jobDip,
93+
'A Job using Queueable / Eloquent traits must not trigger DIP under B1+B2.'
94+
);
95+
}
96+
97+
public function testIgnoreAnnotationSuppressesBillingServiceDip(): void
98+
{
99+
$result = $this->analyzer->analyze($this->sourcePath, 'laravel');
100+
101+
$billingDip = array_filter(
102+
$result->architecture->solidViolations,
103+
fn(SolidViolation $v) => $v->principle === SolidViolation::DIP
104+
&& $v->className === 'App\\Services\\BillingService',
105+
);
106+
$this->assertEmpty(
107+
$billingDip,
108+
'@phpquality-ignore solid.dip on BillingService must suppress its DIP violation.'
109+
);
110+
}
111+
112+
public function testBaselineRoundTripSuppressesAllRemainingViolations(): void
113+
{
114+
$tmp = sys_get_temp_dir() . '/phpquality-mini-baseline-' . uniqid() . '.json';
115+
116+
try {
117+
// Generate
118+
$this->analyzer->setGenerateBaselinePath($tmp);
119+
$this->analyzer->analyze($this->sourcePath, 'laravel');
120+
$this->assertFileExists($tmp);
121+
122+
// Apply
123+
$this->analyzer->setGenerateBaselinePath(null);
124+
$this->analyzer->setBaselinePath($tmp);
125+
$applied = $this->analyzer->analyze($this->sourcePath, 'laravel');
126+
127+
$this->assertEmpty(
128+
$applied->architecture->layerViolations,
129+
'Baseline application must filter all known layer violations.',
130+
);
131+
$this->assertEmpty(
132+
$applied->architecture->solidViolations,
133+
'Baseline application must filter all known SOLID violations.',
134+
);
135+
} finally {
136+
@unlink($tmp);
137+
}
138+
}
139+
140+
private function buildAnalyzer(): ProjectAnalyzer
141+
{
142+
$astParser = new AstParser();
143+
$miCalc = new MaintainabilityIndex();
144+
$fileAnalyzer = new FileAnalyzer($astParser, $miCalc);
145+
146+
$detector = new ProjectTypeDetector([
147+
new PhpProjectType(),
148+
new LaravelProjectType(),
149+
]);
150+
151+
$solidAnalyzer = new SolidAnalyzer();
152+
$layerDetector = new LayerDetector();
153+
$architectureAnalyzer = new ArchitectureAnalyzer($layerDetector, $solidAnalyzer);
154+
155+
return new ProjectAnalyzer(
156+
fileAnalyzer: $fileAnalyzer,
157+
typeDetector: $detector,
158+
dependenciesAnalyzer: new DependenciesAnalyzer(),
159+
architectureAnalyzer: $architectureAnalyzer,
160+
coverageAnalyzer: new CoverageAnalyzer(),
161+
configLoader: new ProjectConfigLoader(),
162+
baselineManager: new BaselineManager(),
163+
);
164+
}
165+
}

0 commit comments

Comments
 (0)