Skip to content

Commit 6f76c9c

Browse files
committed
Replace cached method maps with builder-based discovery
Method maps are now built from #[FluentNamespace] attributes at PHPStan boot instead of being pre-generated into a flat neon cache. Libraries declare builders via a mergeable list parameter: parameters: fluent: builders: - builder: Respect\Validation\ValidatorBuilder - builder: Respect\Validation\ValidatorBuilder namespace: App\Validators MethodMapFactory groups entries by builder, reads each attribute, applies extra namespaces via withNamespace(), and builds the full method/assurance maps through MethodMapBuilder. - Add MethodMapFactory with structured builders parameter - Add ConfigGenerator (replaces CacheGenerator) outputting new format - Fix NamespaceClassDiscovery to iterate all Composer ClassLoaders - Update README for the new workflow - Update extension.neon schema and services
1 parent 4e3119c commit 6f76c9c

16 files changed

+599
-502
lines changed

README.md

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
PHPStan extension for [Respect/Fluent](https://github.com/Respect/Fluent) builders.
44
Provides method resolution, parameter validation, tuple-typed `getNodes()`, and
5-
type narrowing through assertion methods — without generated code.
5+
type narrowing through assertion methods.
66

77
Fluent builders use `__call` to resolve method names to class instances. Since
88
those methods don't exist as real declarations, PHPStan reports errors and can't
@@ -33,38 +33,45 @@ Requires PHP 8.5+ and PHPStan 2.1+.
3333

3434
## Setup
3535

36-
### 1. Generate the method cache
36+
Libraries that ship a Fluent builder declare it in their `fluent.neon`:
3737

38-
```bash
39-
vendor/bin/fluent-analysis generate
38+
```neon
39+
parameters:
40+
fluent:
41+
builders:
42+
- builder: App\MiddlewareStack
4043
```
4144

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+
The extension loads automatically via
46+
[phpstan/extension-installer](https://github.com/phpstan/extension-installer).
47+
Method maps are built from `#[FluentNamespace]` attributes at PHPStan boot.
4548

46-
### 2. Include in your PHPStan config
49+
### Adding custom namespaces
50+
51+
To add extra node namespaces to an existing builder (e.g. custom validators):
4752

4853
```neon
49-
includes:
50-
- vendor/respect/fluent-analysis/extension.neon
51-
- fluent.neon
54+
parameters:
55+
fluent:
56+
builders:
57+
- builder: Respect\Validation\ValidatorBuilder
58+
namespace: App\Validators
5259
```
5360

54-
The extension loads automatically via Composer's PHPStan plugin mechanism.
55-
The `fluent.neon` file provides the method map for your specific builders.
61+
Entries from multiple neon files are merged automatically. Each package,
62+
extension, or user project can append entries independently.
5663

57-
### 3. Re-generate when classes change
64+
### Generating config for new projects
5865

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.
66+
For projects that define their own `#[FluentNamespace]` builders:
6267

6368
```bash
64-
# Custom output path
65-
vendor/bin/fluent-analysis generate -o phpstan/fluent.neon
69+
vendor/bin/fluent-analysis generate
6670
```
6771

72+
This scans your `composer.json` autoload entries for builder classes and writes
73+
a `fluent.neon` with the builder list and service registrations.
74+
6875
## Features
6976

7077
### Method resolution
@@ -181,9 +188,10 @@ The extensions share a `MethodMap` for method resolution and an `AssuranceMap`
181188
for type narrowing configuration, both with parent-class fallback for builder
182189
inheritance.
183190

184-
The `generate` command reads the `#[FluentNamespace]` attribute from each
185-
builder, extracts the factory's resolver and namespaces, discovers classes,
186-
and uses `FluentResolver::unresolve()` to derive method names from class names.
191+
At PHPStan boot, `MethodMapFactory` reads the `builders` parameter, reflects
192+
each builder's `#[FluentNamespace]` attribute, discovers classes in the
193+
declared namespaces, and builds the method/assurance maps. Extra namespaces
194+
from user config are merged via `withNamespace()`.
187195

188196
## FluentAnalysis vs FluentGen
189197

@@ -193,12 +201,11 @@ Both are complementary, offering IDE support and type inference as separate pack
193201

194202
| | FluentAnalysis | FluentGen |
195203
|---------------------|--------------------------------------|--------------------------------------|
196-
| Generated files | None (one small neon cache) | Interface files per builder + prefix |
204+
| Generated files | None | Interface files per builder + prefix |
197205
| Return type | `Builder<array{A, B, C}>` | `Builder` (via `@mixin`) |
198206
| `getNodes()` type | `array{A, B, C}` (exact tuple) | `array<int, Node>` (generic) |
199207
| Element access | `$nodes[0]` typed as `A` | `mixed` |
200208
| Deprecation | Forwarded automatically | Must regenerate |
201-
| Composable prefixes | Resolved from cache | Full method signatures |
209+
| Composable prefixes | Resolved from attribute | Full method signatures |
202210
| Type narrowing | Assertion methods narrow input types | Not supported |
203211
| IDE support | PHPStan-powered (PhpStorm, VS Code) | Direct IDE autocomplete |
204-
| Maintenance | Re-run `generate` on class changes | Manual/generated |

extension.neon

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
11
parameters:
22
fluent:
3-
methods: []
4-
assurances: []
5-
assertions: []
3+
builders: []
64

75
parametersSchema:
86
fluent: structure([
9-
methods: arrayOf(arrayOf(string())),
10-
assurances: arrayOf(arrayOf(arrayOf(anyOf(string(), int(), listOf(anyOf(int(), type('null'))))))),
11-
assertions: arrayOf(listOf(string()))
7+
builders: listOf(anyOf(
8+
structure([
9+
builder: string(),
10+
namespace: string()
11+
]),
12+
structure([
13+
builder: string()
14+
])
15+
))
1216
])
1317

1418
services:
19+
fluentNamespaceDiscovery:
20+
class: Respect\FluentAnalysis\NamespaceClassDiscovery
21+
22+
fluentMapBuilder:
23+
class: Respect\FluentAnalysis\MethodMapBuilder
24+
arguments:
25+
discovery: @fluentNamespaceDiscovery
26+
27+
fluentMapFactory:
28+
class: Respect\FluentAnalysis\MethodMapFactory
29+
arguments:
30+
builders: %fluent.builders%
31+
mapBuilder: @fluentMapBuilder
32+
1533
fluentMethodMap:
1634
class: Respect\FluentAnalysis\MethodMap
17-
arguments:
18-
methods: %fluent.methods%
35+
factory: @fluentMapFactory::createMethodMap
1936

2037
fluentAssuranceMap:
2138
class: Respect\FluentAnalysis\AssuranceMap
22-
arguments:
23-
assurances: %fluent.assurances%
24-
assertions: %fluent.assertions%
39+
factory: @fluentMapFactory::createAssuranceMap
2540

2641
-
2742
class: Respect\FluentAnalysis\FluentMethodsExtension

src/CacheGenerator.php

Lines changed: 0 additions & 208 deletions
This file was deleted.

src/Commands/GenerateCommand.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
namespace Respect\FluentAnalysis\Commands;
1212

1313
use Respect\FluentAnalysis\BuilderClassScanner;
14-
use Respect\FluentAnalysis\CacheGenerator;
15-
use Respect\FluentAnalysis\MethodMapBuilder;
16-
use Respect\FluentAnalysis\NamespaceClassDiscovery;
14+
use Respect\FluentAnalysis\ConfigGenerator;
1715
use Symfony\Component\Console\Attribute\AsCommand;
1816
use Symfony\Component\Console\Command\Command;
1917
use Symfony\Component\Console\Input\InputInterface;
@@ -30,15 +28,13 @@
3028

3129
#[AsCommand(
3230
name: 'generate',
33-
description: 'Generate fluent.neon cache for PHPStan',
31+
description: 'Generate fluent.neon config for PHPStan',
3432
)]
3533
final class GenerateCommand extends Command
3634
{
3735
public function __construct(
3836
private readonly BuilderClassScanner $scanner = new BuilderClassScanner(),
39-
private readonly CacheGenerator $generator = new CacheGenerator(
40-
new MethodMapBuilder(new NamespaceClassDiscovery()),
41-
),
37+
private readonly ConfigGenerator $generator = new ConfigGenerator(),
4238
) {
4339
parent::__construct();
4440
}

0 commit comments

Comments
 (0)