Programster's Blog

Tutorials focusing on Linux, programming, and open-source

What's New In PHP 8

PHP

Try It Out

As you find out about these features, I welcome you to use this site to try out the new features in a web browser, for yourself.

Non-capturing Catches

You no longer have to provide a variable whenever catching exceptions! This is fantastic for me as I love using custom exceptions to capture all the various ways things can go wrong.

catch (ExceptionMissingRequiredInput)

... instead of:

catch (ExceptionMissingRequiredInput $missingRequiredInputException)

To see why this is useful, consider using the following code and the warnings you would otherwise get in your IDE for unused variables for the caught exceptions:

<?php
function foo
{
    try
    {
        if (!isset($_GET['id']))
        {
            throw new ExceptionMissingRequiredInput('id');
        }

        $id = $_POST['id'];
        $foo = Foo::fetchById($id); // throws ExceptionFooNotFound if model with that ID does not exist.
        $httpResponse = new JsonResponse($foo, 200);
    }
    catch (ExceptionMissingRequiredInput)
    {
        $httpResponse = new JsonResponse(['error' => "You are missing the required ID parameter."], 400);
    }
    catch (ExceptionFooNotFound)
    {
        $httpResponse = new JsonResponse(['error' => "Foo with that ID does not exist."], 404);
    }
    catch (Exception)
    {
        $httpResponse = new JsonResponse(['error' => "Whoops, something went wrong!"], 500);
    }

    return $httpResponse;
}

If you want to catch all exceptions and errors, you can use Throwable as the catching type. E.g.

catch (Throwable)

Details of throwable errors.

Stringable Interface

The Stringable interface can be used to type hint anything that implements __toString(). Whenever a class implements __toString(), it automatically implements the interface behind the scenes and there's no need to manually implement it.

This is really useful to anyone who likes making use of object views.

<?php

declare(strict_types = 1);

class HtmlTemplateView implements Stringable
{
    private $m_content;


    public function __construct(Stringable | string $content)
    {
        $this->m_content = $content;
    }


    public function render()
    {
        print "<!DOCTYPE html>
<html>
  <head>
    <meta charset=\"UTF-8\">
    <title>title</title>
  </head>
  <body>
    {$this->m_content}
  </body>
</html>"
    }


    public function __toString()
    {
        return $this->render();
    }
}

$data = [
    ['column1', 'column2'],
    [1, 2],
    [4, 5],
];

$myTable = new HtmlTableView($data); // HtmlTableView implements Stringable
print new HtmlTemplateView($myTable);

Technically the class above does not need to declare the implements Stringable because anything that has the __toString will implicitly declare it, so that it can be used in other methods that use Stringable as a parameter type.

Named arguments

Ever had loads of optional parameters on a method? E.g. how about a random password generator like so:

public static function generateRandomPassword(
    int $length, 
    $includeCapitals=true, 
    $includeNumbers=true, 
    $includeSpecialCharacters=true
);

Now if you want to generate a random password, but do not wish to include special characters you can just call:

$newPassword = generateRandomPassword(12, includeSpecialCharacters: false);

... instead of:

$newPassword = generateRandomPassword(12, true, true, false);

.. and it's a lot more readable now.

Better yet, you can change the order now, and is especially readable:

$newPassword = generateRandomPassword(
    12, 
    includeSpecialCharacters: false, 
    includeCapitals: false, 
    includeNumbers: false
);

Union Types

You can now use the | symbol to say that a parameter or return type can be of multiple types.

public function foo(Foo|Bar $input): int|float;

Most noteably, this does allow the inclusion of null, but not void.

Match Expression

The match expression allows you to easily set a variable based on a "lookup table" of other values, without having to use an array or switch. Here's an example:

$result = match ("1") {
    0 => 'Foo',
    1 => 'Bar',
    "1" => 'String Bar'
    2 => 'Baz',
    default => throw new Exception("Unknown input")
};

print $result; // outputs "String Bar"

The match statement uses strict type comparisons, so "1" is will not match up against integer 1.

You can use multiple values to match against as well. E.g.

