MFormations
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

ModeDescriptionUsage
tracingAnalyse les chemins d'exécutionRecommandé (perf max)
functionCompile fonction par fonctionStable, prévisible
offDé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

  1. Configurez JIT et mesurez les performances avec et sans
  2. Réécrivez un switch complexe en match expression
  3. Créez un enum OrderStatus avec méthodes
  4. Utilisez les fibers pour paralléliser des appels HTTP
  5. 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