PHP 8 - try out all new features

Published on

PHP 8 - try out all new features

PHP 8 is already in it's release candidate stage, with RC 3 being released on October 29th, and the general availability release targeted for November 26th. So it is time to take a look at all the new and upcoming features of PHP 8. You can take a look at PHP 8's release schedule here.

UPDATE: Free video course If you're more of a visual learner, check out my entirely free video course, covering all new features in PHP 8.

Every feature that you see in this blogpost comes with an interactive embedded editor, where you can modify the PHP code and evaluate the results yourself.

The official upgrade guide can be found on GitHub.

Editing and evaluating the code requires cross-site cookies. If you have those disabled, you can click "Edit on Laravel Playground" to jump into the code examples and edit them.

New Features

Added support for union types (RFC)

A union type accepts values of multiple different types, rather than a single one.

<?php
declare(strict_types=1);
 
class Number {
    private int|float $number;
 
    public function setNumber(int|float $number): void {
        $this->number = $number;
    }
 
    public function getNumber(): int|float {
        return $this->number;
    }
}

/**
 * We can pass both floats or integer values
 * to the number object. Try passing a string.
 */
$number = new Number();

$number->setNumber(5);

dump($number->getNumber());

$number->setNumber(11.54);

dump($number->getNumber());

exit;

Added WeakMap (RFC)

Weak maps allow creating a map from objects to arbitrary values (similar to SplObjectStorage) without preventing the objects that are used as keys from being garbage collected. If an object key is garbage collected, it will simply be removed from the map.

This is a very useful new feature, as it allows users to worry even less about leaving memory leaks in their code. While this might not be an issue for most PHP developers, it is certainly something to watch out for when writing long-running processes, for example using ReactPHP. With WeakMaps, references to an object automatically get garbage collected, once the object is no longer available.

If you would do the same thing with an array, you would still have a reference to the object, thus leaking memory.

<?php
declare(strict_types=1);
 

class FooBar {
    public WeakMap $cache;
    
    public function __construct() {
    	$this->cache = new WeakMap();
    }
 
    public function getSomethingWithCaching(object $obj) {
        return $this->cache[$obj] ??= $this->computeSomethingExpensive($obj);
    }
    
    public function computeSomethingExpensive(object $obj) {
		dump("I got called");
		return rand(1, 100);
    }
}

$cacheObject = new stdClass;

$obj = new FooBar;
// "I got called" only will be printed once
$obj->getSomethingWithCaching($cacheObject);
$obj->getSomethingWithCaching($cacheObject);

dump(count($obj->cache));

// When unsetting our object, the WeakMap frees up memory
unset($cacheObject);

dump(count($obj->cache));

exit;

New ValueError Exception

PHP 8 introduces a new built-in exception class called ValueError. It extends \Exception and PHP throws this exception, every time you pass a value to a function, which has a valid type - but can not be used for the operation. Prior to PHP 8, this would result in a warning.

Here are some examples:

<?php
declare(strict_types=1);
 
/**
 * We pass an array to array_rand,
 * which is of the correct type. But
 * array_rand expects non-empty arrays.
 *
 * This throws a ValueError exception.
 */
array_rand([], 0);

/**
 * The depth argument for json_decode is a
 * valid integer, but it must be greater than 0
 */
json_decode('{}', true, -1);

Allowing variadic argument when overriding functions

Any number of function parameters may now be replaced by a variadic argument, as long as the types are compatible. For example, the following code is now allowed:

<?php
declare(strict_types=1);

class A {
    public function method(int $many, string $parameters, $here) {

    }
}
class B extends A {
    public function method(...$everything) {
        dd($everything);
    }
}

$b = new B();
$b->method('i can be overwritten!');
exit;

Static return type (RFC)

The static return type may now be used in PHP 8 to identify that a method returns the class, that this method was actually called on - even if it was inherited (late static binding).

<?php
declare(strict_types=1);

class Test {
    public function doWhatever(): static {
        // Do whatever.
        return $this;
    }
}

exit;

Class name literal on object (RFC)

It is now possible to fetch the class name of an object using $object::class. The result is the same as get_class($object).

<?php
declare(strict_types=1);

auth()->loginUsingId(1);

dump(auth()->user()::class);

// Or with a temporary variable
$user = auth()->user();

dump($user::class);
exit;

Variable Syntax Tweaks (RFC)

New and instanceof can now be used with arbitrary expressions, using new (expression)(...$args) and $obj instanceof (expression).

<?php
declare(strict_types=1);

class Foo {}
class Bar {}


$class = new (collect(['Foo', 'Bar'])->random());

dd($class);

exit;

Stringable interface (RFC)

PHP 8 introduces a new Stringable interfaces, which gets automatically added, once a class implements the __toString method. You do not need to explicitly implement this interface.

<?php
declare(strict_types=1);

class Foo {
    public function __toString() {
        return 'I am a class';
    }
}

