Skip to content

Commit 49b7420

Browse files
committed
Initial commit
General-purpose mixin interface generator for __call-based fluent builders. Scans class namespaces, reflects constructors, and produces typed interface files for IDE autocompletion. Optionally leverages Respect/Fluent's
0 parents  commit 49b7420

32 files changed

+4450
-0
lines changed

.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) <year> <copyright holders>
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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<!--
2+
SPDX-License-Identifier: ISC
3+
SPDX-FileCopyrightText: (c) Respect Project Contributors
4+
SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
5+
-->
6+
7+
# Respect\FluentGen
8+
9+
Generate PHP mixin interfaces from class namespaces, so IDEs can autocomplete
10+
`__call`-based fluent builder chains.
11+
12+
When a builder resolves method calls dynamically, IDEs can't see the available
13+
methods. FluentGen solves this by scanning your classes, reflecting their
14+
constructors, and generating interface files that declare every method with
15+
proper signatures and return types. Your builder class then references the
16+
generated interface via a `@mixin` docblock, and autocompletion works.
17+
18+
FluentGen works with any class namespace that follows a naming convention. If
19+
your classes use the `#[Composable]` attribute from
20+
[Respect/Fluent](https://github.com/Respect/Fluent), FluentGen additionally
21+
generates per-prefix composed interfaces, but that is not required.
22+
23+
## Installation
24+
25+
```bash
26+
composer require --dev respect/fluentgen
27+
```
28+
29+
Requires PHP 8.5+.
30+
31+
## What it generates
32+
33+
Given a namespace full of classes like `AreaFormatter`, `DateFormatter`,
34+
`MaskFormatter`, FluentGen produces two interfaces:
35+
36+
- A **Builder** interface with static methods (`Builder::area()`,
37+
`Builder::date()`, etc.) for starting chains.
38+
- A **Chain** interface with instance methods (`->area()`, `->date()`, etc.)
39+
for continuing them.
40+
41+
Each generated method mirrors the constructor signature of the underlying class.
42+
If `MaskFormatter` has `__construct(string $range, string $replacement = '*')`,
43+
the generated `mask()` method has the same parameters. Doc comments on the
44+
constructor are carried over too.
45+
46+
## Setting it up
47+
48+
FluentGen is typically wired into a Symfony Console command that you run during
49+
development. Here's how it looks like:
50+
51+
First, configure what to scan and where to write:
52+
53+
```php
54+
$config = new Config(
55+
sourceDir: __DIR__ . '/src',
56+
sourceNamespace: 'App\\Formatters',
57+
outputDir: __DIR__ . '/src/Mixins',
58+
outputNamespace: 'App\\Formatters\\Mixins',
59+
);
60+
```
61+
62+
`sourceDir` and `sourceNamespace` tell the scanner where your classes live.
63+
`outputDir` and `outputNamespace` control where the generated interfaces go.
64+
65+
Next, set up scanning. The `NamespaceScanner` reflects every concrete class in
66+
the directory. You can filter by interface and exclude specific classes:
67+
68+
```php
69+
$scanner = new NamespaceScanner(
70+
nodeType: Formatter::class,
71+
excludedClassNames: ['FormatterBuilder'],
72+
);
73+
```
74+
75+
Without filters, the scanner picks up every non-abstract class it finds. The
76+
`nodeType` filter restricts to classes implementing a given interface. The
77+
exclusion list removes specific classes by short name, useful for excluding the
78+
builder class itself if it lives in the same namespace.
79+
80+
Then configure the generator. `MixinGenerator` needs to know what interfaces to
81+
produce. Each `InterfaceConfig` describes one:
82+
83+
```php
84+
$generator = new MixinGenerator(
85+
config: $config,
86+
scanner: $scanner,
87+
methodBuilder: new MethodBuilder(classSuffix: 'Formatter'),
88+
interfaces: [
89+
new InterfaceConfig(
90+
suffix: 'Builder',
91+
returnType: Chain::class,
92+
static: true,
93+
),
94+
new InterfaceConfig(
95+
suffix: 'Chain',
96+
returnType: Chain::class,
97+
rootExtends: [Formatter::class],
98+
),
99+
],
100+
);
101+
```
102+
103+
The `suffix` determines the interface name. The `returnType` is what every
104+
generated method returns, typically your Chain interface, enabling fluent
105+
chaining. Set `static: true` for the builder entry point. Use `rootExtends`
106+
when the chain interface should extend your domain interface.
107+
108+
The `MethodBuilder` handles how class names map to method names. The
109+
`classSuffix` option strips a suffix before generating: `AreaFormatter` becomes
110+
`area()`, `DateFormatter` becomes `date()`.
111+
112+
Finally, call `generate()` to get a filename-to-content map:
113+
114+
```php
115+
$files = $generator->generate();
116+
117+
foreach ($files as $filename => $content) {
118+
file_put_contents($filename, $content);
119+
}
120+
```
121+
122+
Run this as part of your dev tooling: a console command, a Composer script, or
123+
CI check that verifies generated files are up to date.
124+
125+
## Composition support (optional, requires Respect/Fluent)
126+
127+
Some libraries, like Respect/Validation, use prefix composition where
128+
`notEmail()` creates `Not(Email())`. If your classes use the `#[Composable]`
129+
attribute from Respect/Fluent, FluentGen handles this automatically.
130+
131+
Install the optional dependency:
132+
133+
```bash
134+
composer require respect/fluent
135+
```
136+
137+
The `MixinGenerator` discovers composable prefixes and generates per-prefix
138+
interfaces. For example, a `Not` class with `#[Composable('not')]` produces a
139+
`NotBuilder` interface containing `notEmail()`, `notString()`, etc., and a root
140+
`Builder` interface that extends all prefix interfaces.
141+
142+
Composition constraints (`without`, `with`, `optIn` on the `Composable`
143+
attribute) are respected during generation. Forbidden combinations are excluded
144+
from the generated interfaces.
145+
146+
For the runtime prefix map, `PrefixConstantsGenerator` produces a constants
147+
class with `COMPOSABLE`, `COMPOSABLE_WITH_ARGUMENT`, and `FORBIDDEN` arrays
148+
that `ComposableMap` uses at resolve time.
149+
150+
## Customization
151+
152+
**MethodBuilder** controls how constructor parameters become method signatures.
153+
Beyond `classSuffix`, it supports `excludedTypePrefixes` and
154+
`excludedTypeNames` to skip parameters whose types come from external packages
155+
you don't want in your public interface.
156+
157+
**FileRenderer** handles the final output, printing the generated namespace
158+
via Nette PHP Generator and applying the `OutputFormatter`. The formatter
159+
preserves existing SPDX license headers, converts tabs to spaces, normalizes
160+
nullable syntax (`?Type` becomes `Type|null`), and collapses single-line doc
161+
comments. Both are used with sensible defaults; you rarely need to customize
162+
them.
163+
164+
**InterfaceConfig** has a few more options for the root interface:
165+
`rootComment` adds a docblock (like `@mixin FormatterBuilder`), `rootUses`
166+
adds use statements, and `rootExtends` makes the interface extend others.

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 = [ "*.yml", "*.yaml", ".git*", "*.dist", "composer.*", ".github/*.yml", ".github/workflows/**.yml" ]
5+
SPDX-FileCopyrightText = "Respect Project Contributors"
6+
SPDX-License-Identifier = "ISC"

composer.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "respect/fluentgen",
3+
"description": "Code generation for fluent builder interfaces",
4+
"keywords": ["respect", "fluentgen", "mixin", "fluent"],
5+
"type": "library",
6+
"license": "ISC",
7+
"authors": [
8+
{
9+
"name": "Respect/FluentGen Contributors",
10+
"homepage": "https://github.com/Respect/FluentGen/graphs/contributors"
11+
}
12+
],
13+
"require": {
14+
"php": "^8.5",
15+
"nette/php-generator": "^4.1"
16+
},
17+
"require-dev": {
18+
"phpstan/phpstan": "^2.1",
19+
"phpstan/phpstan-deprecation-rules": "^2.0",
20+
"phpstan/phpstan-strict-rules": "^2.0",
21+
"phpunit/phpunit": "^12.5",
22+
"respect/coding-standard": "^5.0",
23+
"respect/fluent": "^1.0"
24+
},
25+
"suggest": {
26+
"respect/fluent": "Enables #[Composable] prefix composition support"
27+
},
28+
"autoload": {
29+
"psr-4": {
30+
"Respect\\FluentGen\\": "src/"
31+
}
32+
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"Respect\\FluentGen\\Test\\": "tests/"
36+
}
37+
},
38+
"scripts": {
39+
"phpcs": "vendor/bin/phpcs",
40+
"phpstan": "vendor/bin/phpstan",
41+
"phpunit": "vendor/bin/phpunit",
42+
"qa": [
43+
"@phpcs",
44+
"@phpstan",
45+
"@phpunit"
46+
]
47+
},
48+
"config": {
49+
"sort-packages": true,
50+
"allow-plugins": {
51+
"dealerdirect/phpcodesniffer-composer-installer": true
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)