$input = 2;

$result = match($input) {
    0 => "hello",
    1,2,3 => "integer world",
    '1', '2', '3' => "string world",
    default => throw new Exception("Unknown input")
};

echo $result; // outputs "integer world"

You don't have to provide a default, I just really like to use it.

Traits Can Now Have Abstract Methods!

Traits can declare abstract methdos, forcing the class that uses them to fill in the relevant body. E.g.

trait TraitLoadFromArray {
    abstract public static function load(array $row): static;
}

class MyModel
{
    use TraitLoadFromArray;

    private function __construct(array $data)
    {
        // do someething with the data here.
    }

    public static function load(array $input) : static
    {
        return new MyModel($input);
    }
}

$model = MyModel::load(['foo' => 'bar']);

This makes them a middle ground between interfaces and abstract classes. This is because traits can have some filled in methods as well, making them like an abstract class, but a single class can use/implement multiple traits, but can't extend multiple classes. However, unlike an interface, traits on their own cannot be used as a typehint for a parameter/return type.

Also, PHP 8 will perform proper method signature validation when using traits, which is actually a huge bonus, not a caveat.

E.g. this won't work because the trait insists on an array input:

trait TraitLoadFromArray {
    abstract public static function load(array $row): static;
}

class MyModel
{
    use TraitLoadFromArray;

    private function __construct(array $data)
    {
        // do someething with the data here.
    }

    public static function load($input) : static
    {
        return new MyModel($input);
    }
}

$model = MyModel::load(['foo' => 'bar']);

If a class implements two traits that share a method name, a fatal error occurs.

Parameter Lists - Trailing Comma

In PHP 5.6 they added the ability to have a trailing comma in arrays. Now they have done the same for when providing arguments to a function like so:

$db = new mysqli(
    $host,
    $user,
    $password,
    $dbname,
    $port,
);

Static Return Type

You can now use static as a return type. You could have always called static($x) to create a child object from an parent class, but now you can specify that the return is going to be whatever the child class is too, which will help your IDE. Reference.


abstract class Animal { public static function createFromName($name): static { return new static($name); } } class Dog extends Animal { protected function __contruct(array $data) { print "Creating dog with data: " . print_r($data, true); $this->m_data = $data; } public function bark() { print "Woof!" . PHP_EOL; } } $dog = Dog::createFromName('Winston'); $dog->bark(); // outputs Woof! print $dog::class; // outputs Dog, more importantly, your IDE knows its Dog, not Animal

Nullsafe Operator - Now Works On Methods

Imagine that in the below example, the booking is a database model object with an accessor on a nullable deletedAt property that only gets set if it was deleted. This deletedAt property is a DateTime object that has the getTimestamp method.

$deletedAtTimestamp = $booking->getDeletedAt()?->getTimestamp();

This will result in setting $deletedAtTimestamp as either the timestamp if it exists, or null.

Private Methods And Inheritance

We can have private methods in child/parent classes with different method signatures.

E.g.

class Foo
{
    public foo() { return $this->bar(); }
    private bar() { return "bar"; }
}

class Bar extends Foo
{
    public foo() { return $this->bar("yolo"); }
    private bar(Bar $bar) { return $bar; }
}

However, I see this as quite dangerous, because you might accidentally try to do this:

class Foo
{
    public foo() { return $this->bar(); }
    private bar() { return "bar"; }
}

class Bar extends Foo
{
    private bar(Bar $bar) { return $bar; }
}

$bar = new Bar();
$bar->foo();

I can't believe I haven't hit this before, but I guess it's because I always try to use composition instead of inheritance wherever possible, and if I'm having a child and parent with the same method name, I'm probably going to make it protected/public as part of an interface.

Throw Is Now An Expression, Not A Statement

This just means that you can now throw an Exception in more places. E.g.

$triggerError = fn () => throw new MyError();

$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');

Weak Maps

The WeakMap has been introduced that is an object that holds references to other objects, but won't stop those objects being garbage collected if they are the only thing that references those objects. This is particularly useful in the realm of ORMs that hold caches of objects/entities.