$obj = new Foo;
dump($obj instanceof Stringable);

exit;

Traits can now define abstract private methods (RFC)

<?php
declare(strict_types=1);


trait MyTrait {
    abstract private function neededByTheTrait(): string;
 
    public function doSomething() {
        return strlen($this->neededByTheTrait());
    }
}
 
class TraitUser {
    use MyTrait;
 
    // This is allowed:
    private function neededByTheTrait(): string { }
 
    // This is forbidden (incorrect return type)
    // private function neededByTheTrait(): stdClass { }
 
    // This is forbidden (non-static changed to static)
    // private static function neededByTheTrait(): string { }
}

exit;

throw can now be used as an expression (RFC)

The throw statement can now be used in places where only expressions are allowed, like arrow functions, the coalesce operator and the ternary/elvis operator.

<?php
declare(strict_types=1);

$callable = fn() => throw new Exception();

$nullableValue = null;

// $value is non-nullable.
$value = $nullableValue ?? throw new \InvalidArgumentException();


exit;

An optional trailing comma is now allowed in parameter lists (RFC)

Similar to the trailing comma in arrays, you can now define a trailing comma in a parameter list.

<?php
declare(strict_types=1);

function method_with_many_arguments(
    $a, 
    $b,
    $c,
    $d,
) {
	dump("this is valid syntax");
}

method_with_many_arguments(
    1,
    2,
    3,
    4,
);

exit;

Catching exceptions without storing in a variable (RFC)

It is now possible to write catch (Exception) to catch an exception without storing it in a variable.

<?php
declare(strict_types=1);

$nullableValue = null;

try {
	$value = $nullableValue ?? throw new \InvalidArgumentException();
} catch (\InvalidArgumentException) {
	dump("Something went wrong");
}


exit;

Added support for mixed type (RFC)

PHP 8 introduces a new type called mixed that you can use. A type of mixed would be equivalent to array|bool|callable|int|float|null|object|resource|string.

<?php
declare(strict_types=1);

function debug_function(mixed ...$data) {
	dump($data);
}

debug_function(1, 'string', []);

exit;

Added support for Attributes

PHP 8's attributes actually consist of a number of RFCs:

Attributes are definitely one of the biggest changes in PHP 8, and they can be a bit tricky to understand at first. In short, attributes allow you to add meta-data to PHP functions, parameters, classes, etc. This meta-data can then be retrieved programatically - whereas in PHP 7 and lower, you would need to parse doclocks, attributes allow you to access this information deeply integrated into PHP itself.

To make this a bit clearer, lets say that you want to allow users to add a middleware to a controller class/method by using an attribute.

<?php
declare(strict_types=1);
// First, we need to define the attribute. An Attribute itself is just a plain PHP class, that is annotated as an Attribute itself.

#[Attribute]
class ApplyMiddleware
{
    public array $middlware = [];

    public function __construct(...$middleware) {
        $this->middleware = $middleware;
    }
}

// This adds the attribute to the MyController class, with the "auth" middleware as an argument.

#[ApplyMiddleware('auth')]
class MyController
{
    public function index() {}
}

// We can then retrieve all ApplyMiddleware attributes on our class using reflection
// And read the given middleware arguments.

$reflectionClass = new ReflectionClass(MyController::class);

$attributes = $reflectionClass->getAttributes(ApplyMiddleware::class);

foreach ($attributes as $attribute) {
    $middlewareAttribute = $attribute->newInstance();
    dump($middlewareAttribute->middleware);
}

exit;

Added support for constructor property promotion (RFC)

This RFC proposes to introduce a short hand syntax, which allows combining the definition of properties and the constructor:

<?php
declare(strict_types=1);

class User {
    public function __construct(
        public int $id,
        public string $name,
    ) {}
}

$user = new User(1, 'Marcel');

dump($user->id);
dump($user->name);

exit;

Added support for match expression. (RFC)

This RFC proposes adding a new match expression that is similar to switch but with safer semantics and the ability to return values.

<?php
declare(strict_types=1);

echo match (1) {
    0 => 'Foo',
    1 => 'Bar',
    2 => 'Baz',
};

exit;

Added support for nullsafe operator (?->) (RFC)

When the left hand side of the operator evaluates to null the execution of the entire chain will stop and evalute to null. When it is not null it will behave exactly like the normal -> operator.

<?php
declare(strict_types=1);

class User {
	public function getAddress() {}
}

$user = new User();

$country = $user?->getAddress()?->country?->iso_code;

dump($country);

exit;

Added support for named arguments (RFC)

Named arguments allow passing arguments to a function based on the parameter name, rather than the parameter position. This makes the meaning of the argument self-documenting, makes the arguments order-independent, and allows skipping default values arbitrarily.

<?php
declare(strict_types=1);

array_fill(start_index: 0, num: 100, value: 50);

exit;