@@ -15,8 +15,8 @@ $stack = Middleware::cors('*')
1515$stack->getNodes(); // [Cors('*'), RateLimit(100), Auth('bearer'), JsonBody()]
1616```
1717
18- Middlewares, validators, processors. Anything that has potential for chain
19- composability could leverage respect/fluent .
18+ Middlewares, validators, processors: anything that composes well as a chain
19+ can leverage Respect/Fluent .
2020
2121## Installation
2222
@@ -28,10 +28,8 @@ composer require respect/fluent
2828
2929### 1. Choose a namespace and interface
3030
31- Fluent uses classes from one or more namespaces, and they all must share
32- a single interface.
33-
34- For example:
31+ Fluent discovers classes from one or more namespaces. Giving them a shared
32+ interface lets your builder enforce type safety and expose domain methods.
3533
3634``` php
3735namespace App\Middleware;
@@ -57,29 +55,25 @@ final readonly class RateLimit implements Middleware
5755
5856### 2. Extend FluentBuilder
5957
60- The ` __call ` method, immutable accumulation, and ` withNamespace ` support are
61- inherited. You only add domain logic:
58+ The ` #[FluentNamespace] ` attribute declares where your classes live and how to
59+ resolve them. The builder inherits ` __call ` , immutable accumulation, and
60+ ` withNamespace ` support, you only add domain logic:
6261
6362``` php
6463namespace App;
6564
65+ use Respect\Fluent\Attributes\FluentNamespace;
6666use Respect\Fluent\Builders\Append;
6767use Respect\Fluent\Factories\NamespaceLookup;
6868use Respect\Fluent\Resolvers\Ucfirst;
6969use App\Middleware\Middleware;
7070
71+ #[FluentNamespace(new NamespaceLookup(new Ucfirst(), Middleware::class, 'App\\Middleware'))]
7172final readonly class MiddlewareStack extends Append
7273{
7374 public function __construct(Middleware ...$layers)
7475 {
75- parent::__construct(
76- new NamespaceLookup(
77- new Ucfirst(), // fooBar -> new FooBar
78- Middleware::class, // FooBar implements Middleware
79- 'App\\Middleware' // App\Middleware\FooBar
80- ),
81- ...$layers,
82- );
76+ parent::__construct(static::factoryFromAttribute(), ...$layers);
8377 }
8478
8579 /** @return array<int , Middleware > */
@@ -90,15 +84,20 @@ final readonly class MiddlewareStack extends Append
9084}
9185```
9286
93- That's it. ` MiddlewareStack::cors()->auth('bearer')->jsonBody() ` then
94- builds the layers for you.
87+ The attribute carries the full factory configuration: the resolver (` Ucfirst ` ),
88+ optional type constraint (` Middleware::class ` ), and namespace to search. The
89+ inherited ` factoryFromAttribute() ` reads it at runtime so there's a single
90+ source of truth.
91+
92+ Now ` MiddlewareStack::cors()->auth('bearer')->jsonBody() ` builds the
93+ layers for you.
9594
9695### 3. Add composition if you want
9796
9897Prefix composition lets ` optionalAuth() ` create ` Optional(Auth()) ` . You're
99- not limited to ` Optional ` cases, you can design nesting as much as you want.
98+ not limited to ` Optional ` cases, you can design nesting as deep as you want.
10099
101- Annotate wrapper classes with ` #[Composable] ` and use ` ComposingLookup ` :
100+ Annotate wrapper classes with ` #[Composable] ` :
102101
103102``` php
104103namespace App\Middleware;
@@ -120,17 +119,16 @@ final readonly class Optional implements Middleware
120119}
121120```
122121
123- Update the constructor to use ` ComposingLookup ` :
122+ Then switch the attribute to use ` ComposingLookup ` , it automatically discovers
123+ ` #[Composable] ` prefixes from the same namespace:
124124
125125``` php
126126use Respect\Fluent\Factories\ComposingLookup;
127- use Respect\Fluent\Resolvers\ComposableAttributes;
128127
129- $flat = new NamespaceLookup(new Ucfirst(), Middleware::class, 'App\\Middleware');
130- parent::__construct(
131- new ComposingLookup($flat, new ComposableAttributes($flat)),
132- ...$layers,
133- );
128+ #[FluentNamespace(new ComposingLookup(
129+ new NamespaceLookup(new Ucfirst(), Middleware::class, 'App\\Middleware'),
130+ ))]
131+ final readonly class MiddlewareStack extends Append { /* ... */ }
134132```
135133
136134Now ` MiddlewareStack::optionalAuth('bearer') ` creates ` Optional(Auth('bearer')) ` .
@@ -174,16 +172,38 @@ a name, constructor arguments, and an optional wrapper.
174172```
175173
176174** NamespaceLookup vs ComposingLookup:** use ` NamespaceLookup ` for simple
177- name→ class mapping. Add ` ComposingLookup ` when you need prefix composition
178- like ` notEmail() ` → ` Not(Email()) ` . ` ComposingLookup ` supports recursive
179- unwrapping, so ` notNullOrEmail() ` → ` Not(NullOr(Email())) ` works too.
175+ name-to- class mapping. Wrap it with ` ComposingLookup ` when you need prefix
176+ composition like ` notEmail() ` → ` Not(Email()) ` . ` ComposingLookup ` supports
177+ recursive unwrapping, so ` notNullOrEmail() ` → ` Not(NullOr(Email())) ` works too.
180178
181179## API Reference
182180
181+ ### FluentNamespace (attribute)
182+
183+ Declares the factory configuration for a builder class. Both the runtime
184+ (` factoryFromAttribute() ` ) and static analysis (FluentAnalysis) read from this
185+ single source of truth:
186+
187+ ``` php
188+ use Respect\Fluent\Attributes\FluentNamespace;
189+
190+ // Simple lookup
191+ #[FluentNamespace(new NamespaceLookup(new Ucfirst(), null, 'App\\Handlers'))]
192+
193+ // With type validation
194+ #[FluentNamespace(new NamespaceLookup(new Ucfirst(), Handler::class, 'App\\Handlers'))]
195+
196+ // With prefix composition
197+ #[FluentNamespace(new ComposingLookup(
198+ new NamespaceLookup(new Ucfirst(), Validator::class, 'App\\Validators'),
199+ ))]
200+ ```
201+
183202### Builders
184203
185204Abstract base ` FluentBuilder ` provides ` __call ` , ` __callStatic ` , ` getNodes() ` ,
186- ` withNamespace() ` , and the abstract ` attach() ` method. Two concrete builders:
205+ ` withNamespace() ` , ` factoryFromAttribute() ` , and the abstract ` attach() ` method.
206+ Two concrete builders:
187207
188208** ` Append ` ** — each ` attach() ` appends nodes to the end:
189209
@@ -203,7 +223,7 @@ $chain = $builder->cors()->auth('bearer');
203223$chain->getNodes(); // [Auth('bearer'), Cors()]
204224```
205225
206- Both are ` readonly ` and not ` final ` — extend them and add your domain methods.
226+ Both are ` readonly ` and not ` final ` , extend them and add your domain methods.
207227` __callStatic ` calls ` new static() ` by default; override it if your subclass
208228needs a different way to obtain a default instance.
209229
@@ -235,40 +255,59 @@ $lookup->create('email', ['strict' => true]); // new App\Handlers\Email(strict:
235255$lookup->resolve('email'); // ReflectionClass (without instantiating)
236256```
237257
258+ The ` $resolver ` and ` $namespaces ` properties are ` public private(set) ` , you
259+ can read them (useful for tooling like FluentAnalysis) but not reassign them.
260+
238261Immutable builders: ` withNamespace() ` prepends a namespace, ` withNodeType() `
239262adds type validation. Both return new instances.
240263
241264#### ComposingLookup
242265
243- Wraps a ` NamespaceLookup ` + ` FluentResolver ` to handle prefix composition. When
244- the resolver produces a wrapper FluentNode, ComposingLookup creates the inner
245- instance first, then wraps it. Supports recursive unwrapping for nested
246- wrappers.
266+ Wraps a ` NamespaceLookup ` to handle prefix composition. When the resolver
267+ produces a wrapper FluentNode, ComposingLookup creates the inner instance
268+ first, then wraps it. Supports recursive unwrapping for nested wrappers.
247269
248270``` php
249- $nested = new ComposingLookup($lookup, new ComposableAttributes($lookup));
250- $nested->create('notEmail'); // Not(Email())
271+ $nested = new ComposingLookup($lookup); // defaults to ComposableAttributes
272+ $nested->create('notEmail'); // Not(Email())
273+ ```
274+
275+ You can pass a custom resolver as the second argument if you don't want
276+ automatic ` #[Composable] ` attribute discovery:
277+
278+ ``` php
279+ $nested = new ComposingLookup($lookup, new ComposableMap(
280+ composable: ['not' => true],
281+ ));
251282```
252283
253284### FluentResolver
254285
255- Interface for name transformers:
286+ Interface for name transformers. Each resolver can ` resolve ` a method name
287+ into a class name, and ` unresolve ` it back:
256288
257289``` php
258290interface FluentResolver
259291{
260292 public function resolve(FluentNode $nodeSpec): FluentNode;
293+ public function unresolve(FluentNode $nodeSpec): FluentNode;
261294}
262295```
263296
297+ The ` unresolve ` method is the inverse of ` resolve ` : it converts a class name
298+ back to the method name that would produce it. This is used by FluentAnalysis
299+ to derive method maps from discovered classes.
300+
264301#### Ucfirst
265302
266303Capitalizes the first letter: ` 'email' ` → ` 'Email' ` .
304+ Unresolve does the opposite: ` 'Email' ` → ` 'email' ` .
267305
268306#### Suffix
269307
270308Strips a prefix and appends a suffix: ` Suffix('of', 'Handler') ` turns
271309` 'ofArray' ` → ` 'ArrayHandler' ` .
310+ Unresolve reverses it: ` 'ArrayHandler' ` → ` 'ofArray' ` .
272311
273312#### Composable (attribute)
274313
@@ -283,6 +322,16 @@ final readonly class Not implements Validator
283322}
284323```
285324
325+ Attribute properties:
326+
327+ | Property | Type | Purpose |
328+ | -------------------| ----------| -------------------------------------------------|
329+ | ` prefix ` | ` string ` | Registers this class as a composition prefix |
330+ | ` prefixParameter ` | ` bool ` | First argument goes to the wrapper |
331+ | ` optIn ` | ` bool ` | Only compose with prefixes listed in ` with ` |
332+ | ` without ` | ` array ` | Prefixes this class should not be composed with |
333+ | ` with ` | ` array ` | Prefixes this class should be composed with |
334+
286335#### ComposableAttributes
287336
288337Discovers ` #[Composable] ` attributes at runtime and decomposes prefixed names:
@@ -293,22 +342,12 @@ $resolver = new ComposableAttributes($lookup);
293342```
294343
295344Caches prefix discoveries, suffix constraints, and negative lookups for
296- performance.
297-
298- Attribute properties:
299-
300- | Property | Type | Purpose |
301- | ---| ---| ---|
302- | ` prefix ` | ` string ` | Registers this class as a composition prefix |
303- | ` prefixParameter ` | ` bool ` | First argument goes to the wrapper |
304- | ` optIn ` | ` bool ` | Only compose with prefixes listed in ` with ` |
305- | ` without ` | ` array ` | Prefixes this class should not be composed with |
306- | ` with ` | ` array ` | Prefixes this class should be composed with |
345+ performance. Unresolve flattens wrapper structures back to flat names.
307346
308347#### ComposableMap
309348
310349Pre-built resolver using a compiled prefix map instead of runtime discovery.
311- Ideal for code-generated setups where all prefixes are known ahead of time:
350+ Useful for code-generated setups where all prefixes are known ahead of time:
312351
313352``` php
314353$resolver = new ComposableMap(
@@ -333,7 +372,7 @@ new FluentNode(
333372### Exceptions
334373
335374All exceptions implement ` FluentException ` (a ` Throwable ` marker interface),
336- so consumers can catch all Fluent errors with a single type:
375+ so you can catch all Fluent errors with a single type:
337376
338377``` php
339378use Respect\Fluent\Exceptions\FluentException;
@@ -345,9 +384,9 @@ try {
345384}
346385```
347386
348- | Exception | Parent | Thrown when |
349- | ---| ---| ---|
350- | ` CouldNotResolve ` | ` InvalidArgumentException ` | Name not found in any registered namespace |
351- | ` CouldNotCreate ` | ` InvalidArgumentException ` | Instantiation failed or type validation failed |
387+ | Exception | Parent | Thrown when |
388+ | ------------------- | ---------------------------- | --------------------------------------------- ---|
389+ | ` CouldNotResolve ` | ` InvalidArgumentException ` | Name not found in any registered namespace |
390+ | ` CouldNotCreate ` | ` InvalidArgumentException ` | Instantiation failed or type validation failed |
352391
353392Both extend ` InvalidArgumentException ` for backwards compatibility.
0 commit comments