Exploring PHP 8.0

Larry Garfield

@Crell

Larry implements Huggable

Recent PHP History

PHP 7.0, 2015
Return types, scalar types, spaceship, null coalesce, anon classes
PHP 7.1, 2016
iterable type, Nullable types, void returns
PHP 7.2, 2017
object type
PHP 7.3, 2018
Some minor stuff
PHP 7.4, 2019
Short lambdas, typed properties, null coalesce assign, array splat
PHP 8.0, 2020
So so much...

PHP 8.0 is the biggest release since...

PHP 7.4

Type improvements

Union types


function mangleUsers(string|array $users): array
{
    If (is_string($users)) {
        $users = [$users];
    }
    // ...
}
					

Union types


function doMath(int|float $in): int|float
{
    // ...
}
					

Union types


function handleRequest(RequestInterface $req):
          RequestInterface|ResponseInterface
{
    // ...
}
					

Speciality Unions

  • null is a type, equivalent to ?
  • false is a return type only, don't use it
  • mixed ~ untyped, so use mixed

static returns


interface TaskBuilder
{
    public function addStep(Step $s): static;
}

class  ImportantTask implements TaskBuilder
{
    public function addStep(Step $s): static
    {
        $this->steps[] = $s;
        return $this;
    }
}
        

addStep() returns ImportantTask, not TaskBuilder.

Stringable


interface Stringable {
  public function __toString(): string;
}
					

function show_message(string|Stringable $message): void
{
    // ...
}
					

It's automated!

Improved string functions

str_*_with()


$needle = 'hello';

if (str_starts_with('hello world', $needle)) {
    print "This must be your first program.";
}
					

$needle = 'world';

if (str_ends_with('hello world', $needle)) {
    print "You say hello to everyone else but not me?";
}
					

str_contains()


$needle = 'on';
if (str_contains('peace on earth', $needle)) {
    print "Yes, let's do that.";
}
					

Never (screw up) writing this again


$needle = 'on';
if (strpos('peace on earth', $needle) == false) {
    print "You have a bug!";
}
					

Numeric strings

A string that is probably close enough to a number that we can
convert it to one automagically and call it a day

Sticks of dynamite tied to a timer

"time bomb" by dkshots is licensed under CC BY-NC 2.0

In PHP 7.4...


0 == "wait, what?";           // true
0 == "";                      // true
99 == '99 bottles of beer';   // true
42 == '     42';              // true
42 == '42     ';              // false, only sometimes
in_array(0, ['a', 'b', 'c']); // true
					
Wat

PHP 8.0

Numeric string:

trim($string)

Whitespace is ignored at the start and end, consistently

Behavior

Comparison

" 42 " < $a_num => Compare as numbers

'abc' < $a_num => Compare as strings

Function args

expects_int(" 42 "); => OK in weak mode, error in strict

expects_int("42 is the answer"); => always type error

Now go enable declare(strict_types=1);

Match expressions

"Burning Match Novoflex adaptor NX NIK 85mm Samsung NX200" by zen whisk is licensed under CC BY-ND 2.0

Have you done this?


$display = $user->isAdmin()
    ? $user->name() . ' (admin)'
    : $user->name() . ' (muggle)'
