Skip to content

Commit 4392693

Browse files
Add DefineType and ImportType attributes
1 parent f23011f commit 4392693

13 files changed

+232
-9
lines changed

README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
Since the release of PHP 8.0 more and more libraries, frameworks and tools have been updated to use attributes instead of annotations in PHPDocs.
1010

11-
However, static analysis tools like PHPStan or Psalm or IDEs like PHPStorm or VS Code have not made this transition to attributes and they still rely on annotations in PHPDocs for a lot of their functionality.
11+
However, static analysis tools like PHPStan or Psalm or IDEs like PhpStorm or VS Code have not made this transition to attributes and they still rely on annotations in PHPDocs for a lot of their functionality.
1212

1313
This library aims to provide a set of PHP attributes which could replace the most commonly used annotations accepted by these tools and will aim to provide related repositories with the extensions or plugins that would allow these attributes to be used in these tools.
1414

@@ -84,8 +84,10 @@ These are the available attributes and their corresponding PHPDoc annotations:
8484

8585
| Attribute | PHPDoc Annotations |
8686
|-------------------------------------------------------|--------------------------------------|
87+
| [DefineType](doc/DefineType.md) | `@type` |
8788
| [Deprecated](doc/Deprecated.md) | `@deprecated` |
8889
| [Immutable](doc/Immutable.md) | `@immutable` |
90+
| [ImportType](doc/ImportType.md) | `@import-type` |
8991
| [Impure](doc/Impure.md) | `@impure` |
9092
| [Internal](doc/Internal.md) | `@internal` |
9193
| [IsReadOnly](doc/IsReadOnly.md) | `@readonly` |
@@ -108,12 +110,12 @@ These are the available attributes and their corresponding PHPDoc annotations:
108110
| [TemplateImplements](doc/TemplateImplements.md) | `@implements` `@template-implements` |
109111
| [TemplateUse](doc/TemplateUse.md) | `@use` `@template-use` |
110112
| [Throws](doc/Throws.md) | `@throws` |
111-
| [Type](doc/Type.md) | `@var` `@return` |
113+
| [Type](doc/Type.md) | `@var` `@return` `@type` |
112114

113-
## PHPStorm Support
114-
A plugin that fully supports these attributes will need to be created. Until this is ready you can get partial support by installing PHPStan, our PHPStan extension and enabling PHPStan support in PHPStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-phpstan.html)). You will then be able to see errors and messages related to these attributes in your code.
115+
## PhpStorm Support
116+
A plugin that fully supports these attributes will need to be created. Until this is ready you can get partial support by installing PHPStan, our PHPStan extension and enabling PHPStan support in PhpStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-phpstan.html)). You will then be able to see errors and messages related to these attributes in your code.
115117

