Skip to content

Commit fbdaecd

Browse files
authored
Merge pull request #114 from michaelpetri/feature/add-in-initializers
Added support for PHP 8.0 promoted parameters
2 parents 4746c4d + 793d9dd commit fbdaecd

File tree

11 files changed

+339
-5
lines changed

11 files changed

+339
-5
lines changed

src/Generator/ClassGenerator.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ClassGenerator extends AbstractGenerator implements TraitUsageInterface
3333
public const IMPLEMENTS_KEYWORD = 'implements';
3434
public const FLAG_ABSTRACT = 0x01;
3535
public const FLAG_FINAL = 0x02;
36+
private const CONSTRUCTOR_NAME = '__construct';
3637

3738
protected ?FileGenerator $containingFileGenerator = null;
3839

@@ -140,7 +141,17 @@ public static function fromReflection(ClassReflection $classReflection)
140141
}
141142

142143
if ($reflectionMethod->getDeclaringClass()->getName() == $className) {
143-
$methods[] = MethodGenerator::fromReflection($reflectionMethod);
144+
$method = MethodGenerator::fromReflection($reflectionMethod);
145+
146+
if (self::CONSTRUCTOR_NAME === strtolower($method->getName())) {
147+
foreach ($method->getParameters() as $parameter) {
148+
if ($parameter instanceof PromotedParameterGenerator) {
149+
$cg->removeProperty($parameter->getName());
150+
}
151+
}
152+
}
153+
154+
$methods[] = $method;
144155
}
145156
}
146157

@@ -857,6 +868,16 @@ public function addMethodFromGenerator(MethodGenerator $method)
857868
));
858869
}
859870

871+
if (self::CONSTRUCTOR_NAME !== strtolower($methodName)) {
872+
foreach ($method->getParameters() as $parameter) {
873+
if ($parameter instanceof PromotedParameterGenerator) {
874+
throw new Exception\InvalidArgumentException(
875+
'Promoted parameter can only be added to constructor.'
876+
);
877+
}
878+
}
879+
}
880+
860881
$this->methods[strtolower($methodName)] = $method;
861882
return $this;
862883
}

src/Generator/MethodGenerator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ public static function copyMethodSignature(MethodReflection $reflectionMethod):
7777
$method->setName($reflectionMethod->getName());
7878

7979
foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
80-
$method->setParameter(ParameterGenerator::fromReflection($reflectionParameter));
80+
$method->setParameter(
81+
$reflectionParameter->isPromoted()
82+
? PromotedParameterGenerator::fromReflection($reflectionParameter)
83+
: ParameterGenerator::fromReflection($reflectionParameter)
84+
);
8185
}
8286

8387
return $method;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\Code\Generator;
6+
7+
use Laminas\Code\Reflection\Exception\RuntimeException;
8+
use Laminas\Code\Reflection\ParameterReflection;
9+
10+
use function sprintf;
11+
12+
final class PromotedParameterGenerator extends ParameterGenerator
13+
{
14+
public const VISIBILITY_PUBLIC = 'public';
15+
public const VISIBILITY_PROTECTED = 'protected';
16+
public const VISIBILITY_PRIVATE = 'private';
17+
18+
/** @psalm-var PromotedParameterGenerator::VISIBILITY_* */
19+
private string $visibility;
20+
21+
/**
22+
* @psalm-param non-empty-string $name
23+
* @psalm-param ?non-empty-string $type
24+
* @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility
25+
*/
26+
public function __construct(
27+
string $name,
28+
?string $type = null,
29+
string $visibility = self::VISIBILITY_PUBLIC,
30+
?int $position = null,
31+
bool $passByReference = false
32+
) {
33+
parent::__construct(
34+
$name,
35+
$type,
36+
null,
37+
$position,
38+
$passByReference,
39+
);
40+
41+
$this->visibility = $visibility;
42+
}
43+
44+
/** @psalm-return non-empty-string */
45+
public function generate(): string
46+
{
47+
return $this->visibility . ' ' . parent::generate();
48+
}
49+
50+
public static function fromReflection(ParameterReflection $reflectionParameter): self
51+
{
52+
if (! $reflectionParameter->isPromoted()) {
53+
throw new RuntimeException(
54+
sprintf('Can not create "%s" from unprompted reflection.', self::class)
55+
);
56+
}
57+
58+
$visibility = self::VISIBILITY_PUBLIC;
59+
60+
if ($reflectionParameter->isProtectedPromoted()) {
61+
$visibility = self::VISIBILITY_PROTECTED;
62+
} elseif ($reflectionParameter->isPrivatePromoted()) {
63+
$visibility = self::VISIBILITY_PRIVATE;
64+
}
65+
66+
return self::fromParameterGeneratorWithVisibility(
67+
parent::fromReflection($reflectionParameter),
68+
$visibility
69+
);
70+
}
71+
72+
/** @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility */
73+
public static function fromParameterGeneratorWithVisibility(ParameterGenerator $generator, string $visibility): self
74+
{
75+
$name = $generator->getName();
76+
$type = $generator->getType();
77+
78+
if ('' === $name) {
79+
throw new \Laminas\Code\Generator\Exception\RuntimeException(
80+
'Name of promoted parameter must be non-empty-string.'
81+
);
82+
}
83+
84+
if ('' === $type) {
85+
throw new \Laminas\Code\Generator\Exception\RuntimeException(
86+
'Type of promoted parameter must be non-empty-string.'
87+
);
88+
}
89+
90+
return new self(
91+
$name,
92+
$type,
93+
$visibility,
94+
$generator->getPosition(),
95+
$generator->getPassedByReference()
96+
);
97+
}
98+
}

