PHP 8.0: News in Data Types (2/4)

3 months ago by David Grudl translated by Filip Klimeš     edit

PHP version 8.0 has just been released. It's full of new features, like no version before. Their introduction deserves four separate articles. In the second part, we will take a look at data types.

Let's go back in history. The introduction of scalar type hints was a significant breakthrough in PHP 7. It almost didn't happen. Andreu Faulds, the author of the ingenious solution, which was entirely backwards compatible and optional thanks to declare(strict_types=1), was harshly rejected by the community. Fortunately, Anthony Ferrara defended her and the proposal at the time, launched a campaign and the RFC passed very closely. Whew. Most of the news in PHP 8 are courtesy of the legendary Nikita Popov and they all passed the voting like a knife through butter. The world is changing for the better.

PHP 8 brings types to perfection. The vast majority of phpDoc annotations like @param, @return and @var will be replaced by native notation. But most importantly, types will be checked by the PHP engine. Only descriptions of structures such as string[] or more complex annotations for PHPStan will remain in the comments.

Union Types

Union types are an enumeration of two or more types that a variable can accept:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

Certain union types have been introduced to PHP before. Nullable types, for example. Such as ?string, which is equivalent to the union type string|null. Question mark notation can be considered an abbreviation. Of course, it also works in PHP 8, but you cannot combine it with vertical bars. So instead of ?string|object you have to write string|object|null. Furthermore, iterable was always equivalent to array|Traversable. You may be surprised that float is also a union type, as it accepts both int|float, but casts the value to float.

You cannot use pseudotypes void and mixed in unions because it would make no sense.

Nette is ready for union types. In Schema, Expect::from(): https://doc.nette.org/en/3.0/schema#… accepts them, and presenters also accept them. You can use them in render and action methods, for example:

public function renderDetail(int|array $id)
{
	...
}

On the other hand, autowiring in Nette DI rejects union types. So far, there is no use case where it would make sense for the constructor to accept either one or another object. Of course, if such use-case appears, it will be possible to adjust the behaviour of the container accordingly.

Methods getParameterType(), getReturnType() and getPropertyType() in Nette\Utils\Reflection throw an exception in the case of the union type (that is in version 3.1; in the earlier version 3.0, these methods do return to maintain null compatibility). Their new siblings getParameterTypes(), getReturnTypes() and getPropertyTypes() work with union types, see documentation.

mixed

The pseudotype mixed accepts any value.

In the case of parameters and properties, it results in the same behaviour as if we do not specify any type. So, what is its purpose? To distinguish when a type is merely missing and when it is intentionally mixed.

In the case of function and method return values, not specifying the type differs from using mixed. It means the opposite of void, as it requires the function to return something. A missing return then produces a fatal error.

In practice, you should rarely use it, because thanks to union types, you can specify the value precisely. It is therefore only suitable in unique situations:

function dump(mixed $var): mixed
{
	// print variable
	return $var;
}

false

Unlike mixed, you can use the new pseudotype false exclusively in union types. It arose from the need for describing the return type of native functions, which historically return false in case of failure:

function strpos(string $haystack, string $needle): int|false
{
}

Therefore, there is no true type. Neither can you use false alone, nor false|null, nor bool|false.

static

Pseudotype static can only be used as a return type of a method. It says that the method returns an object of the same type as the object itself (while self says that it returns the class in which the method is defined). It is excellent for describing fluent interfaces:

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

This type does not exist in PHP 8 and will not be introduced in the future. Resources are a relic from the time when PHP did not have objects. Gradually, resources are going to be replaced by objects. Eventually, they will disappear completely. For example, PHP 8.0 replaces the image resource with GdImage object, and the curl resource with CurlHandle object.

Stringable

It is an interface that gets automatically implemented by every object with a magic method __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

It is possible to explicitly state class Email implements Stringable in the class definition, but it is not necessary.

Nette\Utils\Html also reflects this naming schema by implementing the interface Nette\HtmlStringable instead of the former IHtmlString. Objects of this type, for example, are not escaped by Latte.

Type variance, contravariance, covariance

The Liskov Substitution Principle (LSP) states that the extension classes and interface implementations must never require more and provide less than the parent. That is, the child method must not require more arguments or accept a narrower range of types for parameters than the parent, and vice versa, it must not return a wider range of types. But it can return fewer. Why? Because otherwise, the inheritance would break. A function would accept an object of a specific type, but it would have no idea what parameters it can pass to its methods and what types they would return. Any child could break it.

So in OOP, the child class can:

  • accept a broader range of types in the parameters (this is called contravariance)
  • return a narrower range of types (covariance)
  • and properties cannot change type (they are invariant)

PHP has been able to do this since version 7.4, and all newly introduced types in PHP 8.0 also support contravariance and covariance.

In the case of mixed, the child can narrow the return value to any type, but not void, as it is doesn't represent a value, but rather its absence. The child also cannot omit a type declaration, as this also allows for an absence of value.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

Union types can also be extended in parameters and narrowed in return values:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

Furthermore, false can be extended to bool in the parameter or vice versa bool to false in the return value.

All offences against covariance/contravariance lead to a fatal error in PHP 8.0.

In the next parts of this series, we will show what the attributes are, what new functions and classes have appeared in PHP and we will introduce Just in Time Compiler.

Further reading