What’s new in PHP 7.4?

The PHP development team announced, on november 2019, the immediate availability of PHP 7.4.0. This release marks the fourth feature update to the PHP 7 series.

PHP 7.4.0 comes with numerous improvements and new features such as:

1. Typed properties

Class properties now support type declarations.

<?php
class User 
{
    public int $id;
    public string $name;
}

In the above example the id property accepts an integer value, while the name property accepts a string, in case of passing a value that does not meet the previous conditions, PHP will try to do the automatic conversion, for example PHP will convert the string “15” or floating point 1.6 to an integer automatically so the following code is valid.

<?php
$user = new User;
$user->id = 1.6 // PHP convert 1.6 to integer = 1
$newuser = new User;
$newUser = "15" // PHP convert "15" to integer = 15; 

If PHP cannot convert then it will throw a Fatal error, the following code thrown an error:

<?php
$user = new User;
$user->id = 'id';

if you run the above code you will get the following error:

Fatal error: Uncaught TypeError: Typed property User::$id must be int, string used 

A string property will accept any value of type: string, integer, floating point, boolean or an object that implements the __toString method, the following assignments are valid:

<?php
// String
$user = new User;
$user->name = 'My Name';

// Integer
$user = new User;
$user->name = 1;

// Float
$user = new User;
$user->name = 1.5;

// Object
class MyName
{
    public function __toString() 
    {
        return MyName::class;
    }
}
$user = new User;
$user->name = new MyName;

In case the conversion is not allowed PHP it will throw a Fatal error

<?php
// Try to asign an array
$user = new User;
$user->name = [];

if you run the above code you will get the following error:

Fatal error: Uncaught TypeError: Typed property User::$name must be string, array used

If strict_types = 1 then the assigned value must satisfy the declared type exactly,

<?php
declare(strict_types = 1);

now the properties must be exactly of the declared type.

<?php
$user = new User;
$user->name = 1;

if you run the above code you will get the following error:

Fatal error: Uncaught TypeError: Typed property User::$name must be string, int used

The properties can be of any type except void and callable, you can also specify a type to the static properties, in case a property can accept the null value precede the type with a ?

<?php
class Example {
    // All types with the exception of "void" and "callable" are supported
    public int $scalarType;
    protected ClassName $classType;
    private ?ClassName $nullableClassType;

    // Types are also legal on static properties
    public static iterable $staticProp;

    // Typed properties may have default values (more below)
    public string $str = "foo";
    public ?string $nullableStr = null;

    // The type applies to all properties in one declaration
    public float $x, $y;
}

2. Arrow functions

Anonymous functions allow a better organization and readability in the source code but if the operations are simple then anonymous functions can be verbose so the PHP development team decided to include in PHP 7.4 what is known as arrow functions which present a more compact syntax for anonymous functions.

Both anonymous functions and arrow functions are implemented using the Closure class.

Arrow functions have the basic form:

fn (argument_list) => expr

Note the identifier fn which is now a reserved keyword. Arrow functions, unlike anonymous functions, always return the result of expr.

When a variable used in the expression (expr) is defined in the main scope, it will be captured implicitly by value. In the following example, the $fn1 and $fn2 functions behave in the same way.

<?php
$y = 1;
// Captura el valor de $y automáticamente
$fn1 = fn($x) => $x + $y;
// Equivalente a pasar $y por valor:
$fn2 = function ($x) use ($y) {
    return $x + $y;
};

class=”alignright size-large”The variables of the main scope cannot be modified by the arrow functions therefore the following code has no effect.

<?php
$x = 1;
$fn = fn() => $x++; // No tiene ningún efecto
$fn();
var_export($x);  // Salida 1

The exception is: the variables of object type which are always passed by reference, try executing the following code:

<?php
$o = new class() {
    public $property;
};

$f = fn() => $o->property = 'My';
// Print
// object(class@anonymous)[1]
//   public 'property' => null
var_dump($o);

$f();

// Print
// object(class@anonymous)[1]
//   public 'property' => string 'My' (length=2)
var_dump($o);

Similarly to anonymous functions, the arrow function syntax allows arbitrary function signatures, including parameter and return types, default values, variadics, as well as by-reference passing and returning. All of the following are valid examples of arrow functions:

<?php
// Parámetro $x de tipo array
fn(array $x) => $x;
// Función de flecha estática la cual debe devolver un entero
static fn(): int => $x;    
// Parámetro pasado por valor con valor por defecto
fn($x = 42) => $x;
// Parámetro por referencia
fn(&$x) => $x;
// Se devuelve un valor por referencia,
// Para más información ver: 
// https://www.php.net/manual/es/language.references.return.php
fn&($x) => $x;
// Función de flecha variádica: puede recibir cualquier número de parámetros
fn($x, ...$rest) => $rest;

3. Limited return type covariance and argument type contravariance

In simple terms covariance means subtype and contravariance supertype. These terms are used fundamentally in object-oriented programming and it propose that by overwriting a certain method, the parameter type (contravariance) and the return type (covariance) of the overwritten method can be changed.

Let’s take the following code as an example.

Interfaz PersonList

<?php

interface PersonList
{
    public function add(Person $p);

    public function find(Person $p): ?Person; 
} 

Interfaz StudentList

<?php

interface StudentList extends PersonList 
{
    public function add(object $p);

    public function find(Person $p): ?Student; 
}

Note that: the StudentList interface extends from the PersonList interface, the add method receives a paramter of object type which is a super type of Person (contravariance), the find method returns a instance of Student, which is a subtype of Person (covariance), or null.

If we put all the code in a php script and try to execute it in a version prior to PHP 7.4 we would get the following error:

Fatal error: Declaration of StudentList::add(object $p) must be compatible with PersonList::add(Person $p) 
Fatal error: Declaration of StudentList::find(Person $p): ?Student must be compatible with PersonList::find(Person $p): ?Person

Keep in mind that is not possible to apply covarianza to the parameters neither contravarianza to the return types, if you try it PHP will throw a FatalError.

4. Null coalescing assignment operator

PHP 7.0 introduced the null coalescing operator (??), which provides an convenient and concise alternative to isset.

The null coalescing assignment operator is the short variant of null coalescing operator, let’s look at the following example.

<?php
// These lines do the same
$this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';
// Instead of repeating variables with long names, the null coalescing assignment operator is used
$this->request->data['comments']['user_id'] ??= 'value';
// Before PHP 7.0
if (!isset($this->request->data['comments']['user_id'])) {
    $this->request->data['comments']['user_id'] = 'value';
} 

5. Spread Operator in Array Expression

If one or more elements of an array is prefixed with the operator then the elements of the prefixed element are added to the original array from the position of the prefixed element. Only arrays and objects that implement the Traversable interface can be expanded.

<?php
$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
// ['banana', 'orange', 'apple', 'pear', 'watermelon'];

6. Numeric literal separator

Numerica liteal separator improves code readability by supporting an underscore in numeric literals to visually separate groups of digits. Large numeric literals are commonly used for business logic constants, unit test values, and performing data conversions, let’s take the following PHP script as an example that calculates the amount of memory available to a PHP script

Without number separator

<?php
/**
 * Calculate the free memory available for a PHP script
 * 
 * @return float
 */
function get_free_memory()
{
    /**
     * Maximum amount of memory in bytes that a script is allowed to allocate
     * 
     * @see https://www.php.net/manual/en/ini.core.php#ini.memory-limit
     */
    $memory_limit = ini_get('memory_limit');

    // Recalculate memory_limit in bytes.
    if (preg_match('/\d+(k|K)/', $memory_limit)) { // memory_limit is set in K
        $memory_limit = ((int) $memory_limit) * 1024;
    } elseif (preg_match('/\d+(m|M)/', $memory_limit)) { // memory_limit is set in M
        $memory_limit = ((int) $memory_limit) * 1048576;
    } elseif (preg_match('/\d+(g|G)/', $memory_limit)) { // memory_limit is set in G
        $memory_limit = ((int) $memory_limit) * 1073741824;
    }

    /**
     * Get the free memory
     * @see https://www.php.net/manual/en/function.memory-get-usage.php
     */
    $free_memory = $memory_limit - memory_get_usage(true);

    // Free memory in MB
    return $free_memory / 1048576;
}

Note that the larger the conversion factor, the more difficult it is to read, now let’s try using number separators.

<?php
/**
 * Calculate the free memory available for a PHP script
 * 
 * @return float
 */
function get_free_memory()
{
    /**
     * Maximum amount of memory in bytes that a script is allowed to allocate
     * 
     * @see https://www.php.net/manual/en/ini.core.php#ini.memory-limit
     */
    $memory_limit = ini_get('memory_limit');

    // Recalculate memory_limit in bytes.
    if (preg_match('/\d+(k|K)/', $memory_limit)) { // memory_limit is set in K
        $memory_limit = ((int) $memory_limit) * 1024;
    } elseif (preg_match('/\d+(m|M)/', $memory_limit)) { // memory_limit is set in M
        $memory_limit = ((int) $memory_limit) * 1_048_576;
    } elseif (preg_match('/\d+(g|G)/', $memory_limit)) { // memory_limit is set in G
        $memory_limit = ((int) $memory_limit) * 1_073_741_824;
    }

    /**
     * Get the free memory
     * @see https://www.php.net/manual/en/function.memory-get-usage.php
     */
    $free_memory = $memory_limit - memory_get_usage(true);

    // Free memory in MB
    return $free_memory / 1_048_576;
}

As you can see it is much easier to read and detect errors using number separators, other examples of numbers with number separators are:

<?php
// Todos estos ejemplos producen errores de sintaxis
100_;       // al final
1__1;       // al lado de otro guión bajo
1_.0; 1._0; // al lado de un punto decimal
0x_123;     // al lado de la x en un número hexadecimal
0b_101;     // al lado de la b en un número binario
1_e2; 1e_2; // al lado de la e, en una notación cientifica

The only restriction is that each underscore in a numeric literal must be directly between two digits. This rule means that none of the following usages are valid numeric literals:

<?php
// Todos estos ejemplos producen errores de sintaxis
100_;       // al final
1__1;       // al lado de otro guión bajo
1_.0; 1._0; // al lado de un punto decimal
0x_123;     // al lado de la x en un número hexadecimal
0b_101;     // al lado de la b en un número binario
1_e2; 1e_2; // al lado de la e, en una notación cientifica

7. Weak references

A weak reference is a reference that can be destroyed at any time as it does not protect the referenced object from being swept by the garbage collector, keep in mind that as your application needs more memory the garbage collector will detect the variables that can be destroyed (to optimize memory usage) and between them weak references.

Weak references allow you to implement caches or mappings that contain memory-intensive objects or minimize the number of unnecessary objects in memory.

A weak reference cannot be serialized.

To create a weak reference we will use the create method of the WeakReference class:

<?php
$obj = new stdClass;
$weakref = WeakReference::create($obj);
<?php
weakref->get()

If the object has already been destroyed, it returns NULL.

8. New custom object serialization mechanism

PHP currently provides two mechanisms for custom serialization of objects: the __sleep()/__wakeup() magic methods, as well as the Serializable interface. Unfortunately, both approaches have issues that will be discussed in the following.

Serializable

Classes implementing the Serializable interface are encoded using the C format, which is basically C:ClassNameLen:"ClassName":PayloadLen:{Payload}, where the Payload is an arbitrary string. This is the string returned by Serializable::serialize() and almost always produced by a nested call to serialize():

public function serialize() {
    return serialize([$this->prop1, $this->prop2]);
}

If the same object (or value by reference) is used several times in the same serialized graph, PHP will use shared references in the resulting string, for example if we serialize the following array: [$obj, $obj] the first element will be serialized as usual while the second will be a reference of the form r:1, due the nested serialize call (call inside the Serialize::serialize) shares the serialization state with the external serialize call, the serialize/unserialized operations must be used in the same context, otherwise consistency is lost as shown in the following example:

<?php
class Person implements Serializable
{
    private $name;

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

    public function serialize() 
    {
        return serialize($this->name); 
    }

    public function unserialize($data) 
    {
        $this->name = unserialize($data);
    }

    public function getName() 
    {
        return $this->name;
    }
}


class Professor extends Person
{
}

class Student extends Person
{
    private $average;

    private $professor;

    public function __construct($name, $average, Professor $professor)
    {
        parent::__construct($name);
        $this->average = $average;
        $this->professor = $professor;
    }

    public function serialize() 
    {
        return serialize([$this->average, $this->professor, parent::serialize()]);
    }

    public function unserialize($data) 
    {
        [$average, $professor, $parent] = unserialize($data);
        parent::unserialize($parent);
        $this->average = $average;
        $this->professor = $professor; 
    }
}

$p = new Professor('Juan');

$pepe = new Student('Pepe', 5, $p);
$nik = new Student('Nik', 4.9, $p);

echo "Unserialized/Serialize data<br/>";
var_dump(unserialize(serialize([$pepe, $nik])));

If you run the above code you get:

array (size=2)
  0 => 
    object(Student)[4]
      private 'average' => int 5
      private 'professor' => 
        object(Professor)[5]
          private 'name' (Person) => string 'Juan' (length=4)
      private 'name' (Person) => string 'Pepe' (length=4)
  1 => 
    object(Student)[6]
      private 'average' => null
      private 'professor' => null
      private 'name' (Person) => boolean false

As you can note the object state and consistency is lost.

__sleep () / __wakeup () methods

The main drawback of using __sleep () / __wakeup() is usability since it is not possible to serialize private properties of a parent class in a child class, let’s analyze the following example.

<?php
class Person
{
    private $name;

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

    public function getName() 
    {
        return $this->name;
    }

    public function _sleep(): array 
    {
        return ['name'];
    }

    public function __wakeup(): void 
    {
      // Do something to restore the object state
    }
}

class Professor extends Person
{
    /**
     * Academic rank: assistant, associate, adjunct, ...
     * @string 
     */
    private $rank;

    public function __construct($name, $rank) 
    {
        parent::__construct($name);
        $this->rank = $rank;    
    }

    public function __sleep(): array 
    {
        $parent = parent::__sleep();
        array_push($parent, 'rank');
        return $parent;
    }
}
echo "Unserialized/Serialize data<br/>";
var_dump(unserialize(serialize(new Professor('J', 'assistant'))));

Note that when deserializing the serialized object the value of the name property is lost and this is because the name property is private.

To solve the above problems 2 magic methods were added:

<?php
public function __serialize(): array;

public function __unserialize(array $data): void;

Let’s see how the implementation would be using these 2 new methods:

<?php
class Person 
{
    private $name;

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

    public function __serialize(): array 
    {
        return [$this->name]; 
    }

    public function __unserialize(array $data): void 
    {
        $this->name = $data[0];
    }

    public function getName() 
    {
        return $this->name;
    }
}


class Professor extends Person
{
}

class Student extends Person
{
    private $average;

    private $professor;

    public function __construct($name, $average, Professor $professor)
    {
        parent::__construct($name);
        $this->average = $average;
        $this->professor = $professor;
    }

    public function __serialize(): array 
    {
        return [$this->average, $this->professor, parent::__serialize()];
    }

    public function __unserialize(array $data): void 
    {
        [$average, $professor, $parent] = $data;
        parent::__unserialize($parent);
        $this->average = $average;
        $this->professor = $professor;
    }
}

$p = new Professor('Juan');

$pepe = new Student('Pepe', 5, $p);
$nik = new Student('Nik', 4.9, $p);

echo "Unserialized/Serialize data<br/>";
var_dump(unserialize(serialize([$pepe, $nik])));

If you try the code above, you will notice that the consistency and state of serialized objects is not lost.

9. Preloading

Now it is possible to improve the PHP applications performance by preloading scripts using OPcache (note that you must find a balance between performance and memory usage: “preload all” may be the easiest strategy, but not necessarily the best strategy), the preloaded scripts will be available globally without the need to do an include explicitly, any modifications to the preloaded scripts will have not effect until you restart the PHP process (mod_php, php-fpm).

Enabling preload requires 2 steps: enable OPcache and set the value of the directive opcache.preload that it can be an absolute path or relative to the include_path directive.

opcache.preload=preload.php

preload.php is an arbitrary file that is executed once at server startup (php-fpm, mod_php, etc.) and will load the code into persistent memory, this file can preload other files, either by including them or using the opcache_compile_file() function.

If PHP does not find the preload.php file in the include_path, it will throw a Fatal Error and the service will not start (mod_php, php-fpm):

PHP Fatal error: PHP Startup: Failed opening required 'preload.php' (include_path='.:/usr/local/lib/php') in Unknown on line 0

Also the service will not start if the preload.php file generates a fatal error; do not preload files that depend on web environment variables such as $_SERVER[‘DOCUMENT_ROOT’] or $_GET since the preload.php file is executed in a CLI environment.

Also the directive opcache.preload-user must be configured:

opcache.preload_user=www-data

Limitations

  • Preloading is not supported on Windows.
  • Preloading files that use web environment variables will thrown fatal errors.
  • It is not possible to preload different versions of the same application or framework because they share classes, functions and variables and in this way only one of the versions will be preloaded, the others will be ignored.

Further readings

YouTube video

PHP new features, 4 (8)

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.