src/Reflection/MethodReflection.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ReflectionMethod as PhpReflectionMethod;
66
use ReturnTypeWillChange;
77

8+
use function array_key_exists;
89
use function array_shift;
910
use function array_slice;
1011
use function class_exists;
@@ -116,15 +117,37 @@ public function getPrototype($format = self::PROTOTYPE_AS_ARRAY)
116117
'by_ref' => $parameter->isPassedByReference(),
117118
'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
118119
];
120+
121+
if ($parameter->isPromoted()) {
122+
$prototype['arguments'][$parameter->getName()]['promoted'] = true;
123+
if ($parameter->isPublicPromoted()) {
124+
$prototype['arguments'][$parameter->getName()]['visibility'] = 'public';
125+
} elseif ($parameter->isProtectedPromoted()) {
126+
$prototype['arguments'][$parameter->getName()]['visibility'] = 'protected';
127+
} elseif ($parameter->isPrivatePromoted()) {
128+
$prototype['arguments'][$parameter->getName()]['visibility'] = 'private';
129+
}
130+
}
119131
}
120132

121133
if ($format == self::PROTOTYPE_AS_STRING) {
122134
$line = $prototype['visibility'] . ' ' . $prototype['return'] . ' ' . $prototype['name'] . '(';
123135
$args = [];
124136
foreach ($prototype['arguments'] as $name => $argument) {
125-
$argsLine = ($argument['type'] ?
126-
$argument['type'] . ' '
127-
: '') . ($argument['by_ref'] ? '&' : '') . '$' . $name;
137+
$argsLine =
138+
(
139+
array_key_exists('visibility', $argument)
140+
? $argument['visibility'] . ' '
141+
: ''
142+
) . (
143+
$argument['type']
144+
? $argument['type'] . ' '
145+
: ''
146+
) . (
147+
$argument['by_ref']
148+
? '&'
149+
: ''
150+
) . '$' . $name;
128151
if (! $argument['required']) {
129152
$argsLine .= ' = ' . var_export($argument['default'], true);
130153
}

src/Reflection/ParameterReflection.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ReflectionClass;
77
use ReflectionMethod;
88
use ReflectionParameter;
9+
use ReflectionProperty;
910
use ReturnTypeWillChange;
1011

1112
use function method_exists;
@@ -130,4 +131,41 @@ public function __toString()
130131
{
131132
return parent::__toString();
132133
}
134+
135+
/** @psalm-pure */
136+
public function isPromoted(): bool
137+
{
138+
if (! method_exists(parent::class, 'isPromoted')) {
139+
return false;
140+
}
141+
142+
return (bool) parent::isPromoted();
143+
}
144+
145+
public function isPublicPromoted(): bool
146+
{
147+
return $this->isPromoted()
148+
&& $this->getDeclaringClass()
149+
->getProperty($this->getName())
150+
->getModifiers()
151+
& ReflectionProperty::IS_PUBLIC;
152+
}
153+
154+
public function isProtectedPromoted(): bool
155+
{
156+
return $this->isPromoted()
157+
&& $this->getDeclaringClass()
158+
->getProperty($this->getName())
159+
->getModifiers()
160+
& ReflectionProperty::IS_PROTECTED;
161+
}
162+
163+
public function isPrivatePromoted(): bool
164+
{
165+
return $this->isPromoted()
166+
&& $this->getDeclaringClass()
167+
->getProperty($this->getName())
168+
->getModifiers()
169+
& ReflectionProperty::IS_PRIVATE;
170+
}
133171
}

test/Generator/ClassGeneratorTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use Laminas\Code\Generator\Exception\InvalidArgumentException;
1010
use Laminas\Code\Generator\GeneratorInterface;
1111
use Laminas\Code\Generator\MethodGenerator;
12+
use Laminas\Code\Generator\PromotedParameterGenerator;
1213
use Laminas\Code\Generator\PropertyGenerator;
1314
use Laminas\Code\Reflection\ClassReflection;
15+
use LaminasTest\Code\Generator\TestAsset\ClassWithPromotedParameter;
1416
use LaminasTest\Code\TestAsset\FooClass;
1517
use PHPUnit\Framework\TestCase;
1618
use ReflectionMethod;
@@ -1356,4 +1358,68 @@ class ClassWithFinalConst
13561358
$output = $classGenerator->generate();
13571359
self::assertSame($expectedOutput, $output, $output);
13581360
}
1361+
1362+
/** @requires PHP >= 8.0 */
1363+
public function testGenerateClassWithPromotedConstructorParameter(): void
1364+
{
1365+
$classGenerator = new ClassGenerator();
1366+
$classGenerator->setName('ClassWithPromotedParameter');
1367+
1368+
$classGenerator->addMethod('__construct', [
1369+
new PromotedParameterGenerator(
1370+
'bar',
1371+
'Foo',
1372+
PromotedParameterGenerator::VISIBILITY_PRIVATE,
1373+
),
1374+
]);
1375+
1376+
$expectedOutput = <<<EOS
1377+
class ClassWithPromotedParameter
1378+
{
1379+
public function __construct(private \Foo \$bar)
1380+
{
1381+
}
1382+
}
1383+
1384+
EOS;
1385+
1386+
self::assertEquals($expectedOutput, $classGenerator->generate());
1387+
}
1388+
1389+
/** @requires PHP >= 8.0 */
1390+
public function testClassWithPromotedParameterFromReflection(): void
1391+
{
1392+
$classGenerator = ClassGenerator::fromReflection(
1393+
new ClassReflection(ClassWithPromotedParameter::class)
1394+
);
1395+
1396+
$expectedOutput = <<<EOS
1397+
namespace LaminasTest\Code\Generator\TestAsset;
1398+
1399+
class ClassWithPromotedParameter
1400+
{
1401+
public function __construct(private string \$promotedParameter)
1402+
{
1403+
}
1404+
}
1405+
1406+
EOS;
1407+
1408+
self::assertEquals($expectedOutput, $classGenerator->generate());
1409+
}
1410+
1411+
/** @requires PHP >= 8.0 */
1412+
public function testFailToGenerateClassWithPromotedParameterOnNonConstructorMethod(): void
1413+
{
1414+
$classGenerator = new ClassGenerator();
1415+
$classGenerator->setName('promotedParameterOnNonConstructorMethod');
1416+
1417+
$this->expectExceptionObject(
1418+
new InvalidArgumentException('Promoted parameter can only be added to constructor.')
1419+
);
1420+
1421+
$classGenerator->addMethod('thisIsNoConstructor', [
1422+
new PromotedParameterGenerator('promotedParameter', 'string'),
1423+
]);
1424+
}
13591425
}

test/Generator/MethodGeneratorTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ protected function withParamsAndReturnType($mixed, array $array, ?callable $call
133133
{
134134
}
135135

136+
EOS;
137+
self::assertSame($target, (string) $methodGenerator);
138+
}
139+
140+
/** @requires PHP >= 8.0 */
141+
public function testCopyMethodSignatureForPromotedParameter(): void
142+
{
143+
$ref = new MethodReflection(TestAsset\ClassWithPromotedParameter::class, '__construct');
144+
145+
$methodGenerator = MethodGenerator::copyMethodSignature($ref);
146+
$target = <<<'EOS'
147+
public function __construct(private string $promotedParameter)
148+
{
149+
}
150+
136151
EOS;
137152
self::assertSame($target, (string) $methodGenerator);
138153
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaminasTest\Code\Generator\TestAsset;
6+
7+
final class ClassWithPromotedParameter
8+
{
9+
public function __construct(private string $promotedParameter) {
10+
}
11+
}

0 commit comments

Comments
 (0)