11# Respect\FluentAnalysis
22
33PHPStan extension for [ Respect/Fluent] ( https://github.com/Respect/Fluent ) builders.
4- Provides method resolution, parameter validation, and tuple-typed ` getNodes() `
5- without generated code.
4+ Provides method resolution, parameter validation, tuple-typed ` getNodes() ` , and
5+ type narrowing through assertion methods — without generated code.
66
77Fluent builders use ` __call ` to resolve method names to class instances. Since
88those methods don't exist as real declarations, PHPStan reports errors and can't
@@ -115,9 +115,52 @@ For builders using Respect/Fluent's composable prefixes (like Validation's
115115` notEmail() ` , ` nullOrStringType() ` ), the extension resolves composed methods
116116with correct parameter signatures.
117117
118+ ### Type narrowing
119+
120+ Builders can narrow the type of a variable through assertion methods. Node
121+ classes declare their assurance via the ` #[Assurance] ` attribute, assertion
122+ methods are marked with ` #[AssuranceAssertion] ` , and ` #[AssuranceParameter] `
123+ identifies the validated parameter and constructor parameters used for type
124+ resolution.
125+
126+ Void assertion methods narrow unconditionally:
127+
128+ ``` php
129+ $builder->intNode()->doAssert($x);
130+ // PHPStan now knows $x is int
131+ ```
132+
133+ Bool assertion methods work as type guards:
134+
135+ ``` php
136+ if ($builder->intNode()->isOk($x)) {
137+ // $x is int here
138+ }
139+ // $x is not int here
140+ ```
141+
142+ Chained nodes intersect their assurances:
143+
144+ ``` php
145+ $builder->intNode()->numericNode()->doAssert($x);
146+ // int ∩ (int|float|numeric-string) = int
147+ ```
148+
149+ The extension supports several assurance modes through the ` #[Assurance] `
150+ attribute:
151+
152+ - ** ` type ` ** — a fixed type string (e.g. ` int ` , ` float|int|numeric-string ` )
153+ - ** ` #[AssuranceParameter] ` ** — the type is taken from a constructor parameter
154+ annotated with the attribute (e.g. a class-string parameter)
155+ - ** ` from: value ` ** — narrows to the argument's literal type
156+ - ** ` from: member ` ** — narrows to the iterable value type of the argument
157+ - ** ` from: elements ` ** — narrows to an array of the inner assurance type
158+ - ** ` compose: union|intersect ` ** — combines assurances from multiple builder
159+ arguments
160+
118161## How it works
119162
120- The extension registers two PHPStan hooks:
163+ The extension registers three PHPStan hooks:
121164
1221651 . ** ` FluentMethodsExtension ` ** (` MethodsClassReflectionExtension ` ) — tells
123166 PHPStan which methods exist on each builder, with parameters extracted from
@@ -127,27 +170,35 @@ The extension registers two PHPStan hooks:
127170 ` DynamicStaticMethodReturnTypeExtension ` ) — intercepts each method call to
128171 track accumulated node types as a ` GenericObjectType ` wrapping a
129172 ` ConstantArrayType ` tuple. When ` getNodes() ` is called, the tuple is
130- returned directly.
173+ returned directly. Also accumulates assurance types through the chain.
174+
175+ 3 . ** ` FluentTypeSpecifyingExtension ` ** (` MethodTypeSpecifyingExtension ` ) —
176+ enables type narrowing in control flow. When a builder's assertion method
177+ is called, accumulated assurances are applied to narrow the input variable's
178+ type. Supports void assertions (unconditional) and bool guards (conditional).
131179
132- Both extensions share a ` MethodMap ` that resolves method names to target
133- class FQCNs, with parent-class fallback for builder inheritance.
180+ The extensions share a ` MethodMap ` for method resolution and an ` AssuranceMap `
181+ for type narrowing configuration, both with parent-class fallback for builder
182+ inheritance.
134183
135184The ` generate ` command reads the ` #[FluentNamespace] ` attribute from each
136185builder, extracts the factory's resolver and namespaces, discovers classes,
137186and uses ` FluentResolver::unresolve() ` to derive method names from class names.
138187
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.
188+ ## FluentAnalysis vs FluentGen
189+
190+ Another similar project is [ FluentGen] ( https://github.com/Respect/FluentGen ) .
191+
192+ Both are complementary, offering IDE support and type inference as separate packages.
193+
194+ | | FluentAnalysis | FluentGen |
195+ | ---------------------| --------------------------------------| --------------------------------------|
196+ | Generated files | None (one small neon cache) | Interface files per builder + prefix |
197+ | Return type | ` Builder<array{A, B, C}> ` | ` Builder ` (via ` @mixin ` ) |
198+ | ` getNodes() ` type | ` array{A, B, C} ` (exact tuple) | ` array<int, Node> ` (generic) |
199+ | Element access | ` $nodes[0] ` typed as ` A ` | ` mixed ` |
200+ | Deprecation | Forwarded automatically | Must regenerate |
201+ | Composable prefixes | Resolved from cache | Full method signatures |
202+ | Type narrowing | Assertion methods narrow input types | Not supported |
203+ | IDE support | PHPStan-powered (PhpStorm, VS Code) | Direct IDE autocomplete |
204+ | Maintenance | Re-run ` generate ` on class changes | Manual/generated |
0 commit comments