Modern PHPDoc Annotations
-> imported from https://suckup.de/2020/02/modern-phpdoc-annotations/
We will start very simple with PhpStorm and default PHPDoc, then we will increase the complexity step by step until we have auto-completion for array keys directly from the database with generics, immutable and type safety support.
1.0 PhpStorm & auto-generate PHPDoc blocks
„For documentation comments, PhpStorm provides completion that is enabled by default. PhpStorm creates stubs of „PHPDoc blocks“ when you type the /** opening tag and press Enter, or press Alt+Insert and appoint the code construct (a class, a method, a function, and so on) to document. Depending on your choice, PhpStorm will create the required tags or add an empty documentation stub.“ –
https://www.jetbrains.com/help/phpstorm/phpdoc-comments.html
Code:
_/\*\*_
_\* @param array_ _$row_
_\*_
_\* @return array_
_\*/_
abstract function formatRow(array $row): array;
1.1 Return $this|static|self
It‘s quite annoying that php itself currently only have „self“ as return type (https://wiki.php.net/rfc/static_return_type) for the current class. Because of „late static binding“ you can use „static“ in your code to refer to the class a method was actually called on, even if the method is inherited. But in PHPDoc you can already use:
- @return $this : if you really return $this (e.g. for fluent interface)
- @return static : refer to the class a method was actually called on
- @return self : refer to the class a method was written in
Code:
_/\*\*_
_\* @return_ _static_
_\*/_
abstract function getFoo(): self;
https://blog.jetbrains.com/phpstorm/2019/09/phpstorm-2019-2-2-is-released/
1.2 New (and not that new) Array Syntax
PhpStorm and (PHPStan & Psalm) are supporting some new (and some not that new) array syntax for PHPDoc types, but for now PhpStorm will not auto-generate this types.
Examples:
- int[]: an array with only INT values – [1, 4, 6, 8, 9, …]
- array<int, int> : an array with only INT values – [4 => 1, 8 => 4, 12 => 6, …]
- string[]: an array with only STRING values – [„foo“, „bar“, …]
- array<int, string> : an array with only STRING values – [4 => „foo“, 8 => „bar“, …]
- Order** []**: an array with only „Order“-Object values – [Order, Order, …]
- array<int|string, Order** >**: an array with INT or STRING as key and „Order“-Object values – [4 => Order, ‘foo‘ => Order, …]
- array<int|string, mixed> : an array with INT or STRING as key and mixed as values – [1 => 1, 4 => „foo“, 6 => \stdClass, …]
- array<int, array<int, string>> : an array with INT as key and and an array (with INT as key and string as value) as values – [1 => [1 => „foo“], 4 => [1 => 4], …]
- array<int, string[]****>: an array with INT as key and and an array (with INT as key and string as value) as values – [1 => [„foo“, „lall“], 4 => [„öäü“, „bar“], …]
- array{output: string, debug: string} : an array with the key „output“ and „debug“, the values are STRING values – [‚output‘ => ‚foo‘, ‚debug‘ => ‚bar‘]
- array<int, array{output: string, debug: string}> : an array with the key „output“ and „debug“, the values are STRING values – [1 => [‚output‘ => ‚foo‘, ‚debug‘ => ‚bar‘], 3 => [‚output‘ => ‚foo‘, ‚debug‘ => ‚bar‘], …]
Examples (@psalm-* || @phpstan-*): PHPStan can also use „psalm-*“ prefixed annotations and Psalm understands „phpstan-*“ annotations.
- list<array{output: string, debug: string}> : an array with the key „output“ and „debug“, the values are STRING values – [0 => [‚output‘ => ‚foo‘, ‚debug‘ => ‚bar‘], 1 => [‚output‘ => ‚foo‘, ‚debug‘ => ‚bar‘], …]
list: represents continuous, integer-indexed arrays (always start from index zero) like: [„red“, „yellow“, „blue“]
Live-Examples:
– Psalm: https://psalm.dev/r/922d4ba5b1
– PHPStan: https://phpstan.org/r/ce657ef4-9f18-46a1-b21a-e51e3a0e6d2d
Code:
_/\*\*_
_\* @param array\<int|string, mixed\>_ _$row_
_\*_
_\* @return array\<int|string, mixed\>_
_\*/_
abstract function formatRow(array $row): array;
PhpStorm support?: Sadly PhpStorm did not have good support for these types, so that you often have to add „@psalm-*“ PHPDoc comments. For example PhpStorm will accept „array<int, Order>“ but PhpStorm will not understand the PHPDoc, so that you need to add e.g. „@param Order[] $order“ and „@psalm-param array<int, Order> $order“.
Examples for PhoStorm + PHPStan || Psalm:
/*** @param Order[] $order * @psalm-param array<int, Order> $order * * @return void */public function fooOrder($order): void { ... }// you could also use "..." here/*** @param Order ...$order** @return void*/
public function fooOrder(Order ...$order): void { ... }
/** * @param int $foo_id * * @return Foo[]|Generator * @psalm-return Generator&iterable<Foo> */
abstract function fetchYieldByFoo($foo\_id): Generator;
1.3 Dynamic Autocompletion (+ data from your database) via deep-assoc-completion
If you have a method e.g. „formatRow($row)“ you can use „getFieldArray()[0]“ (data from the database – you have to connection the IDE with your database and your queries need to be analyzable by PhpStorm (take a look at the next screenshot) and combine static data from „getHeaderFieldArray()“, so that you have auto-completion from different sources.
Code:
_/\*\*_
_\* @param array\<int|string, mixed\>_ _$row_ _= $this-\>getFieldArray()[0] + $this-\>getHeaderFieldArray()_
_\*_
_\* @return array\<int|string, mixed\>_
_\*/_
abstract function formatRow(array $row): array;
more information + examples: https://github.com/klesun/deep-assoc-completion
1.4 Immutability Check via Static Code Analyses (via psalm)
And there is even more. You can add PHPDoc annotation that will check if you really use immutable classes or at least methods. Please read more here: https://psalm.dev/articles/immutability-and-beyond
Code:
_/\*\*_
_\* @param array\<int|string, mixed\>_ _$row_ _= $this-\>getFieldArray()[0] + $this-\>getHeaderFieldArray()_
_\*_
_\* @return array\<int|string, mixed\>_
_\*_
_\*_ @psalm-mutation-free
_\*/_
abstract function formatRow(array $row): array;
Live-Example:
– Psalm: https://psalm.dev/r/5bac0a9a07
1.5 Generics in PHP via Static Code Analyses
We can also use Generics via code annotations. PHPStan & Psalm both support it, but Psalm’s support is more feature complete and both tools can use the „@psalm-“-syntax. Here comes some simple examples.
array_last : Will return the last array element from the $array (type: TLast) or the $fallback (type: TLastFallback). We tell the function that the types comes from the input parameters and that the input is an array of TLast or TLastFallback from the fallback.
_/\*\*_
_\*_ _ **@param** _ _array__\<mixed\>_ _$array_
_\*_ _ **@param** _ _mixed_ _$fallback_ _\<p\> __This fallback will be used, if the array is empty.__ \</p\>_
_\*_
_\*_ _ **@return** _ _mixed|null_
_\*_
_\*_ _ **@template** _ _TLast_
_\*_ _ **@template** _ _TLastFallback_
_\*_ _ **@psalm-param** _ _TLast[] $array_
_\*_ _ **@psalm-param** _ _TLastFallback $fallback_
_\*_ _ **@psalm-return** _ _TLast|TLastFallback_
_\*/_
**function** array\_last_**(**_ **array** $array, $fallback = **null** _**)**_
_ **{** _
$key\_last = \array\_key\_last_**(**_$array_**)**_;
**if** _**(**_$key\_last === **null** _**) {**_
**return** $fallback;
_ **}** _
**return** $array_**[**_$key\_last_**]**_;
_ **}** _
array_first : Will return the first array element from the $array (type: TFirst) or the $fallback (type: TFirstFallback). We tell the function that the types comes from the input params and that the input is an array of TFirst or TFirstFallback from the fallback.
_/\*\*_
_\*_ _ **@param** _ _array__\<mixed\>_ _$array_
_\*_ _ **@param** _ _mixed_ _$fallback_ _\<p\> __This fallback will be used, if the array is empty.__ \</p\>_
_\*_
_\*_ _ **@return** _ _mixed|null_
_\*_
_\*_ _ **@template** _ _TFirst_
_\*_ _ **@template** _ _TFirstFallback_
_\*_ _ **@psalm-param** _ _TFirst[] $array_
_\*_ _ **@psalm-param** _ _TFirstFallback $fallback_
_\*_ _ **@psalm-return** _ _TFirst|TFirstFallback_
_\*/_
**function** array\_first_**(**_ **array** $array, $fallback = **null** _**)**_
_ **{** _
$key\_first = array\_key\_first_**(**_$array_**)**_;
**if** _**(**_$key\_first === **null** _**) {**_
**return** $fallback;
_ **}** _
**return** $array_**[**_$key\_first_**]**_;
_ **}** _
So we can define „Templates“ and map input arguments on that types, this can be even more complex if you use it in a class context and you map the „Templates“ on class properties. But the logic will be the same.
Here is a more complex example: https://github.com/voku/Arrayy/blob/master/src/Collection/CollectionInterface.php
PhpStorm support? : Noop, sadly we need to hack this via „PHPSTORM_META“, so here is an example:
- override(\array_filter(0), type(0)); // suppose first parameter type is MyClass[] then return type of array_filter will be MyClass[]
- override(\array_reduce(0), elementType(0)); // suppose first parameter type is MyClass[] then return type of array_reduce will be MyClass
Read more here:
2.0 Resume
It‘s not perfect, and type check and auto-completion only with PHPDoc is not really what I expected for the year 2020. But it‘s working and I hope PhpStorm will bring more support for the new types annotations in the future.
More Links:
https://docs.phpdoc.org/latest/guides/types.html
https://scrutinizer-ci.com/docs/tools/php/php-analyzer/guides/annotating_code
https://github.com/klesun/deep-assoc-completion/blob/master/docs/deep-keys-overview.md
https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc.md#appendix-a-types
https://psalm.dev/docs/annotating_code/supported_annotations/