¿Qué es lo nuevo en PHP 7.4?

El 28 de noviembre del 2019 el equipo de desarrollo de PHP anunció la disponibilidad inmediata de PHP 7.4.0. Esta versión marca la cuarta actualización de la serie PHP 7.

PHP 7.4.0 viene con numerosas mejoras y nuevas características como:

1. Propiedades tipeadas

Ahora es posible declarar propiedades tipeadas.

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

En el ejemplo anterior la propiedad id acepta un valor entero, mientras que la propiedad name acepta una cadena, en caso de pasar un valor que no cumpla las condiciones anteriores PHP tratará de hacer la conversión automática, por ejemplo PHP convertírá la cadena «15» o el punto flotante 1.6 a un entero automáticamente por tanto el siguiente código es válido.

<?php
$user = new User;
$user->id = 1.6 // PHP convertirá 1.6 a entero = 1
$newuser = new User;
$newUser = "15" // PHP convertirá "15" a entero = 15; 

Si PHP no puede realizar la conversión entonces lanzará un Fatal error, el siguiente código da un error:

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

si ejecuta el código anterior obtendrá el siguiente error:

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

Una propiedad de tipo cadena aceptará cualquier valor que sea cadena, entero, punto flotante, booleano o un objeto que implementé el método __toString, las siguientes asignaciones son válidas

<?php
// Cadena
$user = new User;
$user->name = 'Mi nombre';

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

// Punto flotante
$user = new User;
$user->name = 1.5;

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

En caso de que PHP no pueda realizar la conversión lanzará un Fatal error

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

si ejecuta el código anterior obtendrá el siguiente error:

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

Si quiere que los tipos sean analizados en forma estricta entonces ponga la siguiente sentencia en su fichero php

<?php
declare(strict_types = 1);

ahora las propiedades deber ser exactamente del tipo declarado.

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

si ejecuta el código anterior obtendrá el siguiente error:

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

Las propiedades pueden ser de cualquier tipo excepto void y callable, también se le puede especificar un tipo a las propiedades estáticas, en caso de que una propiedad pueda aceptar el valor de nulo preceda el tipo con un ?

<?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. Funciones de flechas

Las funciones anónimas permiten una mejor organización y legibilidad en el código fuente pero si las operaciones son sencillas entonces las funciones anónimas se convierten en verbosas por eso el equipo de desarrollo de PHP decidió incluir en la versión 7.4 lo que se conoce como funciones flechas las cuales presentan una sintaxis más compacta para las funciones anónimas.

Tanto las funciones anónimas como las funciones de flecha se implementan utilizando la clase Closure.

La sintaxis básica de las funciones de flecha es:

fn (argument_list) => expr

Nótese el uso de fn la cual, ahora, es una palabra reservada de PHP. Las funciones de flechas a diferencia de las funciones anónimas, siempre devuelven el resulato de expr

Cuando una variable utilizada en la expresión (expr) se define en el ámbito principal, se capturará implícitamente por valor. En el siguiente ejemplo, las funciones $fn1 y $fn2 se comportan de la misma manera.

<?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;
};

Las variables del ambito principal no pueden ser modificadas por las funciones de flecha por tanto el siguiente código no tiene ningún efecto

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

La regla anterior no se cumple para variables de tipo objeto las cuales siempre se pasan por referencia, pruebe ejecutar el siguiente código:

<?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);

Similar a las funciones anónimas, las funciones de flecha pueden recibir parámetros también es posible especificar valores por defecto y tipos a los parámetros y tipo al valor devuelto, así como pasar y devolver referencias, una función de flecha también puede se variádica, por tanto los siguientes ejemplos son válidos.

<?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. Covarianza y contravarianza limitada

En términos simples covarianza significa subtipo y contravarianza supertipo. Estos términos son usados fundamentalmente en la programación orientadas a objetos y plantea que al sobreescribir un método determinado se puede cambiar el tipo de parámetro (contravarianza) y el tipo de retorno (covarianza) del método sobreescrito.

