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;

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>
*
* @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:

 

Do Not Fear The White Space in your Code

“White space is to be regarded as an active element, not a passive background.”
– Jan Tschichold

At least since Apple is using massive white space for their product pages, every web-designer knows that the visitors prefer simple over the complex designs. And I think we can use some of this design principles to make our code more readable. 

What is white space?

White space is created by pressing the Return key [\n|\r|\r\n], Spacebar key [ ], or the Tab key [\t]. White space is essentially any bit of space in your code that is not taken up by „physical“ characters.

[\n] = LF (Line Feed) → Used as a new line character in Unix/Mac OS X

[\r] = CR (Carriage Return) → Used as a new line character in Mac OS before X

[\r\n] = CR + LF → Used as a new line character in Windows

https://stackoverflow.com/a/15433225/1155858

White space is critical for organizing our code, because we tend to find some relations between “things“ to instinctively find a meaning in the composition. Whatever you see, your mind will filter, add and remove the given input and because of that we should prepare the input. So that we can easily consume the code.

  • Vertical Whitespace:
    • A single blank line.
  • Horizontal Whitespace:
    • Leading White Space (at the start of a line): Leading white space (i.e., indentation) is addressed elsewhere. (Indent with i.e. 4 spaces is a good default.)
    • Internal White Space (in the code itself): The code is more readable with some white space between the different operations in a single line.
    • Trailing White Space (at the end of a line): This kind of white spaces are unnecessary and can complicate diffs.

Horizontal alignment: Alignment can aid readability, but it creates problems for future maintenance, if you do not automate your code style.

„White space also makes content more readable. A study (Lin, 2004) found that good use of white space between paragraphs and in the left and right margins increases comprehension by almost 20%. Readers find it easier to focus on and process generously spaced content.“[Lin, 2004] Evaluating older adults‘ retention in hypertext perusal: impacts of presentation media as a function of text topology by Dyi-Yih Michael Lin in „Computers in Human Behavior“, Volume 20, Issue 4, July 2004, Pages 491-503

Where do we use white space?

We mostly add white space after a parameter, function, method, if-, else-, while-, and other calls so that it’s easier to identify where code that belongs together starts and ends. Unlike a book in which you read from top to bottom, we mostly jump into the code and only read parts of the code. It’s more like a newspaper where you read the headline (i.e. method name) and then jump to the next article (i.e. method).

Whitespace changes are tricky for “diff”

You can use something like “git diff -w” …

  • –ignore-space-at-eol: Ignore changes in whitespace at EOL.
  • -b, –ignore-space-change: Ignore changes in amount of whitespace. This ignores whitespace at line end, and considers all other sequences of one or more whitespace characters to be equivalent.
  • -w, –ignore-all-space: Ignore whitespace when comparing lines. This ignores differences even if one line has whitespace where the other line has none.
  • –ignore-blank-lines: Ignore changes whose lines are all blank.

… but the problems is still there, that white space changes hurts readability from default diffs in i.e. gitlab / github / etc. so that you should try to commit white space changes in a separated commit.

Be consistent!

The most important and the only things that everybody will agree on is: “Be consistent [and try to automate this process, please]!For example, don’t mix tabs and spaces or playing different braces for the same type of code. If the team (or project) didn’t use any code style at all, then it’s quite simple, take an existing coding standard (try to follow the conventions already in place for a language, if there are any) where automation is already implemented for and stick to it in the first run.

Design principles for your code

Proximity: Things closer together will be seen as belonging together.

Code: Put code together that belongs together.

bad:

.example_1,.example_2{background: url(images/example.png) center center no-repeat, url(images/example-2.png) top left repeat, url(images/example-3.png) top right no-repeat;}

not that bad:

.example_1, 
.example_2 {
background: url(images/example.png) center center no-repeat,
url(images/example-2.png) top left repeat,
url(images/example-3.png) top right no-repeat;
}

I also like to have a single blank line before ‚break‘, ‚continue‘, ‚declare‘, ‚return‘, ‚throw‘, ‚try‘, so that you see that this is some kind of important code. But also the readability of simple „if“-statements can be improved.

bad:

if (($loggedInKunde->bestellungen_genehmigen || $loggedInKunde->manuelle_bestellgenehmigung) && ($wareneingang != 5)) {

not that bad:

if (
(
$loggedInKunde->bestellungen_genehmigen
||
$loggedInKunde->manuelle_bestellgenehmigung
)
&&
$wareneingang != 5
) {

better:

if ($this->useOrderApproval()) { 

And please do not mix different things like variable assinment and „if“-statments in one blob of code.

bad:

if (null !== $token = $this->tokenStorage->getToken()) {

not that bad:

$token = $this->tokenStorage->getToken();
if ($token !== null) {

better:

$token = $this->tokenStorage->getToken();
if ($token->exists()) {

 

Similarity: Things with the same characteristics (shape, color, shading, orientation, …) will be seen as belonging together.

Code: Write code that do similar things in the same code style.

bad:

$valueArray = Bar::searchSplitButKeepQuotes($value);
if (\count($valueArray) <= 0) { return ''; }

$valueFoo = Foo::searchSplit($value);
if (\count($valueFoo) === 0) {
return '';

}

not that bad:

$bars = Bar::searchSplitButKeepQuotes($value);
if (\count($bars) === 0) {
return '';
}

$foos = Foo::searchSplit($value);
if (\count($foos) === 0) {
return '';
}

 

Symmetry: Our mind tends to perceive objects by finding a starting point and it’s pleased when it can find uniformity structures. Sometimes our mind will see such structures also if they are not really there.

Code: Try to use uniformity code structures.

bad:

if (widget.IsRegular) {
    service.Process(widget);
} else {
    var result = widget.Calculation();
    widget.ApplyResult(result);
    service.ProcessSpecial(widget);
}

not that bad:

if (widget.IsRegular) {
    service.Process(widget);
} else {
    service.ProcessSpecial(widget);
}

 

Links: