Skip to content

Commit 4fedfeb

Browse files
committed
Replace string-based references with targeted parameter attributes
- Detect ComposableParameter via reflection instead of reading Composable::$prefixParameter - Resolve prefix strings from class short names via MethodBuilder::classToPrefix() instead of reading pre-resolved Composable::$prefix - Build FQCN→prefix lookup map for resolving class-string arrays in Composable::$without/$with - Add MethodBuilder::classToPrefix() for suffix-aware class-to-prefix resolution - Update NotHandler fixture to use self::class
1 parent 0e47b54 commit 4fedfeb

File tree

8 files changed

+190
-32
lines changed

8 files changed

+190
-32
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ composer require respect/fluent
135135
```
136136

137137
The `MixinGenerator` discovers composable prefixes and generates per-prefix
138-
interfaces. For example, a `Not` class with `#[Composable('not')]` produces a
138+
interfaces. For example, a `Not` class with `#[Composable(self::class)]` produces a
139139
`NotBuilder` interface containing `notEmail()`, `notString()`, etc., and a root
140140
`Builder` interface that extends all prefix interfaces.
141141

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"phpstan/phpstan-strict-rules": "^2.0",
2121
"phpunit/phpunit": "^12.5",
2222
"respect/coding-standard": "^5.0",
23-
"respect/fluent": "^1.0"
23+
"respect/fluent": "^2.0"
2424
},
2525
"suggest": {
2626
"respect/fluent": "Enables #[Composable] prefix composition support"

composer.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Fluent/MethodBuilder.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ public function __construct(
4444
) {
4545
}
4646

47+
public function classToPrefix(string $shortName): string
48+
{
49+
if ($this->classSuffix !== '' && str_ends_with($shortName, $this->classSuffix)) {
50+
$shortName = substr($shortName, 0, -strlen($this->classSuffix));
51+
}
52+
53+
return lcfirst($shortName);
54+
}
55+
4756
/** @param ReflectionClass<object> $nodeReflection */
4857
public function build(
4958
PhpNamespace $namespace,

src/Fluent/MixinGenerator.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use ReflectionClass;
1515
use ReflectionParameter;
1616
use Respect\Fluent\Attributes\Composable;
17+
use Respect\Fluent\Attributes\ComposableParameter;
1718
use Respect\FluentGen\CodeGenerator;
1819
use Respect\FluentGen\Config;
1920
use Respect\FluentGen\FileRenderer;
@@ -27,6 +28,7 @@
2728
* name: string,
2829
* prefix: string,
2930
* optIn: bool,
31+
* fqcn: class-string,
3032
* prefixParameter: ReflectionParameter|null,
3133
* }
3234
*/
@@ -101,24 +103,28 @@ private function discoverPrefixesAndFilters(array $nodes): array
101103
$attr = $attributes[0]->newInstance();
102104
$filters[$name] = $attr;
103105

104-
if ($attr->prefix === '') {
106+
if ($attr->prefix === null) {
105107
continue;
106108
}
107109

108110
$constructor = $reflection->getConstructor();
109111
$prefixParameter = null;
110112

111-
if ($attr->prefixParameter && $constructor !== null) {
112-
$parameters = $constructor->getParameters();
113-
if ($parameters !== []) {
114-
$prefixParameter = $parameters[0];
113+
if ($constructor !== null) {
114+
foreach ($constructor->getParameters() as $param) {
115+
if ($param->getAttributes(ComposableParameter::class) !== []) {
116+
$prefixParameter = $param;
117+
break;
118+
}
115119
}
116120
}
117121

118-
$prefixes[$attr->prefix] = [
122+
$prefix = $this->methodBuilder->classToPrefix($reflection->getShortName());
123+
$prefixes[$prefix] = [
119124
'name' => $reflection->getShortName(),
120-
'prefix' => $attr->prefix,
125+
'prefix' => $prefix,
121126
'optIn' => $attr->optIn,
127+
'fqcn' => $reflection->getName(),
122128
'prefixParameter' => $prefixParameter,
123129
];
124130
}
@@ -149,10 +155,10 @@ private function generateInterface(
149155
$filter = $filters[$name] ?? null;
150156

151157
if ($prefix['optIn']) {
152-
if ($filter === null || !in_array($prefix['prefix'], $filter->with, true)) {
158+
if ($filter === null || !in_array($prefix['fqcn'], $filter->with, true)) {
153159
continue;
154160
}
155-
} elseif ($filter !== null && in_array($prefix['prefix'], $filter->without, true)) {
161+
} elseif ($filter !== null && in_array($prefix['fqcn'], $filter->without, true)) {
156162
continue;
157163
}
158164

src/Fluent/PrefixConstantsGenerator.php

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
namespace Respect\FluentGen\Fluent;
1212

13+
use InvalidArgumentException;
1314
use Nette\PhpGenerator\PhpNamespace;
1415
use ReflectionClass;
1516
use Respect\Fluent\Attributes\Composable;
17+
use Respect\Fluent\Attributes\ComposableParameter;
1618
use Respect\FluentGen\CodeGenerator;
1719
use Respect\FluentGen\Config;
1820
use Respect\FluentGen\FileRenderer;
@@ -22,7 +24,7 @@
2224
use function array_keys;
2325
use function ctype_upper;
2426
use function ksort;
25-
use function lcfirst;
27+
use function sprintf;
2628
use function str_starts_with;
2729
use function strlen;
2830
use function uksort;
@@ -33,6 +35,7 @@ public function __construct(
3335
private Config $config,
3436
private NamespaceScanner $scanner,
3537
private string $outputClassName,
38+
private MethodBuilder $methodBuilder = new MethodBuilder(),
3639
private FileRenderer $renderer = new FileRenderer(),
3740
) {
3841
}
@@ -45,9 +48,10 @@ public function generate(): array
4548
$this->config->sourceNamespace,
4649
);
4750
$prefixes = $this->discoverPrefixes($nodes);
51+
$fqcnToPrefixMap = $this->buildFqcnMap($nodes);
4852
$composable = $this->buildComposable($nodes, $prefixes);
4953
$composableWithArgument = $this->buildComposableWithArgument($prefixes);
50-
$forbidden = $this->buildForbidden($nodes, $prefixes);
54+
$forbidden = $this->buildForbidden($nodes, $prefixes, $fqcnToPrefixMap);
5155

5256
$namespace = new PhpNamespace($this->config->outputNamespace);
5357
$class = $namespace->addClass($this->outputClassName);
@@ -78,13 +82,25 @@ private function discoverPrefixes(array $nodes): array
7882
}
7983

8084
$attr = $attributes[0]->newInstance();
81-
if ($attr->prefix === '') {
85+
if ($attr->prefix === null) {
8286
continue;
8387
}
8488

85-
$prefixes[$attr->prefix] = [
86-
'prefix' => $attr->prefix,
87-
'prefixParameter' => $attr->prefixParameter,
89+
$hasPrefixParameter = false;
90+
$constructor = $reflection->getConstructor();
91+
if ($constructor !== null) {
92+
foreach ($constructor->getParameters() as $param) {
93+
if ($param->getAttributes(ComposableParameter::class) !== []) {
94+
$hasPrefixParameter = true;
95+
break;
96+
}
97+
}
98+
}
99+
100+
$prefix = $this->methodBuilder->classToPrefix($reflection->getShortName());
101+
$prefixes[$prefix] = [
102+
'prefix' => $prefix,
103+
'prefixParameter' => $hasPrefixParameter,
88104
];
89105
}
90106

@@ -107,7 +123,7 @@ private function buildComposable(array $nodes, array $prefixes): array
107123
$composable[$prefix] = true;
108124

109125
foreach (array_keys($nodes) as $name) {
110-
$lcName = lcfirst($name);
126+
$lcName = $this->methodBuilder->classToPrefix($name);
111127
if ($lcName === $prefix) {
112128
continue;
113129
}
@@ -129,13 +145,30 @@ private function buildComposable(array $nodes, array $prefixes): array
129145
return $composable;
130146
}
131147

148+
/**
149+
* @param array<string, ReflectionClass<object>> $nodes
150+
*
151+
* @return array<class-string, string>
152+
*/
153+
private function buildFqcnMap(array $nodes): array
154+
{
155+
$map = [];
156+
157+
foreach ($nodes as $reflection) {
158+
$map[$reflection->getName()] = $this->methodBuilder->classToPrefix($reflection->getShortName());
159+
}
160+
161+
return $map;
162+
}
163+
132164
/**
133165
* @param array<string, ReflectionClass<object>> $nodes
134166
* @param array<string, array{prefix: string, prefixParameter: bool}> $prefixes
167+
* @param array<class-string, string> $fqcnToPrefixMap
135168
*
136169
* @return array<string, array<string, true>>
137170
*/
138-
private function buildForbidden(array $nodes, array $prefixes): array
171+
private function buildForbidden(array $nodes, array $prefixes, array $fqcnToPrefixMap): array
139172
{
140173
$forbidden = [];
141174
$prefixNames = array_keys($prefixes);
@@ -148,7 +181,9 @@ private function buildForbidden(array $nodes, array $prefixes): array
148181

149182
$attr = $attributes[0]->newInstance();
150183

151-
$blockedPrefixes = $attr->optIn ? array_diff($prefixNames, $attr->with) : $attr->without;
184+
$resolvedWith = $this->resolveClassStrings($attr->with, $fqcnToPrefixMap, $name);
185+
$resolvedWithout = $this->resolveClassStrings($attr->without, $fqcnToPrefixMap, $name);
186+
$blockedPrefixes = $attr->optIn ? array_diff($prefixNames, $resolvedWith) : $resolvedWithout;
152187

153188
if ($blockedPrefixes === []) {
154189
continue;
@@ -189,4 +224,27 @@ private function buildComposableWithArgument(array $prefixes): array
189224

190225
return $composableWithArgument;
191226
}
227+
228+
/**
229+
* @param list<class-string> $classStrings
230+
* @param array<class-string, string> $fqcnToPrefixMap
231+
*
232+
* @return list<string>
233+
*/
234+
private function resolveClassStrings(array $classStrings, array $fqcnToPrefixMap, string $context): array
235+
{
236+
$resolved = [];
237+
238+
foreach ($classStrings as $fqcn) {
239+
if (!isset($fqcnToPrefixMap[$fqcn])) {
240+
throw new InvalidArgumentException(
241+
sprintf('Composable on %s references unknown class %s', $context, $fqcn),
242+
);
243+
}
244+
245+
$resolved[] = $fqcnToPrefixMap[$fqcn];
246+
}
247+
248+
return $resolved;
249+
}
192250
}

tests/Fixtures/NotHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
use Respect\Fluent\Attributes\Composable;
1313

14-
#[Composable('not')]
14+
#[Composable(self::class)]
1515
final class NotHandler implements Handler
1616
{
1717
public function __construct(

0 commit comments

Comments
 (0)