Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Lines changed: 31 additions & 15 deletions
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

Lines changed: 18 additions & 2 deletions
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

Lines changed: 21 additions & 14 deletions
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 || $classinstanceof InterfaceType) {
202202
foreach ($class->getProperties() as $property) {
203-
$properties[] = $this->printProperty($property, $readOnlyClass);
203+
$properties[] = $this->printProperty($property, $readOnlyClass, $classinstanceof 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

Lines changed: 14 additions & 0 deletions
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
{
Lines changed: 81 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 21 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /