PHP 8.0: Nuevas funciones, clases y JIT (4/4)

hace 4 años por David Grudl  

Ha salido la versión 8.0 de PHP. Está tan repleta de novedades como ninguna versión anterior. Su presentación ha requerido cuatro artículos separados. En este último, veremos las nuevas funciones y clases y presentaremos el Just in Time Compiler.

Nuevas funciones

La biblioteca estándar de PHP dispone de cientos de funciones y en la versión 8.0 han aparecido seis nuevas. Parece poco, pero la mayoría de ellas cubren puntos débiles del lenguaje. Lo cual encaja bien con el tono general de la versión 8.0, que refina y consolida PHP como ninguna versión anterior. Puede encontrar un resumen de todas las nuevas funciones y métodos en la guía de migración.

str_contains() str_starts_with() str_ends_with()

Funciones para determinar si una cadena empieza, termina o contiene una subcadena.

if (str_contains('Nette', 'te')) {
	...
}

Junto con la llegada de este trío, PHP define cómo tratar la cadena vacía al buscar, según lo cual se rigen también todas las demás funciones relacionadas, de manera que la cadena vacía se encuentra en todas partes:

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (antes false)

Gracias a esto, el comportamiento del trío es completamente idéntico a sus análogos en Nette:

str_contains()      # Nette\Utils\String::contains()
str_starts_with()   # Nette\Utils\String::startsWith()
str_ends_with()     # Nette\Utils\String::endsWith()

¿Por qué son tan importantes estas funciones? Las bibliotecas estándar de todos los lenguajes siempre están cargadas con el desarrollo histórico y no se pueden evitar inconsistencias y errores. Pero al mismo tiempo, son la tarjeta de visita de cada lenguaje. Es sorprendente que en un PHP de 25 años falten funciones para operaciones tan básicas como devolver el primer o último elemento de un array, escapar HTML sin trampas (htmlspecialchars no escapa el apóstrofo), o precisamente buscar una cadena dentro de otra. Que se pueda de alguna manera evitar no se sostiene, porque el resultado no es un código legible y comprensible. Es una lección para todos los autores de API. Cuando ve que una parte considerable de la documentación de una función se dedica a explicar sus trampas (como los valores de retorno de strpos), es una señal clara para modificar la biblioteca y añadir precisamente str_contains.

get_debug_type()

Reemplaza al ya obsoleto get_type(). En lugar de tipos largos como integer devuelve los ahora usados int, en el caso de objetos devuelve directamente el tipo:

Valor gettype() get_debug_type()
'abc' string string
[1, 2] array array
231 integer int
3.14 double float
true boolean bool
null NULL null
new stdClass object stdClass
new Foo\Bar object Foo\Bar
function() {} object Closure
new class {} object class@anonymous
new class extends Foo {} object Foo@anonymous
curl_init() resource resource (curl)
curl_close($ch) resource (closed) resource (closed)

Objetivación de recursos

Los valores de tipo resource provienen de tiempos en que PHP aún no tenía objetos, pero en realidad los necesitaba. Así nacieron los resources. Hoy tenemos objetos y, en comparación con los resources, funcionan mucho mejor con el recolector de basura, por lo que el plan es reemplazarlos gradualmente todos por objetos.

Desde PHP 8.0, se convierten en objetos los resources de imágenes, conexiones curl, openssl, xml, etc.. En PHP 8.1, será el turno de las conexiones FTP, etc.

$res = imagecreatefromjpeg('image.jpg');
$res instanceof GdImage  // true
is_resource($res)        // false - BC break

Estos objetos aún no tienen métodos, ni puede crear directamente sus instancias. Por ahora, se trata realmente solo de eliminar los obsoletos resources de PHP sin cambiar la API. Y eso es bueno, porque crear una buena API es una tarea separada y exigente. Nadie desea que en PHP surjan más clases como SplFileObject con métodos llamados fgetc() o fgets().

PhpToken

El tokenizador también se traslada a objetos y, por lo tanto, las funciones alrededor de token_get_all. Esta vez no se trata de deshacerse de los resources, sino que obtenemos un objeto completo que representa un token PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instancia de PhpToken

echo $token->id;             // T_OPEN_TAG
echo $token->text;           // '<?php'
echo $token->line;           // 1
echo $token->getTokenName(); // 'T_OPEN_TAG'
echo $token->is(T_STRING);   // false
echo $token->isIgnorable();  // true

