diff --git a/src/PhpGenerator/ClassManipulator.php b/src/PhpGenerator/ClassManipulator.php index d97ee319..e34b69e4 100644 --- a/src/PhpGenerator/ClassManipulator.php +++ b/src/PhpGenerator/ClassManipulator.php @@ -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)); } @@ -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); @@ -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); + } } } diff --git a/src/PhpGenerator/InterfaceType.php b/src/PhpGenerator/InterfaceType.php index 8743f8d0..a47225d0 100644 --- a/src/PhpGenerator/InterfaceType.php +++ b/src/PhpGenerator/InterfaceType.php @@ -19,6 +19,7 @@ final class InterfaceType extends ClassLike { use Traits\ConstantsAware; use Traits\MethodsAware; + use Traits\PropertiesAware; /** @var string[] */ private array $extends = []; @@ -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."); @@ -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); } } diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index ee22c9b4..113665fa 100644 --- a/src/PhpGenerator/Printer.php +++ b/src/PhpGenerator/Printer.php @@ -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); } } @@ -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(); @@ -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"; } @@ -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}"; } diff --git a/src/PhpGenerator/PropertyHook.php b/src/PhpGenerator/PropertyHook.php index c14b9d53..59b87a1f 100644 --- a/src/PhpGenerator/PropertyHook.php +++ b/src/PhpGenerator/PropertyHook.php @@ -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 = []; @@ -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 { diff --git a/tests/PhpGenerator/ClassManipulator.implementInterface.84.phpt b/tests/PhpGenerator/ClassManipulator.implementInterface.84.phpt new file mode 100644 index 00000000..bcfd9cd3 --- /dev/null +++ b/tests/PhpGenerator/ClassManipulator.implementInterface.84.phpt @@ -0,0 +1,46 @@ +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, +); diff --git a/tests/PhpGenerator/ClassManipulator.implementInterface.phpt b/tests/PhpGenerator/ClassManipulator.implementInterface.phpt index a0e18e52..706a26ea 100644 --- a/tests/PhpGenerator/ClassManipulator.implementInterface.phpt +++ b/tests/PhpGenerator/ClassManipulator.implementInterface.phpt @@ -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( diff --git a/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt b/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt index d9b9bf1b..95d533aa 100644 --- a/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt +++ b/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt @@ -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", ); diff --git a/tests/PhpGenerator/InterfaceType.validate.phpt b/tests/PhpGenerator/InterfaceType.validate.phpt new file mode 100644 index 00000000..b036e425 --- /dev/null +++ b/tests/PhpGenerator/InterfaceType.validate.phpt @@ -0,0 +1,21 @@ +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.'); diff --git a/tests/PhpGenerator/PropertyLike.hooks.phpt b/tests/PhpGenerator/PropertyLike.hooks.phpt index 9e564981..43796d3d 100644 --- a/tests/PhpGenerator/PropertyLike.hooks.phpt +++ b/tests/PhpGenerator/PropertyLike.hooks.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\InterfaceType; require __DIR__ . '/../bootstrap.php'; @@ -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);