What’s New in PHP 8.0

__construct( public int $num ) — declaring properties in the constructor

wiki: https://wiki.php.net/rfc/constructor_promotion
doc: https://www.php.net/manual/ru/language.oop5.decon.php#language.oop5.decon.constructor.promotion

This novelty allows writing less boilerplate code for defining and initializing properties.

class Point {
	public function __construct(
		public float $x = 0.0,
		public float $y = 0.0,
		public float $z = 0.0,
	) {
	}
}

Previously the same was written like this:

class Point {
	public float $x;
	public float $y;
	public float $z;

	public function __construct(
		float $x = 0.0,
		float $y = 0.0,
		float $z = 0.0
	) {
		$this->x = $x;
		$this->y = $y;
		$this->z = $z;
	}
}

If the constructor argument declaration includes a visibility modifier (public, protected, private), PHP interprets it as both a constructor argument and a property of the object, and will automatically assign the value passed to the constructor to the property.

The constructor code will execute after all arguments have been assigned to all corresponding properties.

If no additional logic is intended, the constructor body can be left empty.

func(foo: 'bar') — named arguments (parameters)

wiki: https://wiki.php.net/rfc/named_params

Pros:

  • Allow skipping default values.
  • The order of arguments is not important.
  • Args are self-documenting.

Cons:

  • You cannot just rename a function/method parameter. Now it matters, since it is specified at call time.

Example:

// PHP < 8:
array_fill(0, 100, 50);

// PHP 8+:
array_fill(start_index: 0, num: 100, value: 50);

// Or you can break the order
array_fill(value: 50, num: 100, start_index: 0);

You can mix named and unnamed parameters:

htmlspecialchars($string, double_encode: false);
// Also equivalent to:
htmlspecialchars($string, ENT_COMPAT|ENT_HTML401, 'UTF-8', false);

An example of how auto-documentation is produced:

array_slice($array, $offset, $length, true);
// and
array_slice($array, $offset, $length, preserve_keys: true);

Unpacking an array with parameters:

$input = [
	'start_index' => 5,
	'num' => 100,
	'value' => 50,
];
array_fill(...$input);

// or like this (positional parameters can be specified without a key)
$input = [
	5,
	'num' => 100,
	'value' => 50,
];
array_fill(...$input);

Important: If the array contains an element (key) that is not among the function parameters, you will get an error.

Nullsafe (?) — null-check operator

wiki: https://wiki.php.net/rfc/nullsafe_operator

Instead of checking for null, you can use a chain of calls with the new Nullsafe operator. If any element of the sequence returns null, execution is interrupted and the entire sequence returns null.

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

Previously the same was written like this:

$country = null;

if ( $session !== null ) {
  $user = $session->user;

  if ( $user !== null ) {
	$address = $user->getAddress();

	if ( $address !== null ) {
	  $country = $address->country;
	}
  }
}

This operator is also called:

  • null-safe
  • safe navigation
  • optional chaining
  • null-conditional

match — equivalent to switch or if...elseif...else

wiki: https://wiki.php.net/rfc/match_expression_v2

Analogous to the switch operator, the match expression takes an input expression that is compared to specified values.

Unlike switch:

  • It uses strict comparison ===.
  • Returns a result.
  • Executes only one, the first matching, branch of code, whereas in switch execution falls through from the matched condition to the first encountered break.

If the tested expression does not match any of the conditions, an UnhandledMatchError exception is thrown.

Usage example:

$food = 'cake';

$return_value = match( $food ){
	'apple'  => 'Apple',
	'banana' => 'Banana',
	'cake'   => 'Cake',
};

echo $return_value; //> Cake

Comparison with switch:

// Before
switch( $this->type ){
	case T_SELECT:
		$statement = $this->SelectStatement();
		break;

	case T_UPDATE:
		$statement = $this->UpdateStatement();
		break;

	case T_DELETE:
		$statement = $this->DeleteStatement();
		break;

	default:
		$this->syntaxError( 'SELECT, UPDATE or DELETE' );
		break;
}

// After
$statement = match( $this->type ){
	T_SELECT => $this->SelectStatement(),
	T_UPDATE => $this->UpdateStatement(),
	T_DELETE => $this->DeleteStatement(),
	default => $this->syntaxError('SELECT, UPDATE or DELETE'),
};
match lazily checks for matches and lazily runs value handlers.

The left side executes sequentially until a suitable condition is found.
The right side executes only if the left condition fired (matched).

Example:

$result = match ($x) {
	foo() => foo_val(), // foo_val() will not execute if $x === $this->bar() or $this->baz
	$this->bar() => ..., // $this->bar() will not be executed if $x === foo()
	$this->baz => beep(), // beep() will be executed only if $x === $this->baz
	// etc.
};
Multiple conditions in match.

In this case they should be separated by commas. Multiple conditions work on the OR principle and are effectively a shorthand form for cases where several conditions should be handled identically.

$result = match( $x ){
	// Multiple condition:
	$a, $b, $c => 5,
	// Equivalent to three single ones:
	$a => 5,
	$b => 5,
	$c => 5,
};
default value:
$result = match( $x ){
	1, 2 => foo(),
	3, 4 => bar(),
	default => baz(),
};
Using match to check complex conditions

match can be used for any expressions that return a boolean value. In this case a true expression is passed as the input:

$age = 23;

$result = match( true ){
	( $age >= 65 ) => 'elder',
	( $age >= 25 ) => 'adult',
	( $age >= 18 ) => 'of legal age',
	default => 'child',
};

echo $result; //> adult
Using match to branch based on string content
$text = 'Bienvenue chez nous';

$result = match( true ){
	str_contains( $text, 'Welcome' ) || str_contains( $text, 'Hello' ) => 'en',
	str_contains( $text, 'Bienvenue' ) || str_contains( $text, 'Bonjour' ) => 'fr',
	// ...
};

echo $result; //> fr

foo( int|string $val ) — union types

wiki: https://wiki.php.net/rfc/union_types_v2
doc: https://www.php.net/manual/ru/language.types.declarations.php#language.types.declarations.composite.union

Instead of PHPDoc annotations for union types, you can use union type declarations, which are checked at runtime.

class Number {
	public function __construct(
		private int|float $number
	) {
	}
}

new Number('str'); // TypeError

Previously you would write like this:

class Number {
	/** @var int|float */
	private $number;

	/**
	* @param float|int $number
	*/
	public function __construct( $number ) {
		$this->number = $number;
	}
}

new Number('NaN'); // No error
Another example
function foo( int|string $val ) {
	var_dump( $val );
}

echo foo( 'one' ); // string(3) "one"
echo foo( 1 ); // int(1)

'' + 123 — Fatal Error

RFC: https://wiki.php.net/rfc/invalid_strings_in_arithmetic

Fatal when adding a number and a non-numeric string

In PHP 8 the expression like 1 + 'foo' throws a TypeError, rather than silently converting the string to 0 as before.

This is done for safety and predictability: now bugs are visible immediately.
The strict_types flag does not affect this.
Overview of PHP 8.0 new features like constructor property promotion, named parameters, nullsafe operator, match expression, union types, and arithmetic with non-numeric strings resulting in TypeError.
PHP 8.0 new features; constructor promotion; named parameters; nullsafe operator; match expression; union types