Skip to content

Commit

Permalink
Added support for promoted parameters
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Petri <[email protected]>
  • Loading branch information
michaelpetri committed Nov 29, 2021
1 parent 4746c4d commit 793d9dd
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 5 deletions.
23 changes: 22 additions & 1 deletion src/Generator/ClassGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ClassGenerator extends AbstractGenerator implements TraitUsageInterface
public const IMPLEMENTS_KEYWORD = 'implements';
public const FLAG_ABSTRACT = 0x01;
public const FLAG_FINAL = 0x02;
private const CONSTRUCTOR_NAME = '__construct';

protected ?FileGenerator $containingFileGenerator = null;

Expand Down Expand Up @@ -140,7 +141,17 @@ public static function fromReflection(ClassReflection $classReflection)
}

if ($reflectionMethod->getDeclaringClass()->getName() == $className) {
$methods[] = MethodGenerator::fromReflection($reflectionMethod);
$method = MethodGenerator::fromReflection($reflectionMethod);

if (self::CONSTRUCTOR_NAME === strtolower($method->getName())) {
foreach ($method->getParameters() as $parameter) {
if ($parameter instanceof PromotedParameterGenerator) {
$cg->removeProperty($parameter->getName());
}
}
}

$methods[] = $method;
}
}

Expand Down Expand Up @@ -857,6 +868,16 @@ public function addMethodFromGenerator(MethodGenerator $method)
));
}

if (self::CONSTRUCTOR_NAME !== strtolower($methodName)) {
foreach ($method->getParameters() as $parameter) {
if ($parameter instanceof PromotedParameterGenerator) {
throw new Exception\InvalidArgumentException(
'Promoted parameter can only be added to constructor.'
);
}
}
}

$this->methods[strtolower($methodName)] = $method;
return $this;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Generator/MethodGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ public static function copyMethodSignature(MethodReflection $reflectionMethod):
$method->setName($reflectionMethod->getName());

foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
$method->setParameter(ParameterGenerator::fromReflection($reflectionParameter));
$method->setParameter(
$reflectionParameter->isPromoted()
? PromotedParameterGenerator::fromReflection($reflectionParameter)
: ParameterGenerator::fromReflection($reflectionParameter)
);
}

return $method;
Expand Down
98 changes: 98 additions & 0 deletions src/Generator/PromotedParameterGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Laminas\Code\Generator;

use Laminas\Code\Reflection\Exception\RuntimeException;
use Laminas\Code\Reflection\ParameterReflection;

use function sprintf;

final class PromotedParameterGenerator extends ParameterGenerator
{
public const VISIBILITY_PUBLIC = 'public';
public const VISIBILITY_PROTECTED = 'protected';
public const VISIBILITY_PRIVATE = 'private';

/** @psalm-var PromotedParameterGenerator::VISIBILITY_* */
private string $visibility;

/**
* @psalm-param non-empty-string $name
* @psalm-param ?non-empty-string $type
* @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility
*/
public function __construct(
string $name,
?string $type = null,
string $visibility = self::VISIBILITY_PUBLIC,
?int $position = null,
bool $passByReference = false
) {
parent::__construct(
$name,
$type,
null,
$position,
$passByReference,
);

$this->visibility = $visibility;
}

/** @psalm-return non-empty-string */
public function generate(): string
{
return $this->visibility . ' ' . parent::generate();
}

public static function fromReflection(ParameterReflection $reflectionParameter): self
{
if (! $reflectionParameter->isPromoted()) {
throw new RuntimeException(
sprintf('Can not create "%s" from unprompted reflection.', self::class)
);
}

$visibility = self::VISIBILITY_PUBLIC;

if ($reflectionParameter->isProtectedPromoted()) {
$visibility = self::VISIBILITY_PROTECTED;
} elseif ($reflectionParameter->isPrivatePromoted()) {
$visibility = self::VISIBILITY_PRIVATE;
}

return self::fromParameterGeneratorWithVisibility(
parent::fromReflection($reflectionParameter),
$visibility
);
}

/** @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility */
public static function fromParameterGeneratorWithVisibility(ParameterGenerator $generator, string $visibility): self
{
$name = $generator->getName();
$type = $generator->getType();

if ('' === $name) {
throw new \Laminas\Code\Generator\Exception\RuntimeException(
'Name of promoted parameter must be non-empty-string.'
);
}

if ('' === $type) {
throw new \Laminas\Code\Generator\Exception\RuntimeException(
'Type of promoted parameter must be non-empty-string.'
);
}

return new self(
$name,
$type,
$visibility,
$generator->getPosition(),
$generator->getPassedByReference()
);
}
}
29 changes: 26 additions & 3 deletions src/Reflection/MethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use ReflectionMethod as PhpReflectionMethod;
use ReturnTypeWillChange;

use function array_key_exists;
use function array_shift;
use function array_slice;
use function class_exists;
Expand Down Expand Up @@ -116,15 +117,37 @@ public function getPrototype($format = self::PROTOTYPE_AS_ARRAY)
'by_ref' => $parameter->isPassedByReference(),
'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
];

if ($parameter->isPromoted()) {
$prototype['arguments'][$parameter->getName()]['promoted'] = true;
if ($parameter->isPublicPromoted()) {
$prototype['arguments'][$parameter->getName()]['visibility'] = 'public';
} elseif ($parameter->isProtectedPromoted()) {
$prototype['arguments'][$parameter->getName()]['visibility'] = 'protected';
} elseif ($parameter->isPrivatePromoted()) {
$prototype['arguments'][$parameter->getName()]['visibility'] = 'private';
}
}
}

