Skip to content

Commit

Permalink
Interfaces can have properties
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Nov 28, 2024
1 parent fe94009 commit eb3a488
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 34 deletions.
35 changes: 20 additions & 15 deletions src/PhpGenerator/ClassManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,28 @@ public function __construct(
*/
public function inheritProperty(string $name, bool $returnIfExists = false): Property
{
$extends = $this->class->getExtends();
if ($this->class->hasProperty($name)) {
return $returnIfExists
? $this->class->getProperty($name)
: throw new Nette\InvalidStateException("Cannot inherit property '$name', because it already exists.");

} elseif (!$extends) {
throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has not setExtends() set.");
}

try {
$rp = new \ReflectionProperty($extends, $name);
} catch (\ReflectionException) {
throw new Nette\InvalidStateException("Property '$name' has not been found in ancestor {$extends}");
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");

foreach ($parents as $parent) {
try {
$rp = new \ReflectionProperty($parent, $name);
} catch (\ReflectionException) {
continue;
}
$property = (new Factory)->fromPropertyReflection($rp);
$this->class->addMember($property);
$property->setHooks([]);
return $property;
}

$property = (new Factory)->fromPropertyReflection($rp);
$this->class->addMember($property);
return $property;
throw new Nette\InvalidStateException("Property '$name' has not been found in any ancestor: " . implode(', ', $parents));
}


Expand All @@ -52,16 +55,15 @@ public function inheritProperty(string $name, bool $returnIfExists = false): Pro
*/
public function inheritMethod(string $name, bool $returnIfExists = false): Method
{
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()];
if ($this->class->hasMethod($name)) {
return $returnIfExists
? $this->class->getMethod($name)
: throw new Nette\InvalidStateException("Cannot inherit method '$name', because it already exists.");

} elseif (!$parents) {
throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
}

$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");

foreach ($parents as $parent) {
try {
$rm = new \ReflectionMethod($parent, $name);
Expand Down Expand Up @@ -91,5 +93,8 @@ public function implementInterface(string $interfaceName): void
foreach ($interface->getMethods() as $method) {
$this->inheritMethod($method->getName(), returnIfExists: true);
}
foreach ($interface->getProperties() as $property) {
$this->inheritProperty($property->getName(), returnIfExists: true);
}
}
}
18 changes: 17 additions & 1 deletion src/PhpGenerator/InterfaceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class InterfaceType extends ClassLike
{
use Traits\ConstantsAware;
use Traits\MethodsAware;
use Traits\PropertiesAware;

/** @var string[] */
private array $extends = [];
Expand Down Expand Up @@ -54,12 +55,13 @@ public function addExtend(string $name): static
/**
* Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addMember(Method|Constant $member, bool $overwrite = false): static
public function addMember(Method|Constant|Property $member, bool $overwrite = false): static
{
$name = $member->getName();
[$type, $n] = match (true) {
$member instanceof Constant => ['consts', $name],
$member instanceof Method => ['methods', strtolower($name)],
$member instanceof Property => ['properties', $name],
};
if (!$overwrite && isset($this->$type[$n])) {
throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists.");
Expand All @@ -69,11 +71,25 @@ public function addMember(Method|Constant $member, bool $overwrite = false): sta
}


/** @throws Nette\InvalidStateException */
public function validate(): void
{
foreach ($this->getProperties() as $property) {
if ($property->isInitialized()) {
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have initialized properties.");
} elseif (!$property->getHooks()) {
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have properties without hooks.");
}
}
}


public function __clone(): void
{
parent::__clone();
$clone = fn($item) => clone $item;
$this->consts = array_map($clone, $this->consts);
$this->methods = array_map($clone, $this->methods);
$this->properties = array_map($clone, $this->properties);
}
}
35 changes: 21 additions & 14 deletions src/PhpGenerator/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ public function printClass(
}

$properties = [];
if ($class instanceof ClassType || $class instanceof TraitType) {
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof InterfaceType) {
foreach ($class->getProperties() as $property) {
$properties[] = $this->printProperty($property, $readOnlyClass);
$properties[] = $this->printProperty($property, $readOnlyClass, $class instanceof InterfaceType);
}
}

Expand Down Expand Up @@ -376,7 +376,7 @@ private function printConstant(Constant $const): string
}


private function printProperty(Property $property, bool $readOnlyClass = false): string
private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string
{
$property->validate();
$type = $property->getType();
Expand All @@ -395,7 +395,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false):
. $this->printAttributes($property->getAttributes())
. $def
. $defaultValue
. ($this->printHooks($property) ?: ';')
. ($this->printHooks($property, $isInterface) ?: ';')
. "\n";
}

Expand Down Expand Up @@ -456,27 +456,34 @@ protected function printAttributes(array $attrs, bool $inline = false): string
}


