Skip to content

Commit 2fb5e52

Browse files
committed
Interfaces can have properties
1 parent 6a8a9e8 commit 2fb5e52

9 files changed

+176
-34
lines changed

src/PhpGenerator/ClassManipulator.php

+20-15
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,28 @@ 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+
$property = (new Factory)->fromPropertyReflection($rp);
44+
$this->class->addMember($property);
45+
$property->setHooks([]);
46+
return $property;
4247
}
4348

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

4952

@@ -52,16 +55,15 @@ public function inheritProperty(string $name, bool $returnIfExists = false): Pro
5255
*/
5356
public function inheritMethod(string $name, bool $returnIfExists = false): Method
5457
{
55-
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()];
5658
if ($this->class->hasMethod($name)) {
5759
return $returnIfExists
5860
? $this->class->getMethod($name)
5961
: 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.");
6362
}
6463

64+
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
65+
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
66+
6567
foreach ($parents as $parent) {
6668
try {
6769
$rm = new \ReflectionMethod($parent, $name);
@@ -91,5 +93,8 @@ public function implementInterface(string $interfaceName): void
9193
foreach ($interface->getMethods() as $method) {
9294
$this->inheritMethod($method->getName(), returnIfExists: true);
9395
}
96+
foreach ($interface->getProperties() as $property) {
97+
$this->inheritProperty($property->getName(), returnIfExists: true);
98+
}
9499
}
95100
}

src/PhpGenerator/InterfaceType.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ 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
@@ -15,6 +15,7 @@ class PropertyHook
1515
private string $body = '';
1616
private bool $short = false;
1717
private bool $final = false;
18+
private bool $abstract = false;
1819

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

6465

66+
public function setAbstract(bool $state = true): static
67+
{
68+
$this->abstract = $state;
69+
return $this;
70+
}
71+
72+
73+
public function isAbstract(): bool
74+
{
75+
return $this->abstract;
76+
}
77+
78+
6579
/** @internal */
6680
public function setParameters(array $val): static
6781
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 TestInterface
17+
{
18+
public array $bar { get; }
19+
20+
public function testMethod();
21+
}
22+
23+
$class = new ClassType('TestClass');
24+
$manipulator = new ClassManipulator($class);
25+
26+
// Test valid interface implementation
27+
$manipulator->implementInterface(TestInterface::class);
28+
Assert::match(<<<'XX'
29+
class TestClass implements TestInterface
30+
{
31+
public array $bar;
32+
33+
34+
function testMethod()
35+
{
36+
}
37+
}
38+
39+
XX, (string) $class);
40+
41+
42+
// Test exception for non-interface
43+
Assert::exception(
44+
fn() => $manipulator->implementInterface(stdClass::class),
45+
InvalidArgumentException::class,
46+
);

tests/PhpGenerator/ClassManipulator.implementInterface.phpt

+10-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ $manipulator = new ClassManipulator($class);
1919

2020
// Test valid interface implementation
2121
$manipulator->implementInterface(TestInterface::class);
22-
Assert::true(in_array(TestInterface::class, $class->getImplements(), true));
23-
Assert::true($class->hasMethod('testMethod'));
22+
Assert::match(<<<'XX'
23+
class TestClass implements TestInterface
24+
{
25+
function testMethod()
26+
{
27+
}
28+
}
29+
30+
XX, (string) $class);
31+
2432

2533
// Test exception for non-interface
2634
Assert::exception(

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.');

tests/PhpGenerator/PropertyLike.hooks.phpt

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
declare(strict_types=1);
88

99
use Nette\PhpGenerator\ClassType;
10+
use Nette\PhpGenerator\InterfaceType;
1011

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

@@ -94,3 +95,27 @@ same(<<<'XX'
9495
}
9596

9697
XX, (string) $class);
98+
99+
100+
$interface = new InterfaceType('Demo');
101+
102+
$interface->addProperty('first')
103+
->setType('int')
104+
->setPublic()
105+
->addHook('get');
106+
107+
$prop = $interface->addProperty('second')
108+
->setType('Value')
109+
->setPublic();
110+
111+
$prop->addHook('get');
112+
$prop->addHook('set');
113+
114+
same(<<<'XX'
115+
interface Demo
116+
{
117+
public int $first { get; }
118+
public Value $second { set; get; }
119+
}
120+
121+
XX, (string) $interface);

0 commit comments

Comments
 (0)