El método isIgnorable() devuelve true para los tokens T_WHITESPACE, T_COMMENT, T_DOC_COMMENT y T_OPEN_TAG.

Mapas débiles (Weak maps)

Los mapas débiles están relacionados con el recolector de basura, que libera de la memoria todos los objetos y valores que ya no se utilizan (es decir, no hay ninguna variable o propiedad en uso que los contenga). Dado que la vida de un hilo PHP es efímera y hoy en día tenemos suficiente memoria disponible en los servidores, generalmente no nos preocupamos por cuestiones relacionadas con la liberación eficiente de memoria. Pero para scripts que se ejecutan durante más tiempo, son fundamentales.

El objeto WeakMap es similar a SplObjectStorage. En ambos, se utilizan objetos como claves y permiten almacenar valores arbitrarios bajo ellas. La diferencia es que WeakMap no impide que el objeto sea liberado por el recolector de basura. Es decir, si el único lugar donde todavía existe el objeto es como clave en un mapa débil, será eliminado del mapa y de la memoria.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'data for $obj';

dump(count($map));  // 1
unset($obj);
dump(count($map));  // 0

¿Para qué sirve esto? Por ejemplo, para el almacenamiento en caché. Supongamos que tenemos un método loadComments(), al que le pasamos un artículo de blog y devuelve todos sus comentarios. Como el método se llama repetidamente con el mismo artículo, crearemos también getComments(), que almacenará en caché el resultado del primer método:

class Comments
{
	private WeakMap $cache;

	public function __construct()
	{
		$this->cache = new WeakMap;
	}

	public function getComments(Article $article): ?array
	{
		$this->cache[$article] ??= $this->loadComments($article);
		return $this->cache[$article]
	}

	...
}

El truco está en que, en el momento en que se libera el objeto $article (por ejemplo, la aplicación empieza a trabajar con otro artículo), también se libera su entrada de la caché.

PHP JIT (Compilador Just in Time)

Quizás sepa que PHP se compila en el llamado opcode, que son instrucciones de bajo nivel que puede ver por ejemplo aquí y que ejecuta la máquina virtual de PHP. ¿Y qué es JIT? JIT puede compilar PHP de forma transparente directamente a código máquina, que ejecuta directamente el procesador, por lo que se evita la ejecución más lenta de la máquina virtual.

JIT, por lo tanto, tiene como objetivo acelerar PHP.

El esfuerzo por implementar JIT en PHP se remonta a 2011 y detrás de él está Dmitry Stogov. Desde entonces, ha probado 3 implementaciones diferentes, pero ninguna de ellas llegó a la versión estable de PHP por estas razones: el resultado nunca fue un aumento sustancial del rendimiento para aplicaciones web típicas; complica el mantenimiento de PHP (es decir, nadie excepto Dmitry lo entiende 😉); existían otras formas de mejorar el rendimiento sin necesidad de usar JIT.

El salto en el rendimiento de PHP en la versión 7 fue un subproducto del trabajo en JIT, aunque paradójicamente no se llegó a implementar. Esto ocurre ahora en PHP 8. Pero frenaré de inmediato las expectativas exageradas: probablemente no notará ninguna aceleración.

Entonces, ¿por qué entra JIT en PHP? Por un lado, otras vías para mejorar el rendimiento se están agotando lentamente y JIT simplemente está en la lista. Aunque no aporta aceleración en aplicaciones web comunes, acelera fundamentalmente, por ejemplo, los cálculos matemáticos. Se abre así la posibilidad de empezar a escribir estas cosas en PHP. Y, de hecho, sería posible implementar directamente en PHP funciones que hasta ahora requerían implementación directa en C por velocidad.

JIT forma parte de la extensión opcache y se activa junto con ella en php.ini (lea la documentación sobre ese cuarteto de dígitos):

zend_extension=php_opcache.dll
opcache.jit=1205              ; configuración mediante el cuarteto OTRC
opcache.enable_cli=1          ; para que funcione también en CLI
opcache.jit_buffer_size=128M  ; memoria reservada para el código compilado

Que JIT está funcionando lo sabrá, por ejemplo, en el panel de información en la Barra Tracy.

JIT funciona muy bien cuando todas las variables tienen tipos claramente definidos y no pueden cambiar durante llamadas repetidas al mismo código. Por lo tanto, tengo curiosidad por saber si algún día declararemos tipos también para las variables en PHP: string $s = 'Hola, esta es la conclusión de la serie';