Modern PHP Engineering
Chapitre 3
03 — PHP 8.3
03 — PHP 8.3
Cours 03 — PHP 8.3
03.1 JIT et OpCache
Qu'est-ce que le JIT ?
JIT (Just-In-Time) compile le bytecode PHP en code machine natif à l'exécution. Il est disponible depuis PHP 8.0, amélioré en 8.4.
; php.ini — Configuration JIT
opcache.enable=1
opcache.jit=tracing ; tracing ou function
opcache.jit_buffer_size=128M ; Taille du buffer JIT
opcache.jit_debug=0
<?php
// Vérifier si JIT est actif
echo "JIT actif : " . (opcache_get_status()['jit']['en'] ?? 'non') . "\n";
echo "Buffer JIT : " . (ini_get('opcache.jit_buffer_size')) . "\n";
// Benchmark simple
$start = microtime(true);
$result = 0;
for ($i = 0; $i < 1000000; $i++) {
$result += sqrt($i) * sin($i);
}
$time = microtime(true) - $start;
echo "Temps : " . round($time, 4) . "s\n";
Modes JIT
| Mode | Description | Usage |
|---|---|---|
tracing | Analyse les chemins d'exécution | Recommandé (perf max) |
function | Compile fonction par fonction | Stable, prévisible |
off | Désactivé | Debug |
OpCache tuning
; php.ini — OpCache optimal
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.fast_shutdown=1
opcache.validate_timestamps=0 ; 0 en production
opcache.preload=/path/to/preload.php ; PHP 7.4+
03.2 Named arguments (PHP 8.0+)
<?php
declare(strict_types=1);
class HtmlRenderer
{
public function render(
string $template,
array $data = [],
bool $cache = true,
int $ttl = 3600,
?string $theme = null,
): string {
return "Render: $template (cache: " . ($cache ? 'on' : 'off') . ")";
}
}
$renderer = new HtmlRenderer();
// Avant : arguments positionnels
$renderer->render('home', ['title' => 'Home'], true, 3600, 'dark');
// Après : named arguments
$renderer->render(
template: 'home',
data: ['title' => 'Home'],
theme: 'dark',
// cache et ttl utilisent les valeurs par défaut
);
// Ordre flexible
$renderer->render(
theme: 'light',
template: 'about',
ttl: 7200,
);
03.3 Match expression (PHP 8.0+)
<?php
declare(strict_types=1);
enum HttpStatus: int
{
case Ok = 200;
case NotFound = 404;
case ServerError = 500;
}
function httpMessage(HttpStatus $status): string
{
// match est une expression (retourne une valeur)
return match ($status) {
HttpStatus::Ok => 'OK',
HttpStatus::NotFound => 'Not Found',
HttpStatus::ServerError => 'Server Error',
};
}
// Match avec conditions multiples et default
$age = 25;
$category = match (true) {
$age < 13 => 'Enfant',
$age < 18 => 'Adolescent',
$age < 65 => 'Adulte',
default => 'Senior',
};
// Match vs switch
// Avant (switch) :
switch ($status) {
case 200:
$msg = 'OK';
break;
case 404:
$msg = 'Not Found';
break;
default:
$msg = 'Error';
}
// Après (match) :
$msg = match ($status) {
200 => 'OK',
404 => 'Not Found',
default => 'Error',
};
03.4 Readonly properties (8.1) et classes (8.2)
<?php
declare(strict_types=1);
// Propriétés readonly (8.1)
class Config
{
public function __construct(
public readonly string $dbHost,
public readonly int $dbPort,
public readonly string $appEnv,
) {}
}
// Classes readonly (8.2) — toutes les propriétés sont readonly
readonly class Position
{
public function __construct(
public float $latitude,
public float $longitude,
) {
// Validation possible dans le constructeur
if ($latitude < -90 || $latitude > 90) {
throw new \InvalidArgumentException('Latitude invalide');
}
}
}
// Clonage d'une readonly class
$pos1 = new Position(48.8566, 2.3522);
$pos2 = clone $pos1; // OK
03.5 Enums (PHP 8.1+)
<?php
declare(strict_types=1);
// Enum basique
enum UserRole
{
case Admin;
case Editor;
case User;
case Guest;
}
// Backed enum (avec valeur)
enum HttpMethod: string
{
case Get = 'GET';
case Post = 'POST';
case Put = 'PUT';
case Delete = 'DELETE';
case Patch = 'PATCH';
}
enum HttpStatusCode: int
{
case Ok = 200;
case Created = 201;
case NotFound = 404;
case ServerError = 500;
}
// Méthodes sur les enums
enum PaymentStatus: string
{
case Pending = 'pending';
case Completed = 'completed';
case Failed = 'failed';
case Refunded = 'refunded';
public function label(): string
{
return match ($this) {
self::Pending => 'En attente',
self::Completed => 'Complété',
self::Failed => 'Échoué',
self::Refunded => 'Remboursé',
};
}
public function isFinal(): bool
{
return in_array($this, [self::Completed, self::Failed, self::Refunded], true);
}
}
// Implémentation d'interface
enum SortDirection: string implements \JsonSerializable
{
case Asc = 'asc';
case Desc = 'desc';
public function jsonSerialize(): string
{
return $this->value;
}
}
// Utilisation
$role = UserRole::Admin;
$method = HttpMethod::from('POST'); // From backed value
$method = HttpMethod::tryFrom('UNKNOW'); // null si invalide
// Match sur enum
function canAccess(UserRole $role): bool {
return match ($role) {
UserRole::Admin => true,
UserRole::Editor => true,
UserRole::User => false,
UserRole::Guest => false,
};
}
// Lister les cases
$cases = UserRole::cases(); // [Admin, Editor, User, Guest]
03.6 Fibers (PHP 8.1+)
<?php
declare(strict_types=1);
// Fiber = coroutine en PHP (coopératif, pas préemptif)
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('première suspension');
echo "Reprise avec : $value\n";
$result = Fiber::suspend('deuxième suspension');
echo "Reprise finale : $result\n";
return 'terminé';
});
// Démarrer le fiber
$value = $fiber->start();
echo "Reçu : $value\n"; // 'première suspension'
// Reprendre le fiber
$value = $fiber->resume('premier signal');
echo "Reçu : $value\n"; // 'deuxième suspension'
$result = $fiber->resume('second signal');
echo "Résultat final : $result\n"; // 'terminé'
// Exemple réel : HTTP requests concurrentes
function fetchUrls(array $urls): array
{
$fibers = [];
foreach ($urls as $id => $url) {
$fibers[$id] = new Fiber(function () use ($url): string {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return $result;
});
$fibers[$id]->start();
}
$results = [];
foreach ($fibers as $id => $fiber) {
$results[$id] = $fiber->resume();
}
return $results;
}
03.7 First-class callable (PHP 8.1+)
<?php
declare(strict_types=1);
// Avant PHP 8.1
$callback = function (int $n): int {
return strlen((string) $n);
};
// Après : first-class callable
$callback = strlen(...);
$callback = array_map(strlen(...), ['a', 'ab', 'abc']);
// Avec méthodes
class Calculator {
public function add(int $a, int $b): int { return $a + $b; }
public static function multiply(int $a, int $b): int { return $a * $b; }
}
$calc = new Calculator();
$addFn = $calc->add(...); // Closure sur méthode d'instance
$mulFn = Calculator::multiply(...); // Closure sur méthode statique
// Cas d'usage : passer une fonction à array_map
$numbers = ['1', '2', '3'];
$integers = array_map(intval(...), $numbers);
// [1, 2, 3]
03.8 Nullsafe operator (PHP 8.0+)
<?php
declare(strict_types=1);
class Address {
public function __construct(
public readonly ?string $city = null,
public readonly ?string $country = null,
) {}
}
class User {
public function __construct(
public readonly ?Address $address = null,
) {}
}
// Avant PHP 8.0
$city = null;
if ($user !== null) {
if ($user->address !== null) {
$city = $user->address->city;
}
}
// Après PHP 8.0 — nullsafe
$city = $user?->address?->city;
// Nullsafe avec méthodes
$country = $user?->address?->getCountry()?->getName();
// Nullsafe + null coalescing
$city = $user?->address?->city ?? 'Ville inconnue';
// Nullsafe sur les appels de méthode
$result = $service?->process($data)?->getResult();
// Nullsafe sur les accès statiques (PHP 8.0+)
$value = SomeClass::$property?->method();
03.9 Fonctions strings (PHP 8.0+)
<?php
declare(strict_types=1);
$text = 'Hello Modern PHP World!';
// str_contains — vérifie si une chaîne contient une sous-chaîne
str_contains($text, 'PHP'); // true
// str_starts_with
str_starts_with($text, 'Hello'); // true
// str_ends_with
str_ends_with($text, 'World!'); // true
// Avant PHP 8.0
$contains = strpos($text, 'PHP') !== false; // fonctionnel mais verbeux
// PHP 8.3+ : mb_str_pad
echo mb_str_pad('PHP', 10, '-', STR_PAD_BOTH); // '----PHP----'
// PHP 8.4+ : mb_trim, mb_ltrim, mb_rtrim
echo mb_trim(' café '); // 'café' (conserve le 'é' qui est multi-octets)
03.10 array_is_list (PHP 8.1+)
<?php
declare(strict_types=1);
array_is_list([0, 1, 2]); // true
array_is_list(['a', 'b', 'c']); // true
array_is_list([0 => 'a', 1 => 'b']); // true
array_is_list([1 => 'a', 2 => 'b']); // false (ne commence pas à 0)
array_is_list(['a' => 1, 'b' => 2]); // false
array_is_list([]); // true
// Cas d'usage : vérifier qu'on a un tableau indexé
function processList(array $data): void {
if (!array_is_list($data)) {
throw new \InvalidArgumentException('Un tableau indexé est attendu');
}
}
03.11 Randomizer (PHP 8.2+)
<?php
declare(strict_types=1);
use Random\Randomizer;
$randomizer = new Randomizer();
// Entier aléatoire
$int = $randomizer->getInt(1, 100);
// Élément aléatoire d'un tableau
$fruits = ['pomme', 'banane', 'cerise'];
$fruit = $randomizer->pickArrayKeys($fruits, 1); // [index]
$chosen = $fruits[$fruit[0]];
// Mélanger un tableau
$shuffled = $randomizer->shuffleArray($fruits);
// Bytes aléatoires (cryptographiquement sûr)
$bytes = $randomizer->getBytes(32);
// String aléatoire
$string = $randomizer->getBytesFromString(
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
16
);
// Seedable (reproductible)
$engine = new \Random\Engine\Mt19937(1234);
$rng = new Randomizer($engine);
echo $rng->getInt(1, 100); // Toujours le même résultat avec la seed 1234
03.12 json_validate (PHP 8.3+)
<?php
declare(strict_types=1);
// Avant PHP 8.3 : on décodait pour valider
function isValidJson(string $data): bool {
json_decode($data);
return json_last_error() === JSON_ERROR_NONE;
}
// Après PHP 8.3 : validation sans décodage
$valid = json_validate('{"name": "Alice", "age": 30}'); // true
$valid = json_validate('{invalid json}'); // false
// Plus performant pour les gros payloads
$largeJson = file_get_contents('large-dataset.json');
if (json_validate($largeJson)) {
$data = json_decode($largeJson, true);
}
03.13 Override attribute (PHP 8.3+)
<?php
declare(strict_types=1);
class BaseController
{
public function index(): string
{
return 'Base index';
}
protected function setup(): void {}
}
class UserController extends BaseController
{
// Vérifie que la méthode parente existe
#[\Override]
public function index(): string
{
return 'User index';
}
// #[\Override]
// public function notExist(): void {} // Fatal error : parent n'a pas cette méthode
#[\Override]
protected function setup(): void
{
parent::setup();
// Setup additionnel
}
}
03.14 Typed constants (PHP 8.3+)
<?php
declare(strict_types=1);
// PHP 8.3+ : constantes typées
class DatabaseConfig
{
public const string HOST = 'localhost';
public const int PORT = 3306;
public const string NAME = 'app_db';
public const array OPTIONS = [
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
];
}
// Dans les interfaces
interface ApiVersionInterface
{
public const int MAJOR = 2;
public const int MINOR = 1;
public const string STATUS = 'stable';
}
// Vérification au moment de la compilation
// const int FOO = 'string'; // TypeError à la compilation
// Avec les enums
enum Status: string
{
public const string PREFIX = 'STATUS_';
case Active = 'active';
case Inactive = 'inactive';
}
Exercices
- Configurez JIT et mesurez les performances avec et sans
- Réécrivez un switch complexe en match expression
- Créez un enum
OrderStatusavec méthodes - Utilisez les fibers pour paralléliser des appels HTTP
- Validez un gros fichier JSON avec json_validate
Références
- php.net/manual/fr/opcache.jit.php
- php.net/manual/fr/language.enumerations.php
- php.net/manual/fr/language.fibers.php
- wiki.php.net/rfc