Skip to content

Commit 0ef76f3

Browse files
committed
Initial commit
PHPStan extension for Respect/Fluent builders: provides method resolution, parameter forwarding, tuple-precise return types, deprecation forwarding, and composable prefix support. Includes CLI command for generating fluent.neon from #[FluentNamespace] attributes.
0 parents  commit 0ef76f3

52 files changed

Lines changed: 5754 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
8+
jobs:
9+
tests:
10+
name: Tests
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v6
14+
- uses: shivammathur/setup-php@v2
15+
with:
16+
php-version: '8.5'
17+
- uses: ramsey/composer-install@v3
18+
- run: composer phpunit
19+
20+
static-analysis:
21+
name: Static Analysis
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v6
25+
- uses: shivammathur/setup-php@v2
26+
with:
27+
php-version: '8.5'
28+
- uses: ramsey/composer-install@v3
29+
- run: composer phpcs
30+
- run: composer phpstan

.github/workflows/reuse.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: REUSE
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
8+
jobs:
9+
reuse-check:
10+
name: Compliance Check
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v6
14+
- uses: actions/setup-python@v6
15+
with:
16+
python-version: '3.x'
17+
- run: pip install reuse
18+
- run: reuse lint

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.phpcs.cache
2+
.phpunit.cache/
3+
vendor/

LICENSE

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) Respect Project Contributors
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

LICENSES/ISC.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) Respect Project Contributors
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Respect\FluentAnalysis
2+
3+
PHPStan extension for [Respect/Fluent](https://github.com/Respect/Fluent) builders.
4+
Provides method resolution, parameter validation, and tuple-typed `getNodes()`
5+
without generated code.
6+
7+
Fluent builders use `__call` to resolve method names to class instances. Since
8+
those methods don't exist as real declarations, PHPStan reports errors and can't
9+
validate arguments. This extension teaches PHPStan what each method does: its
10+
parameters, return type, and the exact tuple of accumulated nodes.
11+
12+
```php
13+
$stack = Middleware::cors('*')->rateLimit(100);
14+
15+
// PHPStan knows:
16+
// cors() accepts (string $origin = '*')
17+
// rateLimit() accepts (int $maxRequests = 60)
18+
// getNodes() returns array{Cors, RateLimit}
19+
// $stack is Middleware<array{Cors, RateLimit}>
20+
21+
$stack->getNodes()[0]; // PHPStan infers: Cors
22+
$stack->typo(); // PHPStan error: method not found
23+
$stack->cors(42); // PHPStan error: int given, string expected
24+
```
25+
26+
## Installation
27+
28+
```bash
29+
composer require --dev respect/fluent-analysis
30+
```
31+
32+
Requires PHP 8.5+ and PHPStan 2.1+.
33+
34+
## Setup
35+
36+
### 1. Generate the method cache
37+
38+
```bash
39+
vendor/bin/fluent-analysis generate
40+
```
41+
42+
This scans your project for builder classes with `#[FluentNamespace]`, reads the
43+
factory configuration from the attribute, and writes a `fluent.neon` file mapping
44+
method names to target classes.
45+
46+
### 2. Include in your PHPStan config
47+
48+
```neon
49+
includes:
50+
- vendor/respect/fluent-analysis/extension.neon
51+
- fluent.neon
52+
```
53+
54+
The extension loads automatically via Composer's PHPStan plugin mechanism.
55+
The `fluent.neon` file provides the method map for your specific builders.
56+
57+
### 3. Re-generate when classes change
58+
59+
Run `vendor/bin/fluent-analysis generate` again after adding, removing, or
60+
renaming classes in your fluent namespaces. The command detects unchanged output
61+
and skips the write if nothing changed.
62+
63+
```bash
64+
# Custom output path
65+
vendor/bin/fluent-analysis generate -o phpstan/fluent.neon
66+
```
67+
68+
## Features
69+
70+
### Method resolution
71+
72+
Every method on your builder is resolved to its target class. PHPStan reports
73+
unknown methods as errors: typos are caught at analysis time.
74+
75+
### Constructor parameter forwarding
76+
77+
Method parameters come from the target class constructor. If `Cors` has
78+
`__construct(string $origin = '*')`, then `->cors()` accepts the same
79+
signature. Type mismatches are reported.
80+
81+
### Tuple-typed `getNodes()`
82+
83+
The extension tracks which node types are accumulated through the chain.
84+
`getNodes()` returns a precise tuple instead of `array<int, object>`:
85+
86+
```php
87+
$builder = new MiddlewareStack();
88+
$chain = $builder->cors('*')->rateLimit(100)->auth('bearer');
89+
90+
// PHPStan knows: array{Cors, RateLimit, Auth}
91+
$nodes = $chain->getNodes();
92+
93+
// Individual elements are typed
94+
$nodes[0]; // Cors
95+
$nodes[1]; // RateLimit
96+
$nodes[2]; // Auth
97+
```
98+
99+
Tuple tracking works through variable assignments and static calls:
100+
101+
```php
102+
$a = MiddlewareStack::cors('*');
103+
$b = $a->rateLimit(100);
104+
$b->getNodes(); // array{Cors, RateLimit}
105+
```
106+
107+
### Deprecation forwarding
108+
109+
If a target class is marked `@deprecated`, the fluent method inherits the
110+
deprecation. PHPStan reports it wherever the method is called.
111+
112+
### Composable prefix support
113+
114+
For builders using Respect/Fluent's composable prefixes (like Validation's
115+
`notEmail()`, `nullOrStringType()`), the extension resolves composed methods
116+
with correct parameter signatures.
117+
118+
## How it works
119+
120+
The extension registers two PHPStan hooks:
121+
122+
1. **`FluentMethodsExtension`** (`MethodsClassReflectionExtension`) — tells
123+
PHPStan which methods exist on each builder, with parameters extracted from
124+
the target class constructor.
125+
126+
2. **`FluentDynamicReturnTypeExtension`** (`DynamicMethodReturnTypeExtension` +
127+
`DynamicStaticMethodReturnTypeExtension`) — intercepts each method call to
128+
track accumulated node types as a `GenericObjectType` wrapping a
129+
`ConstantArrayType` tuple. When `getNodes()` is called, the tuple is
130+
returned directly.
131+
132+
Both extensions share a `MethodMap` that resolves method names to target
133+
class FQCNs, with parent-class fallback for builder inheritance.
134+
135+
The `generate` command reads the `#[FluentNamespace]` attribute from each
136+
builder, extracts the factory's resolver and namespaces, discovers classes,
137+
and uses `FluentResolver::unresolve()` to derive method names from class names.
138+
139+
## vs. `@mixin`-style interfaces
140+
141+
| | FluentAnalysis | `@mixin` |
142+
|---------------------|-------------------------------------|--------------------------------------|
143+
| Generated files | None (one small neon cache) | Interface files per builder + prefix |
144+
| Return type | `Builder<array{A, B, C}>` | `Builder` (via `@mixin`) |
145+
| `getNodes()` type | `array{A, B, C}` (exact tuple) | `array<int, Node>` (generic) |
146+
| Element access | `$nodes[0]` typed as `A` | `mixed` |
147+
| Deprecation | Forwarded automatically | Must regenerate |
148+
| Composable prefixes | Resolved from cache | Full method signatures |
149+
| IDE support | PHPStan-powered (PhpStorm, VS Code) | Direct IDE autocomplete |
150+
| Maintenance | Re-run `generate` on class changes | Manual/generated |
151+
152+
Both approaches work. Use FluentAnalysis for precise type tracking. Use `@mixin`s
153+
for broader IDE autocomplete without PHPStan.