Tomemos como ejemplo el siguiente código.

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 que la interfaz StudentList extiende a la interfaz PersonList y que el método add recibe un parámetro de tipo object que es un super tipo de Person (contravarianza) y que el método find devuelve un instancia de Student, que es un subtipo de Person (covarianza), o null.

Si ponemos todo el código en un script php y tratamos de ejecutarlo en una version anterior a PHP 7.4 obtendríamos el siguiente 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

Tenga en cuenta que no se puede aplicar covarianza a los parámetros o contravarianza al tipo de retorno si lo hace PHP generará un FatalError.

4. Operador de asignación de fusión nulo

PHP 7.0 introdujo el operador de fusión nulo (??), el cual proporciona una alternativa conveniente y más concisa a isset.

El operador de asignación de fusión nulo es la variante corta del operador de fusión nulo, veamos el siguiente ejemplo que ilustra lo anterior.

<?php
// Las siguiente líneas hacen lo mismo
$this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';
// En lugar de repetir las variables con un nombre largo, el operador de asignación de fusión nulo es usado
$this->request->data['comments']['user_id'] ??= 'value';
// Antes de PHP 7.0
if (!isset($this->request->data['comments']['user_id'])) {
    $this->request->data['comments']['user_id'] = 'value';
} 

5. Operador de expansión de arreglos

Si uno o más elementos de un arreglo se prefija con el operador entonces los elementos del elemento prefijado se agregan al arreglo original a partir de la posición que ocupa el elemento prefijado. Solo arreglos y objetos que implementen la interfaz Traversable pueden ser expandidos.

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

6. Separadores de números

Los separadores de número añaden legibilidad y claridad al código fuente, permiten discernir números grandes de un solo vistazo, los números grandes se utilizan comúnmente para constantes de lógica empresarial, valores de prueba unitaria y conversión de datos, tomemos como ejemplo el siguiente script PHP que calcula la cantidad de memoria disponible para un script PHP

Sin separador númerico

<?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;
}

Nótese que entre más grande el factor de conversión más díficil leerlo, ahora probemos usando separadores númerico.

<?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;
}

Como puede notar es mucho más fácil leer y detectar errores cuando se usan separadores númericos, otros ejemplos de números con separadores númericos son los siguientes:

6.674_083e-11; // punto flotante
299_792_458;   // decimal
0xCAFE_F00D;   // hexadecimal
0b0101_1111;   // binario
0137_041;      // octal    

La única restricción es que cada guión bajo debe estar entre dos dígitos. Esta regla significa que ninguno de los siguientes usos son literales numéricos válidos:

<?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. Referencias débiles

Una referencia débil es una referencia que puede ser destruida en cualquier momento ya que no protege al objeto referenciado de ser barrido por el recolector de basura, tenga en cuenta que a medida que su aplicación necesite más memoria el recolector de basura detectará las variables que pueden eliminarse (para optimizar el uso de la memoria) y entre ellas las referencias débiles.

Las referencias débiles permiten implementar cachés o mapeos que contienen objetos que consumen mucha memoria o minimizan el número de objetos innecesarios en la memoria .

Una referencia débil no puede ser serializada.

Para crear una referencia débil usaremos el método create de la clase WeakReference:

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

Para obtener el objeto referenciado usaremos el método get

<?php
weakref->get()

Si el objeto ya ha sido destruido, devuelve NULL.

8. Nuevo mecanismo de serialización parcial de objetos

PHP actualmente proporciona dos mecanismos de serialización personalizada: los métodos mágicos __sleep() / __wakeup() y la interfaz Serializable, ambos enfoques presentan incovenientes a la hora de realizar serializaciones parciales, comentemos ambos enfoques.

Serializable

