Skip to content

Commit

Permalink
PHPStan 1.6.0 website changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 26, 2022
1 parent b480ba2 commit 4494b64
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 1 deletion.
1 change: 1 addition & 0 deletions website/.eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = function (eleventyConfig) {

markdownLib.use(require('markdown-it-footnote'));
markdownLib.use(require('markdown-it-abbr'));
markdownLib.use(require('markdown-it-attrs'));

eleventyConfig.setLibrary("md", markdownLib);

Expand Down
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"luxon": "^2.0.0",
"markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^8.0.0",
"markdown-it-attrs": "^4.1.3",
"markdown-it-footnote": "^3.0.3",
"mermaid": "^9.0.0",
"npm-run-all": "^4.1.5",
Expand Down
235 changes: 235 additions & 0 deletions website/src/_posts/phpstan-1-6-0-with-conditional-return-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
---
title: "PHPStan 1.6.0 With Conditional Return Types and More!"
date: 2022-04-26
tags: releases
---

[PHPStan 1.6.0](https://github.com/phpstan/phpstan/releases/tag/1.6.0) which has been in development for the past month or so brings a surprising amount of new features and improvements. Let's dive right into them!

Conditional return types
--------------------------

The majority of this feature was developed by [Richard van Velzen](https://github.com/rvanvelzen). {.text-sm}

Since its first release, PHPStan has offered a way to describe functions that return different types based on arguments passed in the call. So-called [dynamic return type extensions](/developing-extensions/dynamic-return-type-extensions) are really flexible - you can resolve the type based on any logic you can implement. But it comes at a cost - there's a learning curve to the [core concepts](/developing-extensions/core-concepts) of writing PHPStan extensions.

In [PHPStan 0.12](/blog/phpstan-0-12-released) came [generics](/blog/generics-in-php-using-phpdocs). They cover a portion of scenarios with a special PHPDoc syntax where dynamic return type extensions with custom logic were previously needed.

Today PHPStan takes another step in the accessibility of these advanced features. You no longer have to be an expert to take advantage of them. Funnily enough, someone might even call these "no-code" solutions 🤣

Conditional return types allow you to write an "if-else" logic in the PHPDoc `@return` tag.

```php
/**
* @return ($as_float is true ? float : string)
*/
function microtime(bool $as_float): string|float
{
...
}
```

Conditional return types can be combined with generics:

```php
/**
* @template T of object
* @param class-string<T> $class
* @return ($throw is true ? T : T|null)
*/
function getService(string $class, bool $throw = true): ?object
{
...
}
```

Generic template type can be used inside the condition as well:

```php
/**
* @template T of int
* @template U
* @param T $size
* @param U $value
* @return (T is positive-int ? non-empty-array<U> : array<U>)
*/
function fillArray(int $size, $value): array
{
...
}
```

More complicated conditions can be expressed by nesting the conditional types:

```php
/**
* @param int|float $a
* @param int|float $b
* @return ($a is int ? ($b is int ? int : float) : float)
*/
function add($a, $b) {
return $a + $b;
}
```

Integer masks
--------------------------

This feature was developed by [Richard van Velzen](https://github.com/rvanvelzen). {.text-sm}

A common pattern to configure behaviour of a function is to accept an integer value that consists of different flags joined with the `|` operator:

```php
echo json_encode($a, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
```

For this pattern to work, each of these values has to be a distinct power-of-two value (1, 2, 4, 8, ...).

You can now use this in PHPStan:

```php
const FOO = 1;
const BAR = 2;
const BAZ = 4;

/** @param int-mask<FOO, BAR, BAZ> $flag */
function test(int $flag): void
{
$isFoo = ($flag & FOO) !== 0;
$isBar = ($flag & BAR) !== 0;
$isBaz = ($flag & BAZ) !== 0;
}

test(FOO); // OK
test(FOO | BAR); // OK
test(FOO | 8); // Error: Parameter #1 $flag of function test expects int<0, 7>, 9 given.
```

There's also the `int-mask-of<...>` variant which accepts a union type of integer values instead of comma-separated values:

```php
class HelloWorld
{
const FOO_BAR = 1;
const FOO_BAZ = 2;

/** @param int-mask-of<self::FOO_*> $flags */
public static function sayHello(int $flags): void
{
...
}
}
```

Lower memory consumption
--------------------------

PHPStan used to be a really hungry beast. To the point of being brutally killed by CI runners because it consumed not just all the memory up to the `memory_limit` in php.ini, but also all the memory assigned to the runner hardware.

Now it's a less hungry beast. How did I make it happen? It's useful to realize what's going on inside a running program. It's fine to consume memory as useful things are being achieved, but in order to consume less memory in total, it has to be freed afterwards so that it can be used again by different data needed when analysing the next file in line.

To debug memory leaks, I use the [`php-meminfo`](https://github.com/BitOne/php-meminfo) extension. It exports all the scalar values and objects held in memory to a JSON file. It also ships with an analyzer that produces various statistics so that you know where you should start with the optimization.

I quickly realized that most of the memory is occupied by [AST](/developing-extensions/abstract-syntax-tree) nodes. PHP frees the memory occupied by an object when there are no more references to it [^refcount]. It didn't work in case of AST nodes because they kept pointing at each other:

[^refcount]: That's called reference counting.

<pre class="mermaid">
flowchart LR;
TryCatch== stmts ==>array
array== 0 ==>Stmt1
array== 1 ==>Stmt2
array== 2 ==>Stmt3
Stmt1== parent ==>TryCatch
Stmt2== parent ==>TryCatch
Stmt3== parent ==>TryCatch
Stmt1== next ==>Stmt2
Stmt2== next ==>Stmt3
Stmt2== prev ==>Stmt1
Stmt3== prev ==>Stmt2
</pre>

Sure, there's also the garbage collector that [collects cycles](https://www.php.net/manual/en/features.gc.collecting-cycles.php) like these. But PHPStan had it [turned off](https://github.com/phpstan/phpstan-src/blob/1f3ecf8512008fb60fea2258ba53f914118d900f/bin/phpstan#L9) because it improved performance time-wise. When there's a lot of cycles, applications that haven't called `gc_disable()` are burning a lot of CPU time.

Because getting rid of `parent`/`previous`/`next` node attributes is a backward compatibility break for custom rules that read them, I do it only when the [Bleeding Edge](/blog/what-is-bleeding-edge) configuration is enabled. I've written [an article on how to make these rules work again](/blog/preprocessing-ast-for-custom-rules) even without those memory-consuming references.

A nice twist in all this is that removing the `gc_disable()` call no longer represents a significant performance penalty here so it's [no longer used](https://github.com/phpstan/phpstan-src/commit/c72277a3a9f549ad9bfeb8c02bc2eb697646c8dc). [^php73]

[^php73]: It's still used on PHP 7.2 because PHP 7.3 brought [significant performance improvements](https://github.com/php/php-src/pull/3165) of its garbage collector.

In my testing PHPStan now consumes around 50–70 % less memory with [Bleeding Edge](/blog/what-is-bleeding-edge) enabled.

You can enable Bleeding Edge by putting this into your `phpstan.neon`:

```neon
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
```

Fully static reflection
--------------------------

In June 2020 I released PHPStan 0.12.26 with [partially static reflection](/blog/zero-config-analysis-with-static-reflection) which brought many benefits to its users.

It was only partial because the full version consumed more resources. As part of optimizing memory consumption, I've rewritten parts of [BetterReflection](https://github.com/Roave/BetterReflection) so that it doesn't hold onto AST nodes when it no longer needs them. I've [described the process here](https://github.com/Roave/BetterReflection/issues/1073).

In testing it looked really promising, the performance difference between runtime and static reflection was negligible. So I decided it was time to take the next step: start rolling out 100% static reflection engine. It already solves various edge cases like [this](https://github.com/phpstan/phpstan/issues/7077) or [this](https://github.com/phpstan/phpstan/issues/7019).

It's now part of [Bleeding Edge](/blog/what-is-bleeding-edge) and my plan is to enable it for everyone during the PHPStan 1.x release cycle even before the next major version. It's a phased rollout so that I can gather and process feedback from early adopters before unleashing it upon everyone else.


Make `isset()` and null coalescing (`??`) operators consistent in regard to unknown properties
--------------------------

This feature was developed by [Yohta Kimura](https://github.com/rajyan). {.text-sm}

For years PHPStan suffered from this glaring inconsistency:

```php
class Foo
{

}

function (Foo $f): void {
// No error
echo isset($f->prop) ? $f->prop : 'bar';

// Error: Access to an undefined property Foo::$prop.
echo $f->prop ?? 'bar';
};
```

Although the two lines in question are functionally equivalent, PHPStan's behaviour didn't reflect that. The intention was for PHPStan to protect users from typos in property names, but it proven to be more frustrating than useful for most.

The new default behaviour is:

```php
function (Foo $f): void {
// No error
echo isset($f->prop) ? $f->prop : 'bar';

// No error
echo $f->prop ?? 'bar';
};
```

But if you want the stricter behaviour and have both lines report an error (consistently at last), you can enable it with this option in your `phpstan.neon`:

```neon
parameters:
checkDynamicProperties: true
```

This is also enabled in [`phpstan-strict-rules`](https://github.com/phpstan/phpstan-strict-rules) 1.2.0 when combined with [Bleeding Edge](/blog/what-is-bleeding-edge).

Full support of PHP 8.1
--------------------------

Previous PHPStan 1.x releases [brought support](/blog/plan-to-support-php-8-1) of various changes and new features made in PHP 8.1. The last thing PHPStan wasn't aware of were changed signatures of built-in PHP functions. Most notably [`fputcsv`](https://www.php.net/manual/en/function.fputcsv.php) gained a new optional parameter, and [some functions were migrated from resources to objects](https://php.watch/articles/resource-object#resource-php81).

Since PHP 8.0 PHPStan uses a [homegrown stubs repository](https://github.com/phpstan/php-8-stubs) extracted directly from [php-src](https://github.com/php/php-src/) - I can rely on them being correct, but I had to find a way to represent PHP 8.1 changes without doubling the repository in size. I've settled on using custom `#[Until]` and `#[Since]` attributes to mark [signatures that changed](https://github.com/phpstan/php-8-stubs/blob/main/stubs/ext/fileinfo/finfo_open.php).

---

Do you like PHPStan and use it every day? [**Consider supporting further development of PHPStan on GitHub Sponsors**](https://github.com/sponsors/ondrejmirtes/). I’d really appreciate it!
2 changes: 1 addition & 1 deletion website/src/_posts/plan-to-support-php-8-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ So what's the current situation? Since 1.0 PHPStan was able to run on PHP 8.1 wi

[PHPStan 1.4](https://github.com/phpstan/phpstan/releases/tag/1.4.0) brought support for readonly properties.

What remains to be updated are [stubs](https://github.com/phpstan/php-8-stubs) of changed function signatures, and some miscellaneous things.
[PHPStan 1.6](https://github.com/phpstan/phpstan/releases/tag/1.6.0) updated changed function signatures. As of this release, **PHP 8.1 is fully supported**!

---

Expand Down
29 changes: 29 additions & 0 deletions website/src/writing-php-code/phpdoc-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,35 @@ Generics

[Generics »](/blog/generics-in-php-using-phpdocs), [Generics By Examples »](/blog/generics-by-examples)

Conditional return types
-------------------------

A simpler alternative to generics if you just want to infer the return type based on if-else logic.

```php
/**
* @return ($size is positive-int ? non-empty-array : array)
*/
function fillArray(int $size): array
{
...
}
```

It can be combined with generics as well in both the condition and the if-else types:

```php
/**
* @template T of int|array<int>
* @param T $id
* @return (T is int ? static : array<static>)
*/
public function fetch(int|array $id)
{
...
}
```

class-string
-------------------------

Expand Down
5 changes: 5 additions & 0 deletions website/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3905,6 +3905,11 @@ markdown-it-anchor@^8.0.0:
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.4.1.tgz#29e560593f5edb80b25fdab8b23f93ef8a91b31e"
integrity sha512-sLODeRetZ/61KkKLJElaU3NuU2z7MhXf12Ml1WJMSdwpngeofneCRF+JBbat8HiSqhniOMuTemXMrsI7hA6XyA==

markdown-it-attrs@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/markdown-it-attrs/-/markdown-it-attrs-4.1.3.tgz#2b5ab8371ae947155566eaabe8c73548e816dfcd"
integrity sha512-d5yg/lzQV2KFI/4LPsZQB3uxQrf0/l2/RnMPCPm4lYLOZUSmFlpPccyojnzaHkfQpAD8wBHfnfUW0aMhpKOS2g==

markdown-it-footnote@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8"
Expand Down

0 comments on commit 4494b64

Please sign in to comment.