Skip to content

Commit 4ad035e

Browse files
Add RequireExtends and RequireImplements attributes
1 parent c0a2543 commit 4ad035e

7 files changed

+223
-21
lines changed

README.md

+23-21
Original file line numberDiff line numberDiff line change
@@ -92,25 +92,27 @@ And then install any needed extensions/plugins for the tools that you use.
9292

9393
These are the available attributes and their corresponding PHPDoc annotations:
9494

95-
| Attribute | PHPDoc Annotations |
96-
|-------------------------------------------------------|--------------------------------------|
97-
| [Deprecated](doc/Deprecated.md) | `@deprecated` |
98-
| [Internal](doc/Internal.md) | `@internal` |
99-
| [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
100-
| [Method](doc/Method.md) | `@method` |
101-
| [Mixin](doc/Mixin.md) | `@mixin` |
102-
| [Param](doc/Param.md) | `@param` |
103-
| [ParamOut](doc/ParamOut.md) | `@param-out` |
104-
| [Property](doc/Property.md) | `@property` `@var` |
105-
| [PropertyRead](doc/PropertyRead.md) | `@property-read` |
106-
| [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |
107-
| [Returns](doc/Returns.md) | `@return` |
108-
| [SelfOut](doc/SelfOut.md) | `@self-out` `@this-out` |
109-
| [Template](doc/Template.md) | `@template` |
110-
| [TemplateContravariant](doc/TemplateContravariant.md) | `@template-contravariant` |
111-
| [TemplateCovariant](doc/TemplateCovariant.md) | `@template-covariant` |
112-
| [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
113-
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements` |
114-
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
115-
| [Type](doc/Type.md) | `@var` `@return` |
95+
| Attribute | PHPDoc Annotations |
96+
|-------------------------------------------------------|-------------------------------------|
97+
| [Deprecated](doc/Deprecated.md) | `@deprecated` |
98+
| [Internal](doc/Internal.md) | `@internal` |
99+
| [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
100+
| [Method](doc/Method.md) | `@method` |
101+
| [Mixin](doc/Mixin.md) | `@mixin` |
102+
| [Param](doc/Param.md) | `@param` |
103+
| [ParamOut](doc/ParamOut.md) | `@param-out` |
104+
| [Property](doc/Property.md) | `@property` `@var` |
105+
| [PropertyRead](doc/PropertyRead.md) | `@property-read` |
106+
| [PropertyWrite](doc/PropertyWrite.md) | `@property-write` |
107+
| [RequireExtends](doc/RequireExtends.md) | `@require-extends` |
108+
| [RequireImplements](doc/RequireImplements.md) | `@require-implements` |
109+
| [Returns](doc/Returns.md) | `@return` |
110+
| [SelfOut](doc/SelfOut.md) | `@self-out` `@this-out` |
111+
| [Template](doc/Template.md) | `@template` |
112+
| [TemplateContravariant](doc/TemplateContravariant.md) | `@template-contravariant` |
113+
| [TemplateCovariant](doc/TemplateCovariant.md) | `@template-covariant` |
114+
| [TemplateExtends](doc/TemplateExtends.md) | `@extends` `@template-extends` |
115+
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements`|
116+
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
117+
| [Type](doc/Type.md) | `@var` `@return` |
116118

doc/RequireExtends.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# `RequireExtends` Attribute
2+
3+
This attribute is the equivalent of the `@require-extends` annotation. It can be applied to a trait to specify that the class using it must extend a specific class.
4+
5+
## Arguments
6+
7+
The attribute accepts one string that defines the class that needs to be extended. The attribute itself does not have a knowledge of which classes are valid and which are not and this will depend on the implementation for each particular tool.
8+
9+
We aim to accept all the classes accepted by static analysis tools for the `@require-extends` annotation.
10+
11+
## Example usage
12+
13+
```php
14+
<?php
15+
16+
use PhpStaticAnalysis\Attributes\RequireExtends;
17+
18+
abstract class Parent {
19+
}
20+
21+
#[RequireExtends('ParentClass')]
22+
trait myTrait {
23+
}
24+
25+
class Child extends Parent {
26+
use myTrait;
27+
}
28+
```

doc/RequireImplements.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# `RequireImplements` Attribute
2+
3+
This attribute is the equivalent of the `@require-implements` annotation. It can be applied to a trait to indicate that the class using it should implement one or more interfaces.
4+
5+
## Arguments
6+
7+
The attribute accepts one or more strings that define the interfaces that need to be implemented. The attribute itself does not have a knowledge of which interfaces are valid and which are not and this will depend on the implementation for each particular tool.
8+
9+
We aim to accept all the interface names accepted by static analysis tools for the `@require-implements` annotation.
10+
11+
The arguments need to be unnamed arguments.
12+
13+
If the class has more than one interface that we want to require, the different interfaces can either be declared as a list of strings for a single `RequireInterface` attribute or as a list of `RequireInterface` attributes (or even a combination of both, though we don't expect this to be actually used).
14+
15+
## Example usage
16+
17+
```php
18+
<?php
19+
20+
use PhpStaticAnalysis\Attributes\RequireImplements;
21+
22+
interface RequireInterface
23+
{
24+
}
25+
26+
#[RequireImplements('RequireInterface')]
27+
trait MyTrait
28+
{
29+
}
30+
31+
class MyClass implements RequireInterface {
32+
use MyTrait;
33+
}
34+
```

src/RequireExtends.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStaticAnalysis\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(
10+
Attribute::TARGET_CLASS
11+
)]
12+
final class RequireExtends
13+
{
14+
public function __construct(
15+
string $class
16+
) {
17+
}
18+
}

src/RequireImplements.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpStaticAnalysis\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(
10+
Attribute::TARGET_CLASS |
11+
Attribute::IS_REPEATABLE
12+
)]
13+
final class RequireImplements
14+
{
15+
public function __construct(
16+
string ...$interfaces
17+
) {
18+
}
19+
}

tests/RequireExtendsTest.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\RequireExtends;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class RequireExtendsTest extends TestCase
9+
{
10+
public function testClassRequireExtends(): void
11+
{
12+
$reflection = new ReflectionClass(RequireMyTrait::class);
13+
$this->assertEquals('RequireParentClass', self::getRequireExtendssFromReflection($reflection));
14+
}
15+
16+
public static function getRequireExtendssFromReflection(
17+
ReflectionClass $reflection
18+
): string {
19+
$attributes = $reflection->getAttributes();
20+
$extends = '';
21+
foreach ($attributes as $attribute) {
22+
if ($attribute->getName() === RequireExtends::class) {
23+
$attribute->newInstance();
24+
$extends = $attribute->getArguments()[0];
25+
}
26+
}
27+
28+
return $extends;
29+
}
30+
}
31+
32+
class RequireParentClass
33+
{
34+
}
35+
36+
#[RequireExtends('RequireParentClass')]
37+
trait RequireMyTrait
38+
{
39+
}
40+
41+
class RequireChildClass extends RequireParentClass
42+
{
43+
use RequireMyTrait;
44+
}

tests/RequireImplementsTest.php

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\RequireImplements;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class RequireImplementsTest extends TestCase implements RequireTestInterface, RequireTestInterface2, RequireTestInterface3
9+
{
10+
use RequireInterfaceTrait;
11+
12+
public function testClassRequireImplements(): void
13+
{
14+
$reflection = new ReflectionClass(RequireInterfaceTrait::class);
15+
$this->assertEquals([
16+
'RequireTestInterface',
17+
'RequireTestInterface2',
18+
'RequireTestInterface3',
19+
], self::getRequireImplementssFromReflection($reflection));
20+
}
21+
22+
public static function getRequireImplementssFromReflection(
23+
ReflectionClass $reflection
24+
): array {
25+
$attributes = $reflection->getAttributes();
26+
$implements = [];
27+
foreach ($attributes as $attribute) {
28+
if ($attribute->getName() === RequireImplements::class) {
29+
$attribute->newInstance();
30+
$implements = array_merge($implements, $attribute->getArguments());
31+
}
32+
}
33+
34+
return $implements;
35+
}
36+
}
37+
38+
#[RequireImplements('RequireTestInterface')]
39+
#[RequireImplements(
40+
'RequireTestInterface2',
41+
'RequireTestInterface3'
42+
)]
43+
trait RequireInterfaceTrait
44+
{
45+
}
46+
47+
interface RequireTestInterface
48+
{
49+
}
50+
51+
interface RequireTestInterface2
52+
{
53+
}
54+
55+
interface RequireTestInterface3
56+
{
57+
}

0 commit comments

Comments
 (0)