PHP 7.4: What's New and Changed

Version StatusSupported (Latest)
Release Date2019-11-28

PHP 7.4 is brings major improvements to typing to the language, and PHP 7.4 is probably the boldest release since PHP 7.0 that brings major syntax, quality-of-life, and performance improvements. I couldn't be more excited to write about it!

My favorite feature of PHP 7.4 is typed properties. It allows you to declare the types of class properties, and they are enforced at run-time. This can help you remove the getter/setter bloat, and makes room for PHP engine to make performance optimizations. This feature certainly took a lot of discussion and attempts to be in PHP, and the implementation we have with PHP 7.4 quite impressive.

In addition to typed properties, PHP 7.4 supports long awaited arrow functions. It provides a shorthand syntax to the traditional lamba functions, that can help the code look cleaner without the visual clutter of the standard function() {} boilerplate.

PHP 7.4 brings many other improvements that certainly drives PHP to a modern language, keeping up with other languages such as JavaScript and Go.

Typed Properties in PHP 7.4

TypeNew Feature

PHP 7.4 finally brings typed properties. This is a feature I have been looking forward, and I have been spending some quality time working on my existing projects to add support for typed properties.

With typed properties, you can set a type for all class properties. When a type is set, PHP engine prevents anyone from setting a different type to the class property.

class Example {
  public string $name;
  public DateTime $birthday;
}

The snippet above will make sure that Example::$birthday property will always be a DateTime object. Prior to PHP 7.4, this sort of strict data patterns would have required to have setBirthDate(DateTime $birthdate): void and getBirthDate(): \DateTime methods to enforce the data integrity.

Supported property types

Types supported for class properties.
  • Scalar types: int, string, bool, and float.
  • Compound types: array, iterable and object.
  • Any class or interface name (such as DateTime, Foo\Bar) and stdClass.
  • References to parent and own objects: self and parent.
Types not supported for class properties
  • void: Having a void property wouldn't make sense.
  • callable: Not supported because its behavior is unpredictable. Take a look at consistent callables RFC for more background. This basically becomes troublesome when callables can be declared with array syntax, e.g. as [$this, 'buildForm'].

The uninitialized state

This will be where most of the hair pulling might occur. With PHP 7.4 typed properties, class properties have an uninitialized state. This simply means that the property is not initialized yet. This is not the same as null..

If there is no type is declared, properties have null as their uninitialized value:

class Example {
  public $name;
}

$foo = new Example();
var_dump($foo->name === null); // true

When a type is declared, all properties will have an uninitialized state. It is not allowed to access class properties prior to assigning an explicit value.

class Example {
  public string $name;
}

$foo = new Example();
var_dump($foo->name === null);

In this snippet, the $name property is uninitialized. This is not the same as null, and the snippet above will throw an error:

Fatal error: Uncaught Error: Typed property Example::$name must not be accessed before initialization in ...

Check uninitialized state

You can check if a class property is uninitialized using isset($foo->name). Because this value is not the same as null, you cannot use $foo->name === null to check if the property is uninitialized.

Reset property to uninitialized state

To reset a property back to its uninitialized state, use unset($foo->name). Once unset, trying to access the property without assigning it a value will throw the same Typed property ... must not be accessed before initialization ... error.

Nullable types

Similar to PHP 7.1's nullable types, property types can be marked nullable as well. To mark a property can be null, prefix its type with a question mark, e.g: ?string.

class Example {
  public ?string $name;
}

$foo = new Example();
$foo->name = 'Ayesh'; // Valid
$foo->name = null; // Valid

Even if a property is marked nullable, its uninitialized value will not be null. For example, the snippet below will still throw an error:

class Example {
  public ?string $name;
}

$foo = new Example();
var_dump($foo->name);
// Fatal error: Uncaught Error: Typed property Example::$name must not be accessed before initialization

While this can appear cumbersome to work with, this provides a brilliant feature that you can be certain that the class property will always be of that type. If the value is not initialized, PHP will give up and throw an error instead of returning null, as it would for untyped properties.

Strict types

Class properties also support the strict type declaration with declare(strict_types=1) at the top of the file's PHP block. Without strict types, PHP will cast the values to the property type.

class Example {
  public string $name;
}

$foo = new Example();
$foo->name = 420;
var_dump($foo->name);
// string(3) "420"

Notice how we set an integer to the string property, and var_dump() call returns "420" as a string. When assigning the value, the engine casts the value to the declared type.

To minimize the problems with type juggling and to take the full benefits of typed properties, I recommend that you test your classes with declare(strict_types=1). It is easy to overlook when PHP is being helpful when it casts to a type for you, but this can be the root of some bug downstream. It's easier to debug an error that pops up immediately than a bug that only happens on Friday evenings at 6:28PM, only when DST is in effect.

Static properties and references

Static properties can have types declared too. This may seem like an obvious detail, but the former proposals for typed properties did not include static properties. In PHP 7.4, you can declare types for static properties too.

Furthermore, you can return a reference to a typed property, and the types will be still honored:

class Example {
  public string $name;
}

$foo = new Example();
$foo->name = 'Apple';
$bar =& $foo->name;
$bar = []; // Not allowed

This will throw an error:

Fatal error: Uncaught TypeError: Cannot assign ... to reference held by property Example::$name of type ... in ...