;
        
  • Clean branching expression
  • Encourages simple expressions
  • Encourages factoring out to functions
  • Only works for true/false :-(

match() expressions


$display = match($user->isAdmin()) {
    true  => $user->name . ' (admin)',
    false => $user->name . ' (muggle)',
};
        
  • === match
  • Returns expression
  • No fall-through
  • Must be exhaustive

This...


switch ($var) {
    case 'a':
        $message = "The variable was a.";
        break;
    case 'b':
        $message = "The variable was c.";
        break;
    case 'c':
        $message = "The variable was c.";
        break;
    default:
        $message = "The variable was something else.";
        break;
}
        

Becomes this


$message = match($var) {
    'a' => 'The variable was a',
    'b' => 'The variable was b',
    'c' => 'The variable was c',
    default => 'The variable was something else',
};
        

Compound arms


echo match($operator) {
    '+', '-', '*', '/' => 'Basic arithmetic',
    '%' => 'Modulus',
    '!' => 'Negation',
};
        

Complex matches


$count = get_count();
$size = match(true) {
    $count > 0 && $count <=10 => 'small',
    $count <=50 => 'medium',
    $count >50 => 'huge',
};
        

Nullsafe methods

Null: The billion dollar mistake

100 billion dollar bill

"100 Billion Dollars" by Peat Bakke is licensed under CC BY 2.0

Cleaning up someone else's null


$user = get_user($id);
if (!is_null($user)) {
    $address = $user->getAddress();
    if (!is_null($address)) {
        $state = $address->state;
        if (!is_null($state)) {
            // And so on.
        }
    }
}
          

Eeew

PHP 8.0


$state = get_user($id)?->getAddress()?->state;
        

Short-circuiting nulls


$bestSaleItem = $catalog
          ?->getProducts(get_seasonal_type())
          ?->mostPopular(10)[5];
          
  • get_seasonal_type() not called if $catalog is null
  • Doesn't work on the array from mostPopular()

Moar warnings and errors

From warning to error exception

Modify property of non-object
$not_object->foo = 'bar'
Writing to scalar value as array
$an_int[] = 4;
Invalid argument for foreach()
foreach($an_int as $x) {}
foreach($not_traversable as $x) {}
Illegal offset
print $arr[$an_object]
Division by zero
$val = 5/0

From notice to warning

Array offset on non-array
$an_int[4]
Access property of non-object
$a_string->name
Array to string conversion
print $an_array
Undefined property
$object->does_not_exist
Undefined variable
print $does_not_exist

Undefined variables are now Warnings!

Moar commas

2017

RFC: Allow trailing commas in...

  • function arguments
  • function declaration
  • grouped namespaces
  • implements clause for interfaces
  • use clauses for traits
  • class property list
  • closure use statement

¯\_(ツ)_/¯

PHP 7.3

RFC: Allow a trailing comma in function arguments

Passed 30:10

¯\_(ツ)_/¯

PHP 8.0

Trailing commas in function definition: Done

Trailing commas in closure use: Done

¯\_(ツ)_/¯

Commas!


class Address
{
    public function __construct(
        string $street,
        string $number,
        string $city,
        string $state,
        string $zip,     // This is new.
    ) {
    // ...
    }
}
        

More Commas!


$a ='A';
$b = 'B';
$c = 'C';

$dynamic_function = function(
  $first,
  $second,
  $third, // This is new, as before.
) use (
  $a,
  $b,
  $c,     // This is also new.
);
        

Constructor Promotion

PHP 7.4 and earlier

Drake disapproves of this code

class FormRenderer
{
    private ThemeSystem $theme;
    private UserRepository $users;
    private ModuleSystem $modules;

    public function __construct(
        ThemeSystem $theme,
        UserRepository $users,
        ModuleSystem $modules
    ) {
        $this->theme = $theme;
        $this->users = $users;
        $this->modules = $modules;
    }

    public function render(Form $form): string
    {
        // ...
    }
}
          

PHP 8.0

Drake approves of this code

class FormRenderer
{
    public function __construct(
        private ThemeSystem $theme,
        private UserRepository $users,
        private ModuleSystem $modules,
    ) { }

    public function renderForm(Form $form): string
    {
        // ...
    }
}
          

Mix and match


class FormRenderer
{
    private User $currentUser;

    public function __construct(
        private ThemeSystem $theme,
        private UserRepository $users,
        private ModuleSystem $modules,
    ) {
        $this->currentUser = $this->users->getActiveUser();
    }

    public function renderForm(Form $form): string
    {
        // ...
    }
}
        

Trivial value objects


Class MailMessage
{
    public function __construct(
        public string $to,
        public string $subject,
        public string $from,
        public string $body,
        public array $cc,
        public array $bcc,
        public string $attachmentPath,
    ) {}
}
        

Meta(meta(data))

(~Doctrine Annotations but in core)

Attributes


#[GreedyLoad]
class Product
{
    #[Positive]
    protected int $id;

    #[Admin]
    public function refillStock(int $quantity): bool
    {
        // ...
    }
}
        

Example: Tukio


function my_listener(MyEvent $event): void { ... }

$provider = new OrderedListenerProvider();

// How you normally add a listener to a provider.
$provider->addListener('my_listener', 5, 'listener_a', MyEvent::class);
        

Step 1: Define attribute


namespace Crell\Tukio;

use \Attribute;

#[Attribute]
class Listener implements ListenerAttribute
{
   public function __construct(
       public ?string $id = null,
       public ?string $type = null,
   ) {}
}
        

Step 1.5: Restrict attribute


namespace Crell\Tukio;

use \Attribute;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class Listener implements ListenerAttribute
{
   public function __construct(
       public ?string $id = null,
       public ?string $type = null,
   ) {}
}
        

Step 2: Tag it


use Crell\Tukio\Listener;

#[Listener('listener_a')]
function my_listener(MyEvent $event): void { ... }

$provider = new OrderedListenerProvider();

$provider->addListener('my_listener', 5);
        

Step 3: Interpret it


Use Crell\Tukio\Listener;

/// A string means it's a function name,
// so reflect on it as a function.
if (is_string($listener)) {
    $ref = new \ReflectionFunction($listener);
    $attribs = $ref->getAttributes(
        Listener::class,
        \ReflectionAttribute::IS_INSTANCEOF,
    );
    $attributes = array_map(
        fn(\ReflectionAttribute $attrib) => $attrib->newInstance(),
        $attribs,
    );
}
        

Symfony 5.2


use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

class SomeController
{
    #[Route('/path', 'action_name')]
    public function index(#[CurrentUser] MyUser $user)
    {
        // ...
    }
}
        

Promoted parameter attributes


class Point
{
    public function __construct(
        #[Positive]
        public int $x,
        #[Positive]
        public int $y
    ) {}
}
        

Applies to parameter and property

Arguments

Two hardest problems in computer science

  1. Naming things
  2. Cache invalidation
  3. Off by one errors

What does this do?

Drake disapproves of this code

$new = array_fill(0, 100, 50);
          
  • 50, 100 times?
  • 100, 50 times?

That's what this does!

Drake approves of this code

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

Any order


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

Not allowed


array_fill(
  value: 50,
  0,
  count: 100,
);
          

Variadics


// Indexed array, so the values will map to indexed parameters.
$params = [0, 100, 50];
array_fill(...$params);

// Named array, so the values will map to named parameters.
$params = ['count' => 100, 'start_index' => 0, 'value' => 50];
array_fill(...$params);
          

Dynamic arguments


$args['value'] = $request->get('val') ?? 'a';
$args['start_index'] = 0;
$args['count'] = $config->getSetting('array_size');

$array = array_fill(...$args);
            

Value objects


class Url implements UrlInterface
{
    public function __construct(
        private string $scheme = 'https',
        private ?string $authority = null,
        private ?string $userInfo = null,
        private string $host = '',
        private string $host = '',
        private ?int $port = 443,
        private string $path = '',
        private ?string $query = null,
        private ?string $fragment = null,
    ) {}
}
        

How are you going to call?


// PHP <= 7.4
$url = new Url('https', null, null, 'platform.sh', 443, '/blog', null, 'latest');
          

// PHP 8.0
$url = new Url(host: 'platform.sh', path: '/blog', fragment: 'latest');
          

// PHP 8.0
$url = new Url(
    path: '/blog',
    host: 'platform.sh',
    fragment: 'latest'
);
          

cf: Improving PHP's object ergonomics

Attributes


class SomeController
{
    #[Route(
          path: '/path',
          name: 'action',
    )]
    public function someAction()
    {
        // ...
    }
}
        

The most important use case...


if (in_array(haystack: $arr, needle: 'A')) {
    // ...
}
        

¯\_(ツ)_/¯

Caveat coder

Parameter names are now part of your interface contract

Oh, yeah, JIT

In summary...

  • You need to write half as much code
  • Your code is more self-descriptive
  • Undefined variables are always a warning
  • $needle/$haystack is no longer a thing
That's PHP 8.0

Larry Garfield

@Crell

Director of Developer Experience Platform.sh

Stalk us at @PlatformSH

Get the full book!

https://platform.sh/php8-ebook/

Cover of Thinking Functionally in PHP

Buy my book!

https://bit.ly/fn-php

Cover of Thinking Functionally in PHP