if ($format == self::PROTOTYPE_AS_STRING) {
$line = $prototype['visibility'] . ' ' . $prototype['return'] . ' ' . $prototype['name'] . '(';
$args = [];
foreach ($prototype['arguments'] as $name => $argument) {
$argsLine = ($argument['type'] ?
$argument['type'] . ' '
: '') . ($argument['by_ref'] ? '&' : '') . '$' . $name;
$argsLine =
(
array_key_exists('visibility', $argument)
? $argument['visibility'] . ' '
: ''
) . (
$argument['type']
? $argument['type'] . ' '
: ''
) . (
$argument['by_ref']
? '&'
: ''
) . '$' . $name;
if (! $argument['required']) {
$argsLine .= ' = ' . var_export($argument['default'], true);
}
Expand Down
38 changes: 38 additions & 0 deletions src/Reflection/ParameterReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use ReturnTypeWillChange;

use function method_exists;
Expand Down Expand Up @@ -130,4 +131,41 @@ public function __toString()
{
return parent::__toString();
}

/** @psalm-pure */
public function isPromoted(): bool
{
if (! method_exists(parent::class, 'isPromoted')) {
return false;
}

return (bool) parent::isPromoted();
}

public function isPublicPromoted(): bool
{
return $this->isPromoted()
&& $this->getDeclaringClass()
->getProperty($this->getName())
->getModifiers()
& ReflectionProperty::IS_PUBLIC;
}

public function isProtectedPromoted(): bool
{
return $this->isPromoted()
&& $this->getDeclaringClass()
->getProperty($this->getName())
->getModifiers()
& ReflectionProperty::IS_PROTECTED;
}

public function isPrivatePromoted(): bool
{
return $this->isPromoted()
&& $this->getDeclaringClass()
->getProperty($this->getName())
->getModifiers()
& ReflectionProperty::IS_PRIVATE;
}
}
66 changes: 66 additions & 0 deletions test/Generator/ClassGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use Laminas\Code\Generator\Exception\InvalidArgumentException;
use Laminas\Code\Generator\GeneratorInterface;
use Laminas\Code\Generator\MethodGenerator;
use Laminas\Code\Generator\PromotedParameterGenerator;
use Laminas\Code\Generator\PropertyGenerator;
use Laminas\Code\Reflection\ClassReflection;
use LaminasTest\Code\Generator\TestAsset\ClassWithPromotedParameter;
use LaminasTest\Code\TestAsset\FooClass;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
Expand Down Expand Up @@ -1356,4 +1358,68 @@ class ClassWithFinalConst
$output = $classGenerator->generate();
self::assertSame($expectedOutput, $output, $output);
}

/** @requires PHP >= 8.0 */
public function testGenerateClassWithPromotedConstructorParameter(): void
{
$classGenerator = new ClassGenerator();
$classGenerator->setName('ClassWithPromotedParameter');

$classGenerator->addMethod('__construct', [
new PromotedParameterGenerator(
'bar',
'Foo',
PromotedParameterGenerator::VISIBILITY_PRIVATE,
),
]);

$expectedOutput = <<<EOS
class ClassWithPromotedParameter
{
public function __construct(private \Foo \$bar)
{
}
}
EOS;

self::assertEquals($expectedOutput, $classGenerator->generate());
}

/** @requires PHP >= 8.0 */
public function testClassWithPromotedParameterFromReflection(): void
{
$classGenerator = ClassGenerator::fromReflection(
new ClassReflection(ClassWithPromotedParameter::class)
);

$expectedOutput = <<<EOS
namespace LaminasTest\Code\Generator\TestAsset;
class ClassWithPromotedParameter
{
public function __construct(private string \$promotedParameter)
{
}
}
EOS;

self::assertEquals($expectedOutput, $classGenerator->generate());
}

/** @requires PHP >= 8.0 */
public function testFailToGenerateClassWithPromotedParameterOnNonConstructorMethod(): void
{
$classGenerator = new ClassGenerator();
$classGenerator->setName('promotedParameterOnNonConstructorMethod');

$this->expectExceptionObject(
new InvalidArgumentException('Promoted parameter can only be added to constructor.')
);

$classGenerator->addMethod('thisIsNoConstructor', [
new PromotedParameterGenerator('promotedParameter', 'string'),
]);
}
}
15 changes: 15 additions & 0 deletions test/Generator/MethodGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ protected function withParamsAndReturnType($mixed, array $array, ?callable $call
{
}

EOS;
self::assertSame($target, (string) $methodGenerator);
}

/** @requires PHP >= 8.0 */
public function testCopyMethodSignatureForPromotedParameter(): void
{
$ref = new MethodReflection(TestAsset\ClassWithPromotedParameter::class, '__construct');

$methodGenerator = MethodGenerator::copyMethodSignature($ref);
$target = <<<'EOS'
public function __construct(private string $promotedParameter)
{
}

EOS;
self::assertSame($target, (string) $methodGenerator);
}
Expand Down
11 changes: 11 additions & 0 deletions test/Generator/TestAsset/ClassWithPromotedParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Code\Generator\TestAsset;

final class ClassWithPromotedParameter
{
public function __construct(private string $promotedParameter) {
}
}
Loading

0 comments on commit 793d9dd

Please sign in to comment.