PHP 8.0 : Nouvelles fonctions, classes et JIT (4/4)

il y a 4 ans par David Grudl  

La version 8.0 de PHP est sortie. Elle est tellement pleine de nouveautés qu'aucune version précédente ne l'a été. Leur présentation a nécessité quatre articles distincts. Dans ce dernier, nous allons examiner les nouvelles fonctions et classes et présenter le Just in Time Compiler.

Nouvelles fonctions

La bibliothèque standard de PHP dispose de centaines de fonctions et la version 8.0 en a ajouté six nouvelles. Cela semble peu, mais la plupart d'entre elles comblent des points faibles du langage. Ce qui correspond bien à l'esprit de la version 8.0 dans son ensemble, qui peaufine et consolide PHP comme aucune version précédente. Vous trouverez un aperçu de toutes les nouvelles fonctions et méthodes dans le guide de migration.

str_contains() str_starts_with() str_ends_with()

Fonctions pour déterminer si une chaîne commence, se termine ou contient une sous-chaîne.

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

Avec l'arrivée de ce trio, PHP définit comment traiter la chaîne vide lors de la recherche, ce qui régit également toutes les autres fonctions connexes, de telle sorte que la chaîne vide se trouve partout :

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

Grâce à cela, le comportement du trio est totalement identique aux équivalents dans Nette :

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

Pourquoi ces fonctions sont-elles si importantes ? Les bibliothèques standard de tous les langages sont toujours chargées de l'évolution historique et il est impossible d'éviter l'apparition d'incohérences et de faux pas. Mais en même temps, c'est la carte de visite de ce langage. Il est étonnant que PHP, âgé de 25 ans, manque de fonctions pour des opérations aussi basiques que le renvoi du premier ou du dernier élément d'un tableau, l'échappement HTML sans pièges (htmlspecialchars n'échappe pas l'apostrophe), ou justement la recherche d'une chaîne dans une chaîne. Le fait que cela puisse être contourné d'une manière ou d'une autre ne tient pas, car le résultat n'est alors pas un code lisible et compréhensible. C'est une leçon pour tous les auteurs d'API. Lorsque vous voyez qu'une partie considérable de la documentation d'une fonction est consacrée à l'explication des subtilités (comme les valeurs de retour de strpos), c'est un signal clair pour modifier la bibliothèque et ajouter précisément str_contains.

get_debug_type()

Remplace l'ancien get_type(). Au lieu de types longs comme integer, il renvoie les types utilisés aujourd'hui comme int, dans le cas des objets, il renvoie directement le type :

Valeur 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)

Objectification des ressources

Les valeurs de type resource proviennent de l'époque où PHP n'avait pas encore d'objets, mais en avait en fait besoin. C'est ainsi que les ressources sont apparues. Aujourd'hui, nous avons des objets et, par rapport aux ressources, ils fonctionnent beaucoup mieux avec le garbage collector, il est donc prévu de les remplacer progressivement tous par des objets.

À partir de PHP 8.0, les ressources images, connexions curl, openssl, xml, etc. sont transformées en objets. En PHP 8.1, ce sera le tour des connexions FTP, etc.

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

Ces objets n'ont pour l'instant aucune méthode, et vous ne pouvez pas non plus créer directement leurs instances. Il s'agit pour l'instant vraiment juste de se débarrasser des ressources obsolètes de PHP sans changer l'API. Et c'est une bonne chose, car la création d'une bonne API est une tâche distincte et exigeante. Personne ne souhaite que de nouvelles classes comme SplFileObject avec des méthodes nommées fgetc() ou fgets() apparaissent en PHP.

PhpToken

Le tokenizer est également déplacé vers les objets, et donc les fonctions autour de token_get_all. Cette fois, il ne s'agit pas de se débarrasser des ressources, mais nous obtenons un objet à part entière représentant un jeton PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instance 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

La méthode isIgnorable() renvoie true pour les jetons T_WHITESPACE, T_COMMENT, T_DOC_COMMENT et T_OPEN_TAG.

Weak maps