116-
Alternatively install Psalm, our Psalm extension and enable Psalm support in PHPStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-psalm.html))
118+
Alternatively install Psalm, our Psalm extension and enable Psalm support in PhpStorm (as described [here](https://www.jetbrains.com/help/phpstorm/using-psalm.html))
117119

118120
## VS Code Support
119121
An extension that fully supports these attributes will need to be created. Until this is ready you can get partial support by installing PHPStan, our PHPStan extension and a VS Code extension that supports PHPStan (for example [this one](https://github.com/SanderRonde/phpstan-vscode)). When you enable the extension you will be able to see errors and messages related to these attributes in your code.

doc/DefineType.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# `DefineType` Attribute
2+
3+
This attribute is the equivalent of the `@type` annotation and is used to define new aliases for types and they are scoped to the class where they are defined.
4+
5+
We are not using the `Type` name for this attribute because that name is used for the attribute which is equivalent to the `@var` annotation. But if you prefer, you can use the `Type` attribute instead of this one to define these aliases, but we don't recommend it.
6+
7+
## Arguments
8+
9+
The attribute accepts one or more strings which describe the aliased type. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
10+
11+
We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array<string>` or `Collection<int>`. We aim to accept all the types accepted by static analysis tools for the `@type` annotation.
12+
13+
The arguments can be named arguments and the type is aliased with the name of the argument.
14+
15+
They can also be unnamed arguments with a string that contains both the name of the alias and the aliased type, but we recommend using named arguments.
16+
17+
If the class has more than one type alias that we want to specify, the aliases can either be declared as a list of strings for a single `DefineType` attribute or as a list of `DefineType` attributes (or even a combination of both, though we don't expect this to be actually used).
18+
19+
## Example usage
20+
21+
```php
22+
<?php
23+
24+
use PhpStaticAnalysis\Attributes\DefineType;
25+
26+
#[DefineType(UserAddress: 'array{street: string, city: string, zip: string}')] // this is an alias of the listed type
27+
#[DefineType('UserName array{firstName: string, lastName: string}')]
28+
#[DefineType(
29+
StringArray: 'string[]',
30+
IntArray: 'int[]',
31+
)]
32+
class DefineTypeExample
33+
{
34+
...
35+
}
36+
```

doc/ImportType.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# `ImportType` Attribute
2+
3+
This attribute is the equivalent of the `@import-type` annotation and is used to import aliases for types from another class.
4+
5+
## Arguments
6+
7+
The attribute accepts one or more strings which list the class from which the aliased type neeeds to be imported. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
8+
9+
The arguments can be named arguments and the type is aliased with the name of the argument and the value is the name of the class from which it needs to be imported.
10+
11+
They can also be unnamed arguments with a string that contains both the name of the alias and the name of the class from which it needs to be imported, but we recommend using named arguments.
12+
13+
If the class has more than one type alias that we want to specify, the aliases can either be declared as a list of strings for a single `ImportType` attribute or as a list of `ImportType` 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\ImportType;
21+
22+
#[ImportType(UserAddress: User::class)] // this type is imported from the user class
23+
#[ImportType('UserName from User')]
24+
#[ImportType(
25+
stringArray: 'StringClass',
26+
intArray: 'IntClass',
27+
)]
28+
class ImportTypeExample
29+
{
30+
...
31+
}
32+
```

doc/Template.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class TemplateExample
2828
}
2929

3030
// Type variable with a type
31-
#[Template('T', Exception::class)]
31+
#[Template('T', of:Exception::class)]
3232
public function methodWithTemplate(array $param)
3333
{
3434
}

doc/TemplateContravariant.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use Exception;
1919
use PhpStaticAnalysis\Attributes\TemplateContravariant;
2020

2121
#[TemplateContravariant('T')]
22-
#[TemplateContravariant('T', Exception::class)]
22+
#[TemplateContravariant('T', of:Exception::class)]
2323
class TemplateContravariantExample
2424
{
2525
}

doc/TemplateCovariant.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use Exception;
1919
use PhpStaticAnalysis\Attributes\TemplateCovariant;
2020

2121
#[TemplateCovariant('T')]
22-
#[TemplateCovariant('T', Exception::class)]
22+
#[TemplateCovariant('T', of:Exception::class)]
2323
class TemplateCovariantExample
2424
{
2525
}

doc/Type.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ We could not use `Var` for the name of this attribute because `var` is a reserve
66

77
This attribute can also be used instead of using the `Returns` attribute to specify the type of the return value of a function or method, replacing the `@return` annotation.
88

9+
This attribute can also be used instead of using the `DefineType` attribute to specify an alias for a type, replacing the `@type` annotation.
10+
911
## Arguments
1012

1113
The attribute accepts a string which describes the type of the class property, constant or return value. The attribute itself does not have a knowledge of which types are valid and which are not and this will depend on the implementation for each particular tool.
1214

1315
We expect that the attribute will be able to accept both basic types like `string` or `array` and more advanced types like `array<string>` or `Collection<int>`. We aim to accept all the types accepted by static analysis tools for the `@var` annotation.
1416

17+
If used to replace the `@type` tag, the value should be a string that includes both the name of the alias and the type being aliased.
18+
1519
## Example usage
1620

1721
```php
1822
<?php
1923

2024
use PhpStaticAnalysis\Attributes\Type;
2125

26+
#[Type('FloatArray float[]')]
2227
class TypeExample
2328
{
2429
#[Type('string')] // the type of this constant

src/DefineType.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 DefineType
14+
{
15+
public function __construct(
16+
string ...$types
17+
) {
18+
}
19+
}

src/ImportType.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 ImportType
14+
{
15+
public function __construct(
16+
string ...$from
17+
) {
18+
}
19+
}

src/Type.php

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Attribute;
88

99
#[Attribute(
10+
Attribute::TARGET_CLASS |
1011
Attribute::TARGET_CLASS_CONSTANT |
1112
Attribute::TARGET_PROPERTY |
1213
Attribute::TARGET_METHOD |

tests/DefineTypeTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\DefineType;
6+
use PHPUnit\Framework\TestCase;
7+
8+
#[DefineType(UserAddress: 'array{street: string, city: string, zip: string}')]
9+
#[DefineType('UserName array{firstName: string, lastName: string}')]
10+
#[DefineType(
11+
StringArray: 'string[]',
12+
IntArray: 'int[]',
13+
)]
14+
class DefineTypeTest extends TestCase
15+
{
16+
public function testClassTypes(): void
17+
{
18+
$reflection = new ReflectionClass($this);
19+
$this->assertEquals([
20+
0 => 'UserName array{firstName: string, lastName: string}',
21+
'UserAddress' => 'array{street: string, city: string, zip: string}',
22+
'StringArray' => 'string[]',
23+
'IntArray' => 'int[]',
24+
], self::getPropertiesFromReflection($reflection));
25+
}
26+
27+
public static function getPropertiesFromReflection(
28+
ReflectionClass $reflection
29+
): array {
30+
$attributes = $reflection->getAttributes();
31+
$properties = [];
32+
foreach ($attributes as $attribute) {
33+
if ($attribute->getName() === DefineType::class) {
34+
$attribute->newInstance();
35+
$properties = array_merge($properties, $attribute->getArguments());
36+
}
37+
}
38+
39+
return $properties;
40+
}
41+
}

tests/ImportTypeTest.php

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpStaticAnalysis\Attributes\DefineType;
6+
use PhpStaticAnalysis\Attributes\ImportType;
7+
use PHPUnit\Framework\TestCase;
8+
9+
#[ImportType(UserAddress: User::class)]
10+
#[ImportType('UserName from User')]
11+
#[ImportType(
12+
StringArray: 'User',
13+
IntArray: 'User',
14+
)]
15+
class ImportTypeTest extends TestCase
16+
{
17+
public function testClassTypes(): void
18+
{
19+
$reflection = new ReflectionClass($this);
20+
$this->assertEquals([
21+
0 => 'UserName from User',
22+
'UserAddress' => 'User',
23+
'StringArray' => 'User',
24+
'IntArray' => 'User',
25+
], self::getPropertiesFromReflection($reflection));
26+
}
27+
28+
public static function getPropertiesFromReflection(
29+
ReflectionClass $reflection
30+
): array {
31+
$attributes = $reflection->getAttributes();
32+
$properties = [];
33+
foreach ($attributes as $attribute) {
34+
if ($attribute->getName() === ImportType::class) {
35+
$attribute->newInstance();
36+
$properties = array_merge($properties, $attribute->getArguments());
37+
}
38+
}
39+
40+
return $properties;
41+
}
42+
}
43+
44+
#[DefineType(UserAddress: 'array{street: string, city: string, zip: string}')]
45+
#[DefineType('UserName array{firstName: string, lastName: string}')]
46+
#[DefineType(
47+
StringArray: 'string[]',
48+
IntArray: 'int[]',
49+
)]
50+
class User
51+
{
52+
}

tests/TypeTest.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
use PhpStaticAnalysis\Attributes\Type;
66
use PHPUnit\Framework\TestCase;
77

8+
#[Type('FloatArray float[]')]
89
class TypeTest extends TestCase
910
{
11+
#[Type('string')]
12+
public const NAME = 'name';
13+
1014
#[Type('string')]
1115
public string $property;
1216

@@ -23,6 +27,18 @@ class TypeTest extends TestCase
2327
#[Type('string')]
2428
public string $propertyWithMultipleTypes;
2529

30+
public function testClassTemplate(): void
31+
{
32+
$reflection = new ReflectionClass($this);
33+
$this->assertEquals('FloatArray float[]', self::getTypeFromReflection($reflection));
34+
}
35+
36+
public function testClassConstantType(): void
37+
{
38+
$reflection = new ReflectionClassConstant($this, 'NAME');
39+
$this->assertEquals('string', self::getTypeFromReflection($reflection));
40+
}
41+
2642
public function testPropertyType(): void
2743
{
2844
$this->assertEquals('string', $this->propertyType());
@@ -114,7 +130,7 @@ private function getMethodType(string $methodName): string
114130
}
115131

116132
public static function getTypeFromReflection(
117-
ReflectionProperty | ReflectionMethod | ReflectionFunction $reflection
133+
ReflectionProperty | ReflectionClassConstant | ReflectionMethod | ReflectionFunction | ReflectionClass $reflection
118134
): string {
119135
$attributes = $reflection->getAttributes();
120136
$type = '';

0 commit comments

Comments
 (0)