Skip to content

Commit 466918d

Browse files
committed
Interfaces can have properties
1 parent 5126fb8 commit 466918d

9 files changed

+216
-33
lines changed

src/PhpGenerator/ClassManipulator.php

+31-15
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,25 @@ public function __construct(
2525
*/
2626
public function inheritProperty(string $name, bool $returnIfExists = false): Property
2727
{
28-
$extends = $this->class->getExtends();
2928
if ($this->class->hasProperty($name)) {
3029
return $returnIfExists
3130
? $this->class->getProperty($name)
3231
: throw new Nette\InvalidStateException("Cannot inherit property '$name', because it already exists.");
33-
34-
} elseif (!$extends) {
35-
throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has not setExtends() set.");
3632
}
3733

38-
try {
39-
$rp = new \ReflectionProperty($extends, $name);
40-
} catch (\ReflectionException) {
41-
throw new Nette\InvalidStateException("Property '$name' has not been found in ancestor {$extends}");
34+
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
35+
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
36+
37+
foreach ($parents as $parent) {
38+
try {
39+
$rp = new \ReflectionProperty($parent, $name);
40+
} catch (\ReflectionException) {
41+
continue;
42+
}
43+
return $this->implementProperty($rp);
4244
}
4345

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

4949

@@ -52,16 +52,15 @@ public function inheritProperty(string $name, bool $returnIfExists = false): Pro
5252
*/
5353
public function inheritMethod(string $name, bool $returnIfExists = false): Method
5454
{
55-
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()];
5655
if ($this->class->hasMethod($name)) {
5756
return $returnIfExists
5857
? $this->class->getMethod($name)
5958
: throw new Nette\InvalidStateException("Cannot inherit method '$name', because it already exists.");
60-
61-
} elseif (!$parents) {
62-
throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
6359
}
6460

61+
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
62+
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
63+
6564
foreach ($parents as $parent) {
6665
try {
6766
$rm = new \ReflectionMethod($parent, $name);
@@ -94,6 +93,14 @@ public function implement(string $name): void
9493
$this->implementMethod($method);
9594
}
9695
}
96+
97+
if (PHP_VERSION_ID >= 80400) {
98+
foreach ($definition->getProperties() as $property) {
99+
if (!$this->class->hasProperty($property->getName()) && $property->isAbstract()) {
100+
$this->implementProperty($property);
101+
}
102+
}
103+
}
97104
}
98105

99106

@@ -106,6 +113,15 @@ private function implementMethod(\ReflectionMethod $rm): Method
106113
}
107114

108115

116+
private function implementProperty(\ReflectionProperty $rp): Property
117+
{
118+
$property = (new Factory)->fromPropertyReflection($rp);
119+
$property->setHooks([]);
120+
$this->class->addMember($property);
121+
return $property;
122+
}
123+
124+
109125
/** @deprecated use implement() */
110126
public function implementInterface(string $interfaceName): void
111127
{

src/PhpGenerator/InterfaceType.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414

1515
/**
16-
* Definition of an interface with methods and constants.
16+
* Definition of an interface with properties, methods and constants.
1717
*/
1818
final class InterfaceType extends ClassLike
1919
{
2020
use Traits\ConstantsAware;
2121
use Traits\MethodsAware;
22+
use Traits\PropertiesAware;
2223

2324
/** @var string[] */
2425
private array $extends = [];
@@ -54,12 +55,13 @@ public function addExtend(string $name): static
5455
/**
5556
* Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true.
5657
*/
57-
public function addMember(Method|Constant $member, bool $overwrite = false): static
58+
public function addMember(Method|Constant|Property $member, bool $overwrite = false): static
5859
{
5960
$name = $member->getName();
6061
[$type, $n] = match (true) {
6162
$member instanceof Constant => ['consts', $name],
6263
$member instanceof Method => ['methods', strtolower($name)],
64+
$member instanceof Property => ['properties', $name],
6365
};
6466
if (!$overwrite && isset($this->$type[$n])) {
6567
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
6971
}
7072

7173

74+
/** @throws Nette\InvalidStateException */
75+
public function validate(): void
76+
{
77+
foreach ($this->getProperties() as $property) {
78+
if ($property->isInitialized()) {
79+
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have initialized properties.");
80+
} elseif (!$property->getHooks()) {
81+
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have properties without hooks.");
82+
}
83+
}
84+
}
85+
86+
7287
public function __clone(): void
7388
{
7489
parent::__clone();
7590
$clone = fn($item) => clone $item;
7691
$this->consts = array_map($clone, $this->consts);
7792
$this->methods = array_map($clone, $this->methods);
93+
$this->properties = array_map($clone, $this->properties);
7894
}
7995
}

src/PhpGenerator/Printer.php

+21-14
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,9 @@ public function printClass(
198198
}
199199

200200
$properties = [];
201-
if ($class instanceof ClassType || $class instanceof TraitType) {
201+
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof InterfaceType) {
202202
foreach ($class->getProperties() as $property) {
203-
$properties[] = $this->printProperty($property, $readOnlyClass);
203+
$properties[] = $this->printProperty($property, $readOnlyClass, $class instanceof InterfaceType);
204204
}
205205
}
206206

@@ -376,7 +376,7 @@ private function printConstant(Constant $const): string
376376
}
377377

378378

379-
private function printProperty(Property $property, bool $readOnlyClass = false): string
379+
private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string
380380
{
381381
$property->validate();
382382
$type = $property->getType();
@@ -395,7 +395,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false):
395395
. $this->printAttributes($property->getAttributes())
396396
. $def
397397
. $defaultValue
398-
. ($this->printHooks($property) ?: ';')
398+
. ($this->printHooks($property, $isInterface) ?: ';')
399399
. "\n";
400400
}
401401

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

458458

459-
private function printHooks(Property|PromotedParameter $property): string
459+
private function printHooks(Property|PromotedParameter $property, bool $isInterface = false): string
460460
{
461461
$hooks = $property->getHooks();
462462
if (!$hooks) {
463463
return '';
464464
}
465465

466+
$simple = true;
466467
foreach ($property->getHooks() as $type => $hook) {
468+
$simple = $simple && ($hook->isAbstract() || $isInterface);
467469
$hooks[$type] = $this->printDocComment($hook)
468470
. $this->printAttributes($hook->getAttributes())
469-
. ($hook->isFinal() ? 'final ' : '')
470-
. ($hook->getReturnReference() ? '&' : '')
471-
. $type
472-
. ($hook->getParameters() ? $this->printParameters($hook) : '')
473-
. ' '
474-
. ($hook->isShort()
475-
? '=> ' . $hook->getBody() . ';'
476-
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}');
471+
. ($hook->isAbstract() || $isInterface
472+
? ($hook->getReturnReference() ? '&' : '')
473+
. $type . ';'
474+
: ($hook->isFinal() ? 'final ' : '')
475+
. ($hook->getReturnReference() ? '&' : '')
476+
. $type
477+
. ($hook->getParameters() ? $this->printParameters($hook) : '')
478+
. ' '
479+
. ($hook->isShort()
480+
? '=> ' . $hook->getBody() . ';'
481+
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}'));
477482
}
478483

479-
return " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
484+
return $simple
485+
? ' { ' . implode(' ', $hooks) . ' }'
486+
: " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
480487
}
481488

482489

src/PhpGenerator/PropertyHook.php

+14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ final class PropertyHook
1818
private string $body = '';
1919
private bool $short = false;
2020
private bool $final = false;
21+
private bool $abstract = false;
2122

2223
/** @var Parameter[] */
2324
private array $parameters = [];
@@ -65,6 +66,19 @@ public function isFinal(): bool
6566
}
6667

6768

69+
public function setAbstract(bool $state = true): static
70+
{
71+
$this->abstract = $state;
72+
return $this;
73+
}
74+
75+
76+
public function isAbstract(): bool
77+
{
78+
return $this->abstract;
79+
}
80+
81+
6882
/** @internal */
6983
public function setParameters(array $val): static
7084
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/**
4+
* @phpVersion 8.4
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\PhpGenerator\ClassManipulator;
10+
use Nette\PhpGenerator\ClassType;
11+
use Tester\Assert;
12+
13+
require __DIR__ . '/../bootstrap.php';
14+
15+
16+
interface ParentInterface
17+
{
18+
public array $interfaceProperty { get; }
19+
public function interfaceMethod();
20+
}
21+
22+
interface TestInterface extends ParentInterface
23+
{
24+
}
25+
26+
abstract class ParentAbstract
27+
{
28+
abstract public array $abstractProperty { get; }
29+
public array $concreteProperty;
30+
abstract public function abstractMethod();
31+
public function concreteMethod() {}
32+
}
33+
34+
abstract class TestAbstract extends ParentAbstract
35+
{
36+
}
37+
38+
39+
$class = new ClassType('TestClass');
40+
$manipulator = new ClassManipulator($class);
41+
42+
// Test interface implementation
43+
$manipulator->implement(TestInterface::class);
44+
Assert::match(<<<'XX'
45+
class TestClass implements TestInterface
46+
{
47+
public array $interfaceProperty;
48+
49+
50+
function interfaceMethod()
51+
{
52+
}
53+
}
54+
55+
XX, (string) $class);
56+
57+
58+
// Test abstract class extension
59+
$class = new ClassType('TestClass');
60+
$manipulator = new ClassManipulator($class);
61+
$manipulator->implement(TestAbstract::class);
62+
Assert::match(<<<'XX'
63+
class TestClass extends TestAbstract
64+
{
65+
public array $abstractProperty;
66+
67+
68+
public function abstractMethod()
69+
{
70+
}
71+
}
72+
73+
XX, (string) $class);
74+
75+
76+
// Test exception for regular class
77+
Assert::exception(
78+
fn() => $manipulator->implement(stdClass::class),
79+
InvalidArgumentException::class,
80+
"'stdClass' is not an interface or abstract class."
81+
);

tests/PhpGenerator/ClassManipulator.implement.phpt

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ interface TestInterface extends ParentInterface
2020

2121
abstract class ParentAbstract
2222
{
23+
public array $concreteProperty;
24+
25+
2326
abstract public function abstractMethod();
2427

2528

tests/PhpGenerator/ClassManipulator.inheritProperty.phpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ $manipulator = new ClassManipulator($class);
2121
Assert::exception(
2222
fn() => $manipulator->inheritProperty('bar'),
2323
Nette\InvalidStateException::class,
24-
"Class 'Test' has not setExtends() set.",
24+
"Class 'Test' has neither setExtends() nor setImplements() set.",
2525
);
2626

2727
$class->setExtends('Unknown');
2828
Assert::exception(
2929
fn() => $manipulator->inheritProperty('bar'),
3030
Nette\InvalidStateException::class,
31-
"Property 'bar' has not been found in ancestor Unknown",
31+
"Property 'bar' has not been found in any ancestor: Unknown",
3232
);
3333

3434

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\PhpGenerator\InterfaceType;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
Assert::exception(function () {
12+
$interface = new InterfaceType('Demo');
13+
$interface->addProperty('first', 123);
14+
$interface->validate();
15+
}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have initialized properties.');
16+
17+
Assert::exception(function () {
18+
$interface = new InterfaceType('Demo');
19+
$interface->addProperty('first');
20+
$interface->validate();
21+
}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have properties without hooks.');

0 commit comments

Comments
 (0)