Les weak maps sont liées au garbage collector, qui libère de la mémoire tous les objets et valeurs qui ne sont plus utilisés (c'est-à-dire qu'il n'y a aucune variable ou propriété utilisée qui les contiendrait). Comme la durée de vie d'un thread PHP est éphémère et que nous disposons aujourd'hui de suffisamment de mémoire sur les serveurs, nous ne nous préoccupons généralement pas du tout des questions relatives à la libération efficace de la mémoire. Mais pour les scripts qui s'exécutent plus longtemps, elles sont essentielles.

L'objet WeakMap est similaire à SplObjectStorage. Dans les deux cas, les objets sont utilisés comme clés et permettent de stocker des valeurs arbitraires sous celles-ci. La différence est que WeakMap n'empêche pas l'objet d'être libéré par le garbage collector. C'est-à-dire que si le seul endroit où l'objet se trouve encore est la clé dans la weak map, il sera supprimé de la map et de la mémoire.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'données pour $obj';

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

À quoi cela sert-il ? Par exemple, pour la mise en cache. Supposons que nous ayons une méthode loadComments(), à laquelle nous passons un article de blog et elle renvoie tous ses commentaires. Comme la méthode est appelée de manière répétée avec le même article, nous allons créer une autre méthode getComments(), qui mettra en cache le résultat de la première méthode :

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]
	}

	...
}

L'astuce est qu'au moment où l'objet $article est libéré (par exemple, l'application commence à travailler avec un autre article), son entrée dans le cache est également libérée.

PHP JIT (Just in Time Compiler)

Vous savez peut-être que PHP est compilé en ce qu'on appelle l'opcode, qui sont des instructions de bas niveau que vous pouvez visualiser par exemple ici et qui sont exécutées par la machine virtuelle PHP. Et qu'est-ce que le JIT ? Le JIT peut compiler PHP de manière transparente directement en code machine, qui est exécuté directement par le processeur, ce qui évite l'exécution plus lente par la machine virtuelle.

Le JIT est donc censé accélérer PHP.

L'effort d'implémenter le JIT dans PHP remonte à 2011 et est dû à Dmitry Stogov. Depuis lors, il a essayé 3 implémentations différentes, mais aucune d'entre elles n'est arrivée dans la version stable de PHP, et ce pour les raisons suivantes : le résultat n'a jamais été une augmentation significative des performances pour les applications web typiques ; cela complique la maintenance de PHP (c'est-à-dire que personne d'autre que Dmitry ne comprend 😉) ; il existait d'autres moyens d'améliorer les performances sans avoir à utiliser le JIT.

L'augmentation spectaculaire des performances de PHP dans la version 7 a été un produit dérivé du travail sur le JIT, bien que paradoxalement, il n'ait pas été déployé. Cela n'arrive qu'en PHP 8. Mais je vais tout de suite freiner les attentes excessives : vous ne remarquerez probablement aucune accélération.

Pourquoi alors le JIT entre-t-il dans PHP ? D'une part, les autres voies d'amélioration des performances s'épuisent lentement et le JIT est tout simplement le prochain sur la liste. Bien qu'il n'apporte pas d'accélération dans les applications web courantes, il accélère considérablement, par exemple, les calculs mathématiques. Cela ouvre la possibilité de commencer à écrire ces choses en PHP. Et en fait, il serait ainsi possible d'implémenter directement en PHP des fonctions qui nécessitaient jusqu'à présent une implémentation directement en C pour des raisons de vitesse.

Le JIT fait partie de l'extension opcache et s'active en même temps qu'elle dans php.ini (lisez la documentation concernant ces quatre chiffres) :

zend_extension=php_opcache.dll
opcache.jit=1205              ; configuration via le quatuor OTRC
opcache.enable_cli=1          ; pour que cela fonctionne aussi en CLI
opcache.jit_buffer_size=128M  ; mémoire réservée pour le code compilé

Vous saurez que le JIT fonctionne, par exemple, dans le panneau d'information de la barre Tracy.

Le JIT fonctionne très bien lorsque toutes les variables ont des types clairement définis et ne peuvent pas changer lors d'appels répétés du même code. Je suis donc curieux de savoir si un jour nous déclarerons également les types des variables en PHP : string $s = 'Bonjour, ceci est la fin de la série';