PHP types
Tags: php, yii, spiral, cycle
Published: 2023-05-07 22:50:27

It would be too trivial to say that following strict types and avoiding implicit conversions reduces code magic, which leads to stability and reliability.
That's why in [Spiral](https://github.com/spiral), [Yii](https://github.com/yiisoft), [Cycle](https://github.com/cycle) and other projects where I contribute, we use strict types whenever possible.
It's kind of obvious, but... This is **PHP**. And that means you can't do without nuances 💩


## ⭕️ `declare(strict_types=1);`

Straight from the documentation: *by default, PHP will coerce values of the wrong type into the expected scalar type declaration if possible.* ...
_It is possible to enable strict mode on a per-file basis. In strict mode, only a value corresponding exactly to the type declaration will be accepted, otherwise a TypeError will be thrown. The only exception to this rule is that an int value will pass a float type declaration._

_**Warning** Function calls from within internal functions will not be affected by the `strict_types` declaration._

Note the warning. Many core functions **do not follow** type strictness.
For example, `array_map()` and `array_filter()` will make an implicit type conversion.

```php
print_r(array_map(
    fn(int $a, int $b) => $a + $b,
    [1, '10', 3],
    [4, 5, '1e2'],
));
// output:
Array (
    [0] => 5
    [1] => 15
    [2] => 103
)
```

But `call_user_func()` will complain about type mismatch.

Reflection doesn't follow strict types too. So instead of calling `newInstanceArgs()`/`newInstance()` in the [Container Factory](https://github.com/spiral/core/blob/32a2d8694c81119bed633916b01b0f320442229d/src/Internal/Factory.php#L384), we have
```php
$instance = new $class(...$arguments);
```
It may be a little slower, but it's more reliable.

Now let's go to hacks.


## ⭕️ Variable Typing

With a simple hack you can bind a type to a variable.

```php
function makeInt(int &$i): void
{
    static $list = [];
    $list[] = $obj = new class() {
        public int $i;
    };
    $obj->i = &$i;
}

$int = 1;
makeInt($int);

$int = 42; // 42
$int = 'foo'; // Fatal error: Uncaught TypeError: Cannot assign string...
```

*Don't use it because it leaks.*


## ⭕️ PHP 8.2

The `null`, `false` and `true` types can now be used stand-alone.
What is this for? To provide covariance. For example, when extending a method, the return value with `bool` can be narrowed down to `true` or `false`, and nullable (`?Foo`) can be narrowed to less specific `null`. There are a lot of such cases in libraries.

The DNF (Disjunctive Normal Form) has arrived.
Now you may meet such monster in code:
```
(Countable&Traversable)|array
```


## Notes

ℹ️ Curious but nullable-sugar (`?Foo`) was added in PHP 7.1, before Union Types.
ℹ️ Always explicitly specify the type of nullable parameters (`?Foo $foo = null`) rather than relying only on the default null value (`Foo $foo = null`).
ℹ️ The `never` type was added in PHP 8.1. But you don't need it if you use RoadRunner.
ℹ️ The `callable` type exists only in function and method signatures. It can't be used in properties, so you can't put it into Promoted properties either. That's because in different contexts callable can be different.
So how can we get a `callable` and write it to a property? For example like this:

```php
// A class declaration
private \Closure $callback;

public function __construct(callable $callback)
{
    $this->callback = $callback(...);
}
```