Skip to content

Commit

Permalink
Merge pull request #112 from michaelpetri/feature/add-readonly-proper…
Browse files Browse the repository at this point in the history
…ties

Added support for readonly properties
  • Loading branch information
Ocramius authored Nov 20, 2021
2 parents a98efae + e7730fe commit 4746c4d
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 20 deletions.
13 changes: 6 additions & 7 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,8 @@
<MixedArgumentTypeCoercion occurrences="1">
<code>$name</code>
</MixedArgumentTypeCoercion>
<MixedAssignment occurrences="2">
<MixedAssignment occurrences="1">
<code>$defaultValue</code>
<code>$value</code>
</MixedAssignment>
<PossiblyFalseArgument occurrences="1">
<code>$reflectionProperty-&gt;getDocBlock()</code>
Expand Down Expand Up @@ -1585,8 +1584,7 @@
</PossiblyNullArgument>
</file>
<file src="test/Generator/PropertyGeneratorTest.php">
<InvalidArgument occurrences="2">
<code>[PropertyGenerator::FLAG_CONSTANT, $flag]</code>
<InvalidArgument occurrences="1">
<code>new stdClass()</code>
</InvalidArgument>
<InvalidReturnStatement occurrences="1"/>
Expand All @@ -1596,6 +1594,9 @@
<MissingReturnType occurrences="1">
<code>testOmitType</code>
</MissingReturnType>
<MixedAssignment occurrences="1">
<code>$generator</code>
</MixedAssignment>
<MixedInferredReturnType occurrences="1">
<code>Generator</code>
</MixedInferredReturnType>
Expand Down Expand Up @@ -1686,11 +1687,9 @@
</UnusedVariable>
</file>
<file src="test/Generator/TestAsset/TestClassWithManyProperties.php">
<MissingPropertyType occurrences="6">
<MissingPropertyType occurrences="4">
<code>$_barProperty</code>
<code>$_barStaticProperty</code>
<code>$_bazProperty</code>
<code>$_bazStaticProperty</code>
<code>$_complexType</code>
<code>$fooStaticProperty</code>
</MissingPropertyType>
Expand Down
2 changes: 1 addition & 1 deletion src/Generator/AbstractMemberGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public function setVisibility($visibility)
}

/**
* @return string
* @psalm-return static::VISIBILITY_*
*/
public function getVisibility()
{
Expand Down
88 changes: 77 additions & 11 deletions src/Generator/PropertyGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@

use Laminas\Code\Reflection\PropertyReflection;

use function array_reduce;
use function get_class;
use function gettype;
use function is_bool;
use function is_object;
use function method_exists;
use function sprintf;
use function str_replace;
use function strtolower;

class PropertyGenerator extends AbstractMemberGenerator
{
public const FLAG_CONSTANT = 0x08;
public const FLAG_READONLY = 0x80;

protected bool $isConst = false;

protected ?PropertyValueGenerator $defaultValue = null;

private bool $omitDefaultValue = false;

/**
* @return static
*/
/** @return static */
public static function fromReflection(PropertyReflection $reflectionProperty)
{
$property = new static();
Expand All @@ -43,6 +48,10 @@ public static function fromReflection(PropertyReflection $reflectionProperty)
$property->setStatic(true);
}

if (method_exists($reflectionProperty, 'isReadonly') && $reflectionProperty->isReadonly()) {
$property->setReadonly(true);
}

if ($reflectionProperty->isPrivate()) {
$property->setVisibility(self::VISIBILITY_PRIVATE);
} elseif ($reflectionProperty->isProtected()) {
Expand All @@ -68,6 +77,7 @@ public static function fromReflection(PropertyReflection $reflectionProperty)
* @configkey static bool
* @configkey visibility string
* @configkey omitdefaultvalue bool
* @configkey readonly bool
* @throws Exception\InvalidArgumentException
* @param array $array
* @return static
Expand Down Expand Up @@ -112,19 +122,34 @@ public static function fromArray(array $array)
case 'omitdefaultvalue':
$property->omitDefaultValue($value);
break;
case 'readonly':
if (! is_bool($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s is expecting boolean on key %s. Got %s',
__METHOD__,
$name,
is_object($value)
? get_class($value)
: gettype($value)
));
}

$property->setReadonly($value);
break;
}
}

return $property;
}