Default values in constructors and property declaration

For historical reasons, PHP allows you to set a default value for function arguments in its declaration even if the type is not nullable.

class Example {
  public function __construct(string $name = null) {
      var_dump($name);
  }
}

$foo = new Example();
// NULL

In the constructor, we explicitly mark that the $name argument is not nullable, and yet PHP accepts null as the default value. This behavior only applies to null default values. Although this is semantically invalid, this behavior is allowed for historical and implementation reasons.

With typed properties, this is not allowed:

class Example {
    private string $name = null;
}

$foo = new Example();
// NULL

This will promptly throw an error:

Fatal error: Default value for property of type ... may not be null. Use the nullable type ?... to allow null default value in ...

Type Variance

PHP 7.4 comes with return type variance, which means a child class can return a more specific instance. This is not yet supported for property types. For example:

class A {}
class B extends A {}

class Fruits {
    public B $foo;
}
class Banana extends Fruits {
    public A $foo;
}

Code above would not work. Although B is a subset of A class, changing the type declaration of Banana::$foo is not allowed. You can still assign an instance of A to Banana::$foo. This is called covariance, and it is now supported for return types.
Trying the above will throw an error like the following:

Fatal error: Type of Banana::$foo must be B (as in class Fruits) in W:\localhost\test\test.php on line 11

The following code is still valid:

class A {}
class B extends A {}

class Fruits {
    public A $foo;
}
class Banana extends Fruits {
    public A $foo;
}

$banana = new Banana();
$banana->foo = new B();

Notice how the property declaration in Fruits::$foo and Banana::$foo is A, but we assign an instance B to it.

To summarize:

  • You cannot substitute a child class for the property.
  • You cannot add types to child classes if the parent does not have types enforced.
  • You cannot mark a non-nullable type as nullable in a child class.
  • You cannot mark a nullable type as non-nullable in a child class.

To visualize this, take a look at the following inheritance chain. None of the following are allowed:

class A {}
class B extends A{}

class Fruits {
    public $no_type;
    public A $strict_type;
    public ?string $nullable;
    public string $non_nullable;
}
class Banana extends Fruits {
    public A $no_type; // Not allowed to add types in a subclass.
    public $strict_type; // Not allowed to remove type in a childclass.
    public string $nullable; // Not allowed to make non-nullable
    public ?string $non_nullable; // Not allowed to make nullable
}

Above will throw the following errors (not at the same time):

Fatal error: Type of Banana::$no_type must not be defined (as in class Fruits) in ... on line ...
Fatal error: Type of Banana::$strict_type must be A (as in class Fruits) in ... on line 17
Fatal error: Type of Banana::$nullable must be ?string (as in class Fruits) in ... on line 17
Fatal error: Type of Banana::$non_nullable must be string (as in class Fruits) in ... on line 17

Backwards compatibility

Declaring property types is optional, and all your existing code should work. If you plan to upgrade an existing code base to typed properties, keep an eye on the uninitialized state, and inheritance where rules are enforced rather strictly. Further, property types do not carry the legacy behavior of allowing null values in their function/method arguments, and it can come as a surprise.

Final Thoughts

Property types is a feature that I was personally excited about, and now that it's finally here, I have spent some time adding property types to my existing private projects. PHPStorm 2019.3 comes with support for all PHP 7.4 features, and there is a quick-fix to add property types if it can be gathered from property docblock comments or from the constructor.
Even in projects that had 100% test coverage, I still discovered a few bugs that were there all the time, but property types bring them front and center!

Open source projects might take some time to require PHP 7.4 as the minimum version, but it wouldn't prevent you from using them in your private projects.

RFC Externals.io discussion Implementation

Underscore numeric separator

TypeNew Feature

A simple, yet well thought-out change in PHP 7.4 is the numeric literal separator.

We are used to read numbers when they are grouped. For example, 1,000,000 is easier to subitize than 1000000. With numeric literal separator support in PHP, you can now use numbers in PHP like this:

$num = 1_000_000;

Prior to PHP 7.4, trying above would result in a syntax error:

Parse error: syntax error, unexpected '_000_000' (T_STRING) in ... on line ...

PHP engine removes the underscores when lexing.

Support for all numeric notations

You can use the underscore pattern in all PHP numeric notations.

  • Decimal: 1_000_000
  • Scientific: 6.62_607_004e-34
  • Float: 0.300_000_000_000_000_04
  • Binary: 0b1111_0000_1001_1111_1001_0010_1010_1001
  • Octal: 0123_7264
  • Hex: 0xBEEF_BABE

Disallowed patterns

The RFC also mentions the patterns that are not allowed:

  • Two or more underscores: 1__6
  • Underscores before and after the number: _16 and 16_
  • Underscore around the decimal point: 1_.6 or 1._6
  • Underscore around the e in scientific notation: 1.6_e-28 or 1.6e_-28
  • Underscore around the b in binary notation: 0_b01010 or 0b_010101
  • Underscore around the x in hex notation: 0_xf00d or 0x_food

Backwards compatibility

Because the underscore separator would have thrown a syntax error in versions prior 7.4, all existing code should work without any problems. It is not possible to polyfill this behavior, so if you plan to use this feature, keep in mind that you will need to bump your minimum PHP version to 7.4.

RFC Externals.io discussion Implementation