Skip to content

Commit cd583c3

Browse files
committed
Refactor namespace/import management
1 parent 6a5ac51 commit cd583c3

13 files changed

+819
-116
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ echo $generator->dumpFile(function () use ($generator) {
189189
'final class DemoClass extends %s implements %s, %s',
190190
$generator->import('Example\BaseClass'),
191191
$generator->import('Example\Interfaces\FirstInterface'),
192-
$generator->import('Example\Interfaces\SecondInterface', byParent: true),
192+
$generator->import('Example\Interfaces\SecondInterface'),
193193
);
194194
yield '{';
195195
yield $generator->indent(function () use ($generator) {
@@ -484,9 +484,9 @@ use Doctrine\Common\Collections\Collection;
484484
use Example\Attributes\Entity;
485485
use Example\Attributes\Table;
486486
use Example\BaseClass;
487-
use Example\Interfaces;
488487
use Example\Interfaces\BaseInterface;
489488
use Example\Interfaces\FirstInterface;
489+
use Example\Interfaces\SecondInterface;
490490
use Example\SomeClass;
491491
use Logger;
492492
use self;
@@ -497,7 +497,7 @@ use stdClass;
497497

498498
#[Entity]
499499
#[Table]
500-
final class DemoClass extends BaseClass implements FirstInterface, Interfaces\SecondInterface
500+
final class DemoClass extends BaseClass implements FirstInterface, SecondInterface
501501
{
502502
// Class properties
503503
private DateTimeImmutable $date;

examples/example.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
'final class DemoClass extends %s implements %s, %s',
2424
$generator->import('Example\BaseClass'),
2525
$generator->import('Example\Interfaces\FirstInterface'),
26-
$generator->import('Example\Interfaces\SecondInterface', byParent: true),
26+
$generator->import('Example\Interfaces\SecondInterface'),
2727
);
2828
yield '{';
2929
yield $generator->indent(function () use ($generator) {

src/Alias.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\CodeGenerator;
6+
7+
use InvalidArgumentException;
8+
use Override;
9+
use Stringable;
10+
11+
final readonly class Alias implements Stringable
12+
{
13+
public string $alias;
14+
public FullyQualified | FunctionName | NamespaceName $target;
15+
16+
public function __construct(
17+
string $alias,
18+
FullyQualified | FunctionName | NamespaceName $target,
19+
) {
20+
$alias = trim($alias);
21+
22+
if ($alias === '') {
23+
throw new InvalidArgumentException('Alias cannot be empty');
24+
}
25+
26+
if (str_contains($alias, '\\')) {
27+
throw new InvalidArgumentException('Alias cannot contain namespace separator');
28+
}
29+
30+
$this->alias = $alias;
31+
$this->target = $target;
32+
}
33+
34+
#[Override]
35+
public function __toString() : string
36+
{
37+
return sprintf('%s as %s', $this->target, $this->alias);
38+
}
39+
40+
public function equals(object $other) : bool
41+
{
42+
return $other instanceof self
43+
&& $this->alias === $other->alias
44+
&& $this->target->equals($other->target);
45+
}
46+
47+
public function compare(object $other) : int
48+
{
49+
// Aliases should sort by their target, not their alias name
50+
return $this->target->compare($other);
51+
}
52+
53+
/**
54+
* Generate the use statement for this alias
55+
*/
56+
public function toUseStatement() : string
57+
{
58+
if ($this->target instanceof FunctionName) {
59+
return sprintf('use %s as %s;', $this->target, $this->alias);
60+
}
61+
62+
return sprintf('use %s as %s;', $this->target, $this->alias);
63+
}
64+
}

src/ClassName.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\CodeGenerator;
6+
7+
use InvalidArgumentException;
8+
use Override;
9+
use Stringable;
10+
11+
final readonly class ClassName implements Stringable
12+
{
13+
/**
14+
* @var non-empty-string
15+
*/
16+
public string $name;
17+
18+
public function __construct(
19+
string $name,
20+
) {
21+
$name = trim($name);
22+
23+
if ($name === '') {
24+
throw new InvalidArgumentException('Class name cannot be empty');
25+
}
26+
27+
if (str_contains($name, '\\')) {
28+
throw new InvalidArgumentException('Class name cannot contain namespace separator');
29+
}
30+
31+
$this->name = $name;
32+
}
33+
34+
/**
35+
* @phpstan-return ($input is null ? null : self)
36+
*/
37+
public static function maybeFromString(null | self | string $input) : ?self
38+
{
39+
if ($input === null) {
40+
return null;
41+
}
42+
43+
if ($input instanceof self) {
44+
return $input;
45+
}
46+
47+
return new self($input);
48+
}
49+
50+
#[Override]
51+
public function __toString() : string
52+
{
53+
return $this->name;
54+
}
55+
56+
public function equals(object $other) : bool
57+
{
58+
return $other instanceof self && $this->name === $other->name;
59+
}
60+
61+
public function compare(object $other) : int
62+
{
63+
if ($other instanceof self) {
64+
return strcasecmp($this->name, $other->name);
65+
}
66+
67+
if ($other instanceof Alias) {
68+
return strcasecmp($this->name, $other->alias);
69+
}
70+
71+
if ($other instanceof FunctionName) {
72+
return strcasecmp($this->name, $other->shortName);
73+
}
74+
75+
if ($other instanceof FullyQualified || $other instanceof NamespaceName) {
76+
return strcasecmp($this->name, (string) $other);
77+
}
78+
79+
return 0;
80+
}
81+
}

src/CodeGenerator.php

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
final class CodeGenerator
1616
{
1717
/**
18-
* @var array<string, string>
18+
* @var array<string, Alias|FullyQualified|FunctionName|NamespaceName>
1919
*/
2020
private array $imports = [];
21+
private readonly ?NamespaceName $namespace;
2122

2223
public function __construct(
23-
private readonly ?string $namespace = null,
24-
) {}
24+
null | NamespaceName | string $namespace = null,
25+
) {
26+
$this->namespace = NamespaceName::maybeFromString($namespace);
27+
}
2528

2629
/**
2730
* Dumps the generated code with proper formatting (removes consecutive newlines, trims)
@@ -74,30 +77,46 @@ public function dumpFile(array | Closure | Generator | string $iterable) : strin
7477
*/
7578
private function dumpImports() : Generator
7679
{
77-
uasort(
78-
$this->imports,
79-
fn($left, $right) => strcasecmp(
80-
str_replace('\\', ' ', str_starts_with($left, 'function ') ? substr($left, 9) : $left),
81-
str_replace('\\', ' ', str_starts_with($right, 'function ') ? substr($right, 9) : $right),
82-
),
83-
);
80+
uasort($this->imports, fn($left, $right) => $left->compare($right));
8481

8582
foreach ($this->imports as $alias => $import) {
86-
if ( ! str_starts_with($import, 'function ')) {
87-
[$namespace, $class] = $this->splitFqcn($import);
83+
if ($import instanceof Alias) {
84+
yield $import->toUseStatement();
8885

89-
if ($namespace === $this->namespace) {
90-
continue;
91-
}
86+
continue;
87+
}
9288

93-
if ($alias !== $class) {
94-
yield sprintf('use %s as %s;', $import, $alias);
89+
if ($import instanceof FunctionName) {
90+
// Handle function imports
91+
yield sprintf('use %s;', $import);
9592

96-
continue;
93+
continue;
94+
}
95+
96+
if ($import instanceof NamespaceName) {
97+
// Parent namespace import - check if we need an alias
98+
$lastPart = $import->lastPart;
99+
100+
if ($alias !== $lastPart) {
101+
yield sprintf('use %s as %s;', $import, $alias);
102+
} else {
103+
yield sprintf('use %s;', $import);
97104
}
105+
106+
continue;
98107
}
99108

100-
yield sprintf('use %s;', $import);
109+
// Skip if it's in the same namespace as the file
110+
if ($import->namespace !== null && $this->namespace !== null && $import->namespace->equals($this->namespace)) {
111+
continue;
112+
}
113+
114+
// Check if we need an alias
115+
if ($alias !== $import->className->name) {
116+
yield sprintf('use %s as %s;', $import, $alias);
117+
} else {
118+
yield sprintf('use %s;', $import);
119+
}
101120
}
102121
}
103122

@@ -137,27 +156,21 @@ public function maybeDump(
137156
yield from self::resolveIterable($after);
138157
}
139158

140-
/**
141-
* Splits a fully qualified class name into namespace and class name parts
142-
* @return array{string, string}
143-
*/
144-
public function splitFqcn(string $fqcn) : array
145-
{
146-
$parts = explode('\\', $fqcn);
147-
$className = array_pop($parts);
148-
$namespace = implode('\\', $parts);
149-
150-
return [$namespace, $className];
151-
}
152-
153159
/**
154160
* Finds an available alias for a type, appending numbers if the alias is already taken
155161
*/
156-
private function findAvailableAlias(string $type, string $alias, int $i = 1) : string
162+
private function findAvailableAlias(Alias | FullyQualified | FunctionName | NamespaceName $type, string $alias, int $i = 1) : string
157163
{
158164
$aliasToCheck = $i === 1 ? $alias : sprintf('%s%d', $alias, $i);
159165

160-
if ( ! isset($this->imports[$aliasToCheck]) || $this->imports[$aliasToCheck] === $type) {
166+
if ( ! isset($this->imports[$aliasToCheck])) {
167+
return $aliasToCheck;
168+
}
169+
170+
$existing = $this->imports[$aliasToCheck];
171+
172+
// Check if it's the same import
173+
if ($existing->equals($type)) {
161174
return $aliasToCheck;
162175
}
163176

@@ -167,60 +180,44 @@ private function findAvailableAlias(string $type, string $alias, int $i = 1) : s
167180
/**
168181
* Imports a class, function, or enum and returns the alias to use in the generated code
169182
*/
170-
public function import(string | UnitEnum $fqcnOrEnum, bool $byParent = false) : string
183+
public function import(FullyQualified | FunctionName | string | UnitEnum $fqcnOrEnum) : string
171184
{
172-
if (is_string($fqcnOrEnum) && str_starts_with($fqcnOrEnum, 'function ')) {
173-
$this->imports[$fqcnOrEnum] = $fqcnOrEnum;
174-
175-
$parts = explode('\\', $fqcnOrEnum);
185+
if ($fqcnOrEnum instanceof FunctionName) {
186+
$alias = $this->findAvailableAlias($fqcnOrEnum, $fqcnOrEnum->shortName);
187+
$this->imports[$alias] = $fqcnOrEnum;
176188

177-
return array_pop($parts);
189+
return $alias;
178190
}
179191

180192
if ($fqcnOrEnum instanceof UnitEnum) {
181-
$type = $fqcnOrEnum::class;
182-
} else {
183-
$type = $fqcnOrEnum;
184-
}
185-
186-
[$namespace, $alias] = $this->splitFqcn($type);
187-
188-
if ($byParent) {
189-
[, $parent] = $this->splitFqcn($namespace);
190-
191-
$parent = $this->findAvailableAlias($namespace, $parent);
193+
$fqcn = new FullyQualified($fqcnOrEnum::class);
194+
$alias = $this->findAvailableAlias($fqcn, $fqcn->className->name);
195+
$this->imports[$alias] = $fqcn;
192196

193-
$this->imports[$parent] = $namespace;
194-
195-
$reference = sprintf('%s\\%s', $parent, $alias);
196-
} else {
197-
$alias = $this->findAvailableAlias($type, $alias);
198-
199-
$this->imports[$alias] = $type;
200-
$reference = $alias;
197+
return sprintf('%s::%s', $alias, $fqcnOrEnum->name);
201198
}
202199

203-
if ($fqcnOrEnum instanceof UnitEnum) {
204-
return sprintf('%s::%s', $reference, $fqcnOrEnum->name);
205-
}
200+
$fqcn = FullyQualified::maybeFromString($fqcnOrEnum);
201+
$alias = $this->findAvailableAlias($fqcn, $fqcn->className->name);
202+
$this->imports[$alias] = $fqcn;
206203

207-
return $reference;
204+
return $alias;
208205
}
209206

210207
/**
211208
* Generates a PHP attribute string for the given fully qualified class name
212209
*/
213-
public function dumpAttribute(string $fqcn) : string
210+
public function dumpAttribute(FullyQualified | string $fqcn) : string
214211
{
215212
return sprintf('#[%s]', $this->import($fqcn));
216213
}
217214

218215
/**
219216
* Generates a class reference string (e.g., Foo::class)
220217
*/
221-
public function dumpClassReference(string $fqcn, bool $import = true, bool $byParent = false) : string
218+
public function dumpClassReference(FullyQualified | string $fqcn, bool $import = true) : string
222219
{
223-
return sprintf('%s::class', $import ? $this->import($fqcn, $byParent) : '\\' . $fqcn);
220+
return sprintf('%s::class', $import ? $this->import($fqcn) : '\\' . (string) $fqcn);
224221
}
225222

226223
/**

0 commit comments

Comments
 (0)