PHP 8.0: News in Data Types (2/4)
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/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).
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.
Sign in to submit a comment