Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for PHP 8.0 promoted parameters #114

Merged
merged 1 commit into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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