/**
* @param ?string $name
* @param PropertyValueGenerator|string|array|null $defaultValue
* @param int $flags
* @param int|int[] $flags
*/
public function __construct($name = null, $defaultValue = null, $flags = self::FLAG_PUBLIC)
public function __construct(?string $name = null, $defaultValue = null, $flags = self::FLAG_PUBLIC)
{
parent::__construct();

if (null !== $name) {
$this->setName($name);
}
Expand All @@ -142,12 +167,12 @@ public function __construct($name = null, $defaultValue = null, $flags = self::F
*/
public function setConst($const)
{
if ($const) {
if (true === $const) {
$this->setFlags(self::FLAG_CONSTANT);
} else {
$this->removeFlag(self::FLAG_CONSTANT);
return $this;
}

$this->removeFlag(self::FLAG_CONSTANT);
return $this;
}

Expand All @@ -159,11 +184,47 @@ public function isConst()
return (bool) ($this->flags & self::FLAG_CONSTANT);
}

public function setReadonly(bool $readonly): self
{
if (true === $readonly) {
$this->setFlags(self::FLAG_READONLY);
return $this;
}

$this->removeFlag(self::FLAG_READONLY);
return $this;
}

public function isReadonly(): bool
{
return (bool) ($this->flags & self::FLAG_READONLY);
}

/**
* {@inheritDoc}
*/
public function setFlags($flags)
{
$flags = array_reduce((array) $flags, static function (int $a, int $b): int {
return $a | $b;
}, 0);

if ($flags & self::FLAG_READONLY && $flags & self::FLAG_STATIC) {
throw new Exception\RuntimeException('Modifier "readonly" in combination with "static" not permitted.');
}

if ($flags & self::FLAG_READONLY && $flags & self::FLAG_CONSTANT) {
throw new Exception\RuntimeException('Modifier "readonly" in combination with "constant" not permitted.');
}

return parent::setFlags($flags);
}

/**
* @param PropertyValueGenerator|mixed $defaultValue
* @param string $defaultValueType
* @param string $defaultValueOutputMode
* @return $this
* @return static
*/
public function setDefaultValue(
$defaultValue,
Expand All @@ -190,6 +251,7 @@ public function getDefaultValue()
/**
* @throws Exception\RuntimeException
* @return string
* @psalm-return non-empty-string
*/
public function generate()
{
Expand Down Expand Up @@ -220,7 +282,11 @@ public function generate()
. ($defaultValue !== null ? $defaultValue->generate() : 'null;');
}

$output .= $this->indentation . $this->getVisibility() . ($this->isStatic() ? ' static' : '') . ' $' . $name;
$output .= $this->indentation
. $this->getVisibility()
. ($this->isReadonly() ? ' readonly' : '')
. ($this->isStatic() ? ' static' : '')
. ' $' . $name;

if ($this->omitDefaultValue) {
return $output . ';';
Expand Down
73 changes: 72 additions & 1 deletion test/Generator/PropertyGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
use Laminas\Code\Reflection\ClassReflection;
use Laminas\Code\Reflection\PropertyReflection;
use LaminasTest\Code\Generator\TestAsset\ClassWithTypedProperty;
use PHP_CodeSniffer\Tokenizers\PHP;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use stdClass;

use function array_shift;
use function str_replace;
use function uniqid;

/**
* @group Laminas_Code_Generator
Expand Down Expand Up @@ -150,10 +152,47 @@ public function testPropertyCanProduceFinalConstantModifier(): void
self::assertSame(' final public const someVal = \'some string value\';', $codeGenProperty->generate());
}

/**
* @dataProvider visibility
*/
public function testPropertyCanProduceReadonlyModifier(int $flag, string $visibility): void
{
$codeGenProperty = new PropertyGenerator(
'someVal',
'some string value',
PropertyGenerator::FLAG_READONLY | $flag
);

self::assertSame(
' ' . $visibility . ' readonly $someVal = \'some string value\';',
$codeGenProperty->generate()
);
}

public function testFailToProduceReadonlyStatic(): void
{
$codeGenProperty = new PropertyGenerator('someVal', 'some string value');

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Modifier "readonly" in combination with "static" not permitted.');

$codeGenProperty->setFlags(PropertyGenerator::FLAG_READONLY | PropertyGenerator::FLAG_STATIC);
}

public function testFailToProduceReadonlyConstant(): void
{
$codeGenProperty = new PropertyGenerator('someVal', 'some string value');

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Modifier "readonly" in combination with "constant" not permitted.');

$codeGenProperty->setFlags(PropertyGenerator::FLAG_READONLY | PropertyGenerator::FLAG_CONSTANT);
}

/**
* @group PR-704
*/
public function testPropertyCanProduceContstantModifierWithSetter(): void
public function testPropertyCanProduceConstantModifierWithSetter(): void
{
$codeGenProperty = new PropertyGenerator('someVal', 'some string value');
$codeGenProperty->setConst(true);
Expand Down Expand Up @@ -260,6 +299,7 @@ public function testCreateFromArray(): void

self::assertSame('SampleProperty', $propertyGenerator->getName());
self::assertFalse($propertyGenerator->isConst());
self::assertFalse($propertyGenerator->isReadonly());
self::assertInstanceOf(ValueGenerator::class, $propertyGenerator->getDefaultValue());
self::assertInstanceOf(DocBlockGenerator::class, $propertyGenerator->getDocBlock());
self::assertTrue($propertyGenerator->isAbstract());
Expand All @@ -275,6 +315,22 @@ public function testCreateFromArray(): void
self::assertTrue($reflectionOmitDefaultValue->getValue($propertyGenerator));
}

public function testCreateReadonlyFromArray(): void
{
$propertyGenerator = PropertyGenerator::fromArray([
'name' => 'ReadonlyProperty',
'readonly' => true,
]);

self::assertSame('ReadonlyProperty', $propertyGenerator->getName());
self::assertFalse($propertyGenerator->isConst());
self::assertFalse($propertyGenerator->isAbstract());
self::assertFalse($propertyGenerator->isFinal());
self::assertFalse($propertyGenerator->isStatic());
self::assertTrue($propertyGenerator->isReadonly());
self::assertSame(PropertyGenerator::VISIBILITY_PUBLIC, $propertyGenerator->getVisibility());
}

/**
* @group 3491
*/
Expand Down Expand Up @@ -340,4 +396,19 @@ public function testFromReflectionOmitsTypeHintInTypedProperty(): void

self::assertSame(' private $typedProperty;', $code);
}

/** @requires PHP >= 8.1 */
public function testFromReflectionReadonlyProperty(): void
{
$className = uniqid('ClassWithReadonlyProperty', false);

eval('namespace ' . __NAMESPACE__ . '; class ' . $className . '{ public readonly string $readonly; }');

$reflectionProperty = new PropertyReflection(__NAMESPACE__ . '\\' . $className, 'readonly');

$generator = PropertyGenerator::fromReflection($reflectionProperty);
$code = $generator->generate();

self::assertSame(' public readonly $readonly;', $code);
}
}

0 comments on commit 4746c4d

Please sign in to comment.