REUSE.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version = 1
2+
3+
[[annotations]]
4+
path = [ "tests/fixtures/**", "*.neon", "*.yml", "*.yaml", ".git*", "*.dist", "*.md", "composer.*", "LICENSE", ".github/*.yml", ".github/workflows/**.yml" ]
5+
SPDX-FileCopyrightText = "Respect Project Contributors"
6+
SPDX-License-Identifier = "ISC"

bin/fluent-analysis

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-License-Identifier: ISC
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
require $GLOBALS['_composer_autoload_path'] ?? __DIR__ . '/../vendor/autoload.php';
12+
13+
use Respect\FluentAnalysis\Commands\GenerateCommand;
14+
use Symfony\Component\Console\Application;
15+
16+
return (static function () {
17+
$application = new Application('Respect/FluentAnalysis', '1.0');
18+
$application->addCommand(new GenerateCommand());
19+
$application->setDefaultCommand('generate');
20+
21+
return $application->run();
22+
})();

composer.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "respect/fluent-analysis",
3+
"description": "PHPStan extension for Respect/Fluent builder method resolution",
4+
"type": "phpstan-extension",
5+
"license": "ISC",
6+
"require": {
7+
"php": "^8.5",
8+
"phpstan/phpstan": "^2.1",
9+
"respect/fluent": "^1.0",
10+
"symfony/console": "^6.0|^7.0"
11+
},
12+
"require-dev": {
13+
"phpunit/phpunit": "^12.5",
14+
"respect/coding-standard": "^5.0"
15+
},
16+
"bin": ["bin/fluent-analysis"],
17+
"extra": {
18+
"phpstan": {
19+
"includes": ["extension.neon"]
20+
}
21+
},
22+
"autoload": {
23+
"psr-4": {
24+
"Respect\\FluentAnalysis\\": "src/"
25+
}
26+
},
27+
"autoload-dev": {
28+
"psr-4": {
29+
"Respect\\FluentAnalysis\\Test\\": "tests/"
30+
}
31+
},
32+
"scripts": {
33+
"phpcs": "vendor/bin/phpcs",
34+
"phpstan": "vendor/bin/phpstan",
35+
"phpunit": "vendor/bin/phpunit",
36+
"qa": [
37+
"@phpcs",
38+
"@phpstan",
39+
"@phpunit"
40+
]
41+
},
42+
"config": {
43+
"sort-packages": true,
44+
"allow-plugins": {
45+
"dealerdirect/phpcodesniffer-composer-installer": true
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)