private function printHooks(Property|PromotedParameter $property): string
private function printHooks(Property|PromotedParameter $property, bool $isInterface = false): string
{
$hooks = $property->getHooks();
if (!$hooks) {
return '';
}

$simple = true;
foreach ($property->getHooks() as $type => $hook) {
$simple = $simple && ($hook->isAbstract() || $isInterface);
$hooks[$type] = $this->printDocComment($hook)
. $this->printAttributes($hook->getAttributes())
. ($hook->isFinal() ? 'final ' : '')
. ($hook->getReturnReference() ? '&' : '')
. $type
. ($hook->getParameters() ? $this->printParameters($hook) : '')
. ' '
. ($hook->isShort()
? '=> ' . $hook->getBody() . ';'
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}');
. ($hook->isAbstract() || $isInterface
? ($hook->getReturnReference() ? '&' : '')
. $type . ';'
: ($hook->isFinal() ? 'final ' : '')
. ($hook->getReturnReference() ? '&' : '')
. $type
. ($hook->getParameters() ? $this->printParameters($hook) : '')
. ' '
. ($hook->isShort()
? '=> ' . $hook->getBody() . ';'
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}'));
}

return " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
return $simple
? ' { ' . implode(' ', $hooks) . ' }'
: " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
}


Expand Down
14 changes: 14 additions & 0 deletions src/PhpGenerator/PropertyHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class PropertyHook
private string $body = '';
private bool $short = false;
private bool $final = false;
private bool $abstract = false;

/** @var Parameter[] */
private array $parameters = [];
Expand Down Expand Up @@ -62,6 +63,19 @@ public function isFinal(): bool
}


public function setAbstract(bool $state = true): static
{
$this->abstract = $state;
return $this;
}


public function isAbstract(): bool
{
return $this->abstract;
}


/** @internal */
public function setParameters(array $val): static
{
Expand Down
46 changes: 46 additions & 0 deletions tests/PhpGenerator/ClassManipulator.implementInterface.84.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* @phpVersion 8.4
*/

declare(strict_types=1);

use Nette\PhpGenerator\ClassManipulator;
use Nette\PhpGenerator\ClassType;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


interface TestInterface
{
public array $bar { get; }

public function testMethod();
}

$class = new ClassType('TestClass');
$manipulator = new ClassManipulator($class);

// Test valid interface implementation
$manipulator->implementInterface(TestInterface::class);
Assert::match(<<<'XX'
class TestClass implements TestInterface
{
public array $bar;
function testMethod()
{
}
}

XX, (string) $class);


// Test exception for non-interface
Assert::exception(
fn() => $manipulator->implementInterface(stdClass::class),
InvalidArgumentException::class,
);
12 changes: 10 additions & 2 deletions tests/PhpGenerator/ClassManipulator.implementInterface.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ $manipulator = new ClassManipulator($class);

// Test valid interface implementation
$manipulator->implementInterface(TestInterface::class);
Assert::true(in_array(TestInterface::class, $class->getImplements(), true));
Assert::true($class->hasMethod('testMethod'));
Assert::match(<<<'XX'
class TestClass implements TestInterface
{
function testMethod()
{
}
}

XX, (string) $class);


// Test exception for non-interface
Assert::exception(
Expand Down
4 changes: 2 additions & 2 deletions tests/PhpGenerator/ClassManipulator.inheritProperty.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ $manipulator = new ClassManipulator($class);
Assert::exception(
fn() => $manipulator->inheritProperty('bar'),
Nette\InvalidStateException::class,
"Class 'Test' has not setExtends() set.",
"Class 'Test' has neither setExtends() nor setImplements() set.",
);

$class->setExtends('Unknown');
Assert::exception(
fn() => $manipulator->inheritProperty('bar'),
Nette\InvalidStateException::class,
"Property 'bar' has not been found in ancestor Unknown",
"Property 'bar' has not been found in any ancestor: Unknown",
);


Expand Down
21 changes: 21 additions & 0 deletions tests/PhpGenerator/InterfaceType.validate.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

use Nette\PhpGenerator\InterfaceType;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


Assert::exception(function () {
$interface = new InterfaceType('Demo');
$interface->addProperty('first', 123);
$interface->validate();
}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have initialized properties.');

Assert::exception(function () {
$interface = new InterfaceType('Demo');
$interface->addProperty('first');
$interface->validate();
}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have properties without hooks.');
25 changes: 25 additions & 0 deletions tests/PhpGenerator/PropertyLike.hooks.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
declare(strict_types=1);

use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\InterfaceType;

require __DIR__ . '/../bootstrap.php';

Expand Down Expand Up @@ -96,3 +97,27 @@ same(<<<'XX'
}

XX, (string) $class);


$interface = new InterfaceType('Demo');

$interface->addProperty('first')
->setType('int')
->setPublic()
->addHook('get');

$prop = $interface->addProperty('second')
->setType('Value')
->setPublic();

$prop->addHook('get');
$prop->addHook('set');

same(<<<'XX'
interface Demo
{
public int $first { get; }
public Value $second { set; get; }
}

XX, (string) $interface);

0 comments on commit eb3a488

Please sign in to comment.