Las clases que implementan la interfaz serializable se codifican usando el formato C, que es básicamente C:ClassNameLen:»ClassName»:PayloadLen:{Payload}, donde Payload es una cadena arbitraria. Esta es la cadena devuelta por Serializable::serialize() y casi siempre producida por una llamada anidada a serialize():

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

Si un mismo objeto (o valor por referencia) se usa varias veces en un mismo grafo serializado, PHP usará referencias compartidas en la cadena resultante, por ejemplo si serializamos el siguiente arreglo: [$obj, $obj] el primer elemento será serializado como siempre mientras el segundo será una referencia de la forma r:1, debido a que la llamada serialize anidada (llamada dentro the Serialize::serialize) comparte el estado de serialización con la llamada serialize externa, las operaciones de serialize/unserialized deben ser usadas en un mismo contexto caso contrario se pierde la consistencia como se muestra en el siguiente ejemplo:

<?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])));

Si ejecutamos el código anterior obtenemos:

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

Como puede notar se pierde la consistencia y el estado en los objetos deserializados.

Métodos __sleep() / __wakeup()

El principal incoveniente de usar __sleep() / __wakeup() es la usabilidad ya que no es posible serializar en una clase hija propiedades privadas de una clase padre, analicemos el siguiente ejemplo.

<?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'))));

Si ejecuta el código anterior obtendrá:

Unserialized/Serialize data
/var/www/html/php74.php:50:
object(Professor)[1]
  private 'rank' => string 'assistant' (length=9)
  private 'name' (Person) => null

Nótese que al deserializar el objeto serializado se pierde el valor de la propiedad name y ello se debe que la propieda name es privada, además obtendrá un warning al tratar de serializar una propiedad privada de la clase padre.

Para resolver los problemas anteriores se agregaron 2 métodos mágicos:

<?php
public function __serialize(): array;

public function __unserialize(array $data): void;

Veamos como sería la implementación usando estos 2 nuevos métodos:

<?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])));

Si prueba el código anterior notará que no se pierde la consistencia ni el estado de los objetos serializados.

9. Precarga

Ahora es posible mejorar el rendimiento de las aplicaciones al precargar scripts en memoria haciendo uso de OPcache (tenga en cuenta que debe hallar un balance entre rendimiento y uso de memoria: «precargar todo» puede ser la estrategia más fácil, pero no necesariamente la mejor estrategia), los scripts precargados estarán disponibles globalmente sin necesidad de hacer un include explicitamente, cualquier modificación en los scripts precargados no tendrán ningún efecto hasta que reinicie el proceso PHP (mod_php, php-fpm).

Habilitar la precarga requiere de 2 pasos: habilitar OPcache y establecer el valor de la directiva opcache.preload que puede ser un camino absoluto o relativo a la directiva include_path.

opcache.preload=preload.php

preload.php es un archivo arbitrario que se ejecuta una vez al inicio del servidor (php-fpm, mod_php, etc.) y cargará el código en la memoria persistente, este archivo puede precargar otros archivos, ya sea incluyéndolos o usando la función opcache_compile_file().

Si PHP no encuentra el fichero preload.php en el include_path, lanzará un Fatal Error y el servicio no se iniciará (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

Tampoco el servicio se iniciará si el fichero preload.php genera un fatal error.

No precargue ficheros que usen variables de entorno web como $_SERVER[‘DOCUMENT_ROOT’] o $_GET ya que el fichero preload.php se ejecuta en un entorno CLI.

Conjuntamente con la directiva opcache.preload=preload.php se debe configurar la directiva:

opcache.preload_user=www-data

opcache.preload_user doc.

Limitaciones:

  • No es posible activar está característica en Windows.

  • No es posible precargar ficheros que hagan uso de variable de entorno Web.

  • No es posible precargar versiones diferentes de una misma aplicación o framework debido a que comparten clases, funciones y variables y de esta manerá solo se precargará una de las versiones, las demás se ignorarán.

Lecturas recomendadas

YouTube video

PHP nuevas características, 4 (8)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.