class Foo extends Model
{
    private static WeakMap $s_cache;

    public static function fetchById(int $id): Foo
    {
        if (!isset(self::$s_cache[$id]))
        {
            $row = self::getDb()->query("SELECT * FROM `foo` WHERE `id` = $id);

            if ($row === false)
            {
                throw new ExceptionModelDoesNotExist();
            }

            self::$s_cache[$id] = self::hydrate($row);
        }

        return self::$s_cache[$id];
    }
}

::class on Objects

You can now do:

$foo = new Foo();
print $foo::class

... instead of:

$foo = new Foo();
print get_class($foo)

str_contains, str_starts_with, and str_ends_with

There are now built-in functions for checking if a string is within another string, if a string starts with another string, or if a string ends with another string. This isn't that much of a big deal for me as I wrote all of these into my core libs package's StringLib class years ago, but for most people this is long overdue.

var_dump(str_contains('hello world', 'orld')); // true
var_dump(str_contains('hello world', 'yolo')); // false

var_dump(str_starts_with('hello world', 'hello')); // true
var_dump(str_starts_with('hello world', 'world')); // false

var_dump(str_ends_with('hello world', 'hello')); // false
var_dump(str_ends_with('hello world', 'world')); // true

Constructor Property Promotion

Parameters inside constructors will automatically

class Transaction 
{ 
    public function __construct(
        Currency $currency,
        float $amount,
    ) {
    }
}

$transaction = new Transaction(Currency::createEuros(), 3.50);
print $transaction->amount;

However, for the purposes of defensive programming, and readability, one really should do the following instead:

class Transaction 
{  
    public function __construct(
        private Currency $currency,
        private float $amount,
    ) {
    }

    # Accessors
    public function getCurrency() : Currency { return $this->currency; }
    public function getAmount() : float { return $this->amount; }
}

$transaction = new Transaction(Currency::createEuros(), 3.50);
print $transaction->getAmount();

It is worth mentioning that you can immediately access the promoted attribute inside the constructor. E.g.

class Transaction 
{  
    public function __construct(
        private Currency $currency,
        private float $amount,
    ) {
        if ($this->amount < 10) { throw new Exception("Transaction is too small"); }
    }
}

Caveat 1 - Redeclaration Issue

Unfortunately, you cannot make use of the constructor promotion syntax, and still declare the types at the top of the class, otherwise you get an exception about redeclaring your attributes, which makes this feature rather pointless for me as I really like seeing all of the attributes right at the top of the class.

E.g. you can't do the following:

class Transaction 
{  
    private Currency $currency;
    private float $amount;


    public function __construct(
        private Currency $currency,
        private float $amount,
    ) {
    }

    # Accessors
    public function getCurrency() : Currency { return $this->currency; }
    public function getAmount() : float { return $this->amount; }
}

$transaction = new Transaction(Currency::createEuros(), 3.50);
print $transaction->getAmount();

Caveat 2 - Not Allowed In Abstract Constructors

I don't know why this is a thing since you can essentially do this the "normal" way around, but for some reason this doesn't work on abstract constructors like so:

abstract class AbstractController
{
    public function __construct(
        protected Request $request, 
        protected Response $response, 
        protected $args
    ){

    }
}

Caveat 3 - Simple Defaults Only

Simple defaults are allowed, but not new, but this is not much of a caveat as I'm pretty sure you can't do this the "normal" way either. E.g.

public function __construct(
    public string $name = 'Brent',
    public Bar $bar = new Bar('bar'),
) {}

Attributes

I have a feeling this feature is going to be a lot more useful than I understand for now. Read up about them here.

Mixed Return Type

I am only including this because it is new in PHP 8, and not because I think that anybody should use it. It's always worth knowing what is there, in case there is some niche case where you might need it, but I would say that 99% of the time, there is a better alternative.

The mixed return type means that the returned value is any one of the following:

  • array
  • bool
  • callable
  • int
  • float
  • null
  • object
  • resource
  • string
function foo() : mixed

References

Last updated: 18th February 2021
First published: 24th January 2021