PHP 7.0 est sorti le 3 décembre. Un mois et demi plus tard, j’ai migré deux projets et les résultats sont difficiles à ignorer.
Le chiffre phare : 2x plus rapide que PHP 5.6. Ce n’est pas un benchmark cherry-pick — c’est la médiane sur des applications réelles. Le Zend Engine a été réécrit pour utiliser une nouvelle représentation interne des valeurs, ce qui réduit significativement l’utilisation mémoire et diminue les allocations. Sur un projet, le temps de réponse moyen a chuté de 40% sans aucune modification du code. On met à jour, et ça va plus vite.
Mais les performances ne sont pas la partie la plus intéressante.
Les types, enfin
PHP a eu les type hints pour les objets depuis la 5.0, pour les tableaux depuis la 5.1. En 7.0, on peut enfin déclarer des types scalaires pour les paramètres de fonctions et les valeurs de retour :
function add(int $a, int $b): int {
return $a + $b;
}
En mode strict (declare(strict_types=1)), passer un float à cette fonction lève une TypeError. En mode coercitif par défaut, PHP convertit la valeur. Cette distinction compte : le mode strict est par fichier, on peut donc l’adopter progressivement sans tout casser d’un coup.
Les déclarations de type de retour constituent l’autre moitié. Placer l’intention dans la signature plutôt que dans un docblock signifie que c’est le moteur qui l’applique, pas un code reviewer à moitié endormi.
L’opérateur null coalescent
?? est petit mais utilisé en permanence :
$username = $_GET['user'] ?? 'guest';
Ça remplace isset($_GET['user']) ? $_GET['user'] : 'guest'. Il se chaîne aussi : $a ?? $b ?? $c. Après des années de bruit avec isset(), ça seul valait la mise à jour.
La partie qui casse
La refonte de la gestion des erreurs est le vrai risque lors de la migration. Beaucoup d’erreurs fatales sont maintenant des exceptions Error, attrapables mais différentes des Exception. Le code qui comptait sur les erreurs fatales pour stopper l’exécution silencieusement a maintenant besoin d’une gestion explicite. La suppression d’erreurs avec @ fonctionne aussi différemment par endroits.
Lire le guide de migration avant de toucher une appli en production. Le gain est réel, mais le fossé entre 5.6 et 7.0 est le plus large que PHP ait jamais eu.
L’opérateur vaisseau spatial
<=> est un opérateur de comparaison combiné qui retourne -1, 0 ou 1. Il est surtout là pour le tri :
usort($users, function ($a, $b) {
return $a->age <=> $b->age;
});
Avant ça, un comparateur de tri personnalisé était un petit exercice de mémoire arithmétique. $a - $b fonctionne pour les entiers mais plante silencieusement pour les flottants. <=> fait ce qu’il faut pour chaque type comparable.
Les classes anonymes
On peut maintenant instancier une classe définie en ligne, sur le moment, sans lui donner de nom :
$logger = new class($config) implements LoggerInterface {
public function __construct(private array $config) {}
public function log(string $message): void {
file_put_contents($this->config['path'], $message . PHP_EOL, FILE_APPEND);
}
};
Le cas d’usage canonique, ce sont les doublures de test et les implémentations d’interface ponctuelles qui ne méritent pas un fichier. Ça supprime une vraie friction : le fossé entre “j’ai besoin d’un objet” et “je dois créer un fichier de classe pour un truc de 10 lignes”.
Aléatoire cryptographiquement sûr
Les rand() et mt_rand() de PHP 5 n’ont jamais été conçus pour la sécurité. La 7.0 ajoute deux fonctions qui le sont :
$token = bin2hex(random_bytes(32)); // token hexadécimal de 64 caractères
$pin = random_int(100000, 999999);
random_bytes() puise dans le CSPRNG du système d’exploitation. random_int() enveloppe ça pour les entiers. Ces fonctions remplacent tous les schémas de génération de tokens maison qui faisaient ça mal en silence, ce qui représente la majorité d’entre eux.
Les déclarations use groupées
Avant 7.0, importer cinq éléments depuis le même namespace nécessitait cinq instructions use. Maintenant :
use App\Model\{User, Order, Product};
use function App\Helpers\{formatDate, slugify};
use const App\Config\{MAX_RETRIES, TIMEOUT};
Petite amélioration ergonomique, mais qui réduit le bruit visuel en haut des fichiers avec des hiérarchies de namespaces profondes.
Les générateurs ont grandi
Les générateurs en 5.5 étaient intéressants mais incomplets. La 7.0 ajoute deux choses. Premièrement, un générateur peut maintenant avoir une valeur de retour, accessible après la fin de l’itération :
function process(): Generator {
yield 'step 1';
yield 'step 2';
return 'done';
}
$gen = process();
foreach ($gen as $step) { /* ... */ }
echo $gen->getReturn(); // "done"
Deuxièmement, yield from délègue à un autre générateur ou itérable, en transmettant transparemment les valeurs et valeurs de retour :
function inner(): Generator {
yield 1;
yield 2;
return 'inner done';
}
function outer(): Generator {
$result = yield from inner();
echo $result; // "inner done"
yield 3;
}
Ça rend la composition de générateurs pratique sans avoir à câbler manuellement les valeurs entre eux.
Closure::call()
Une façon plus directe de lier une closure à un objet et de l’appeler immédiatement :
class Counter {
private int $count = 0;
}
$increment = function (int $by): void {
$this->count += $by;
};
$increment->call(new Counter(), 5);
bindTo() existait avant mais nécessitait deux étapes. call() les fusionne et est plus rapide à l’exécution car il évite la création d’une closure intermédiaire.
Syntaxe d’échappement Unicode dans les chaînes
On peut maintenant intégrer des caractères Unicode directement dans les chaînes entre guillemets doubles ou les heredocs via un point de code :
echo "\u{1F418}"; // 🐘
echo "\u{00E9}"; // é
C’est mieux que de copier-coller des caractères depuis une table Unicode dans les fichiers sources, ce que les gens faisaient vraiment.
Un unserialize() plus sûr
unserialize() a une longue histoire d’être un vecteur d’attaques par injection d’objets. La 7.0 ajoute une option allowed_classes :
$data = unserialize($input, ['allowed_classes' => false]);
$data = unserialize($input, ['allowed_classes' => [User::class, Order::class]]);
Passer false empêche toute instanciation d’objet pendant la désérialisation. C’est le comportement par défaut à adopter quand on désérialise des données non fiables.
:1234: Division entière
intdiv() est une division entière explicite sans intermédiaire flottant :
$pages = intdiv(count($items), $perPage); // int, pas besoin de cast
Oui, on pourrait caster le résultat d’une division. intdiv() rend l’intention claire et évite les cas limites de précision flottante que le cast introduit pour les grands nombres.
Les constantes en tableaux
Avant 7.0, define() n’acceptait que les valeurs scalaires. Les tableaux fonctionnaient avec const au niveau de la classe ou du namespace mais pas avec define(). Maintenant si :
define('HTTP_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
Utile pour la configuration qui doit être une constante mais qui vit en dehors d’une classe.
Des assertions avec des dents
assert() a reçu une vraie refonte. En PHP 5, les assertions étaient un eval de chaînes à l’exécution. Maintenant elles peuvent lever des exceptions et être complètement supprimées en production avec zéro overhead :
// Dans php.ini ou au bootstrap :
// assert.active = 1 (dev), 0 (prod)
// assert.exception = 1
assert($user->isVerified(), new \LogicException('Unverified user reached checkout'));
Quand assert.active = 0, l’expression n’est jamais évaluée. Quand c’est activé, une assertion qui échoue lève directement l’exception fournie. C’est enfin un outil qu’on peut utiliser sans honte.
La refonte de session_start()
session_start() accepte maintenant un tableau d’options qui surchargent les directives php.ini pour cet appel :
session_start([
'cookie_lifetime' => 86400,
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
]);
Avant ça, on définissait soit les options globalement dans php.ini, soit on appelait ini_set() avant session_start(). Aucune des deux n’était top quand on avait besoin de configurations de session différentes dans différentes parties d’une appli.