Pourquoi PHP 8.x ?

"PHP is dead" — the rumor that refuses to die.

En réalité : PHP 8.3 est 2× plus rapide que PHP 5, propulsé par un compilateur JIT. Laravel, Symfony, WordPress (43% du web) — PHP est partout.

🐘
PHP 8.0 — 2020

JIT compiler, match(), named args, nullsafe ?->, union types

PHP 8.1 — 2021

Enums, readonly, Fibers, intersection types, never

🚀
PHP 8.2-8.3 — 2022-23

readonly classes, DNF types, typed class constants, json_validate()

PHP vs Python vs JS (comparaison rapide)

FonctionnalitéPHP 8.xPythonJavaScript
Typageint|string $xx: int | strTypeScript only
Null safe$obj?->method()obj?.method() (N/A)obj?.method()
Matchingmatch($x) {...}match x: case ...switch/if
Enumsenum Status: stringclass Status(Enum)TypeScript enums
AsyncFibers / ReactPHPasync/await natifasync/await natif
PackagesComposer / Packagistpip / PyPInpm / Yarn

Types & déclarations

PHP 8 introduit un système de types riche. La première chose à faire dans tout fichier PHP moderne :

<?php
declare(strict_types=1);
// Maintenant int|string $x est STRICTEMENT vérifié
// passer "5" à une fonction attendant int → TypeError

Union types & types spéciaux

<?php
declare(strict_types=1);

// Union types (PHP 8.0)
function formatId(int|string $id): string {
    return is_int($id) ? "ID-{$id}" : strtoupper($id);
}

// mixed — accepte tout (dernier recours)
function debug(mixed $value): void {
    var_dump($value);
}

// never — fonction qui ne retourne jamais (throw ou exit)
function abort(string $message): never {
    throw new RuntimeException($message);
}

// Nullsafe operator (PHP 8.0)
class User {
    public ?Address $address = null;
}
class Address {
    public ?City $city = null;
}
class City {
    public string $name = 'Paris';
}

$user = new User();
$cityName = $user?->address?->city?->name; // null, pas d'erreur

// Named arguments (PHP 8.0)
function createUser(
    string $name,
    int    $age = 18,
    string $role = 'user'
): array {
    return compact('name', 'age', 'role');
}

// Sans named args → ordre obligatoire
createUser('Alice', 25, 'admin');

// Avec named args → clarté + skip optionnels
createUser(name: 'Bob', role: 'editor');
Bonne pratique Laravel : Toujours mettre declare(strict_types=1) en tête de tous tes fichiers PHP. Laravel lui-même l'utilise.

POO avancée

readonly & constructor promotion

<?php
declare(strict_types=1);

// PHP 8.0 — Constructor promotion
// AVANT :
class OldUser {
    public string $name;
    public int $age;
    public function __construct(string $name, int $age) {
        $this->name = $name;
        $this->age  = $age;
    }
}

// APRÈS (PHP 8.0+) :
class User {
    public function __construct(
        public readonly string $name,    // readonly = PHP 8.1
        public readonly string $email,
        public int $age = 18,
    ) {}
}

$user = new User('Alice', 'alice@test.com');
echo $user->name;   // 'Alice'
$user->name = 'Bob'; // Error: readonly property

Interfaces, Traits, Abstract

<?php
declare(strict_types=1);

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): static;
}

trait Timestampable {
    private ?DateTimeImmutable $createdAt = null;
    private ?DateTimeImmutable $updatedAt = null;

    public function touch(): void {
        $now = new DateTimeImmutable();
        $this->createdAt ??= $now;
        $this->updatedAt = $now;
    }

    public function getCreatedAt(): ?DateTimeImmutable {
        return $this->createdAt;
    }
}

abstract class BaseModel {
    use Timestampable;

    abstract public function validate(): bool;

    public function save(): void {
        if ($this->validate()) {
            $this->touch();
            // persist...
        }
    }
}

// #[Attribute] — PHP 8.0
#[Attribute(Attribute::TARGET_METHOD)]
class Route {
    public function __construct(
        public readonly string $path,
        public readonly string $method = 'GET',
    ) {}
}

readonly class (PHP 8.2)

<?php
declare(strict_types=1);

// PHP 8.2 — readonly class : toutes les propriétés sont readonly
readonly class Point {
    public function __construct(
        public float $x,
        public float $y,
        public float $z = 0.0,
    ) {}

    public function distanceTo(Point $other): float {
        return sqrt(
            ($this->x - $other->x) ** 2 +
            ($this->y - $other->y) ** 2 +
            ($this->z - $other->z) ** 2
        );
    }
}

$origin = new Point(0, 0);
$dest   = new Point(3, 4);
echo $dest->distanceTo($origin); // 5.0

match & Fibers

match() — meilleur switch

<?php
declare(strict_types=1);

// switch : comparaison lâche (==), pas de retour de valeur
$status = 'published';

// Ancienne façon
switch ($status) {
    case 'draft':     $label = 'Brouillon'; break;
    case 'published': $label = 'Publié';    break;
    default:          $label = 'Inconnu';
}

// Nouvelle façon avec match()
$label = match($status) {
    'draft'     => 'Brouillon',
    'published' => 'Publié',
    'archived'  => 'Archivé',
    default     => throw new ValueError("Statut inconnu: {$status}"),
};

// match() avec conditions complexes
$httpCode = 422;
$type = match(true) {
    $httpCode >= 500 => 'server_error',
    $httpCode >= 400 => 'client_error',
    $httpCode >= 300 => 'redirect',
    $httpCode >= 200 => 'success',
    default          => 'unknown',
};
echo $type; // 'client_error'

// match() multi-valeurs
$day = 3;
$type = match($day) {
    1, 2, 3, 4, 5 => 'weekday',
    6, 7           => 'weekend',
};

Fibers (PHP 8.1)

<?php
declare(strict_types=1);

// Fiber = coroutine PHP — suspend/reprend l'exécution
$fiber = new Fiber(function(): void {
    $value = Fiber::suspend('premier'); // suspend et envoie 'premier'
    echo "Fiber a reçu : {$value}\n";
    Fiber::suspend('deuxième');
    echo "Fiber terminée\n";
});

$val1 = $fiber->start();          // démarre, reçoit 'premier'
echo "Main reçoit : {$val1}\n";   // "Main reçoit : premier"
$val2 = $fiber->resume('hello');  // reprend, envoie 'hello'
echo "Main reçoit : {$val2}\n";   // "Main reçoit : deuxième"
$fiber->resume();                 // termine la fiber

// Fibonacci non-bloquant avec Fiber
$fibonacci = new Fiber(function(): void {
    $a = 0; $b = 1;
    while (true) {
        Fiber::suspend($a);
        [$a, $b] = [$b, $a + $b];
    }
});

$fibonacci->start();
for ($i = 0; $i < 10; $i++) {
    echo $fibonacci->resume() . ' '; // 0 1 1 2 3 5 8 13 21 34
}
Note Laravel : Laravel utilise les Fibers en interne via Octane + Swoole pour le traitement concurrent. En apprentissage, concentre-toi sur match() — tu l'utiliseras tous les jours.

Enums (PHP 8.1)

<?php
declare(strict_types=1);

// Pure enum (sans valeur associée)
enum Direction {
    case North;
    case South;
    case East;
    case West;
}

$dir = Direction::North;
var_dump($dir === Direction::North); // true

// Backed enum (string)
enum Status: string {
    case Draft     = 'draft';
    case Published = 'published';
    case Archived  = 'archived';

    public function label(): string {
        return match($this) {
            Status::Draft     => 'Brouillon',
            Status::Published => 'Publié',
            Status::Archived  => 'Archivé',
        };
    }

    public function isPublic(): bool {
        return $this === Status::Published;
    }

    // Méthode statique utile
    public static function fromLabel(string $label): self {
        foreach (self::cases() as $case) {
            if ($case->label() === $label) return $case;
        }
        throw new ValueError("Label inconnu: {$label}");
    }
}

// Utilisation
$s = Status::Published;
echo $s->value;      // 'published'
echo $s->label();    // 'Publié'
echo $s->isPublic(); // true (1)

// from() et tryFrom()
$s2 = Status::from('draft');           // Status::Draft
$s3 = Status::tryFrom('invalid');      // null (pas d'exception)

// cases() — lister toutes les valeurs
$all = Status::cases();
// [Status::Draft, Status::Published, Status::Archived]

// Backed enum integer
enum Priority: int {
    case Low    = 1;
    case Medium = 2;
    case High   = 3;

    public function isUrgent(): bool {
        return $this->value >= Priority::High->value;
    }
}

// Enums dans Eloquent (Laravel)
// $casts = ['status' => Status::class]
// $post->status === Status::Published  ✓

Namespaces & Autoloading

<?php
declare(strict_types=1);

// Namespaces — évitent les conflits de noms
namespace App\Models;

use App\Contracts\Serializable;
use App\Traits\Timestampable;
use Carbon\Carbon;           // package externe
use Illuminate\Database\Eloquent\Model as EloquentModel; // alias

class User extends EloquentModel implements Serializable {
    use Timestampable;
    // ...
}

PSR-4 Autoloading avec Composer

// composer.json
{
    "name": "monapp/monapp",
    "require": {
        "php": "^8.3",
        "laravel/framework": "^11.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0",
        "fakerphp/faker": "^1.23"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        }
    },
    "scripts": {
        "test": "php artisan test",
        "analyse": "vendor/bin/phpstan analyse"
    }
}
# Installer les dépendances
composer install

# Ajouter un package
composer require spatie/laravel-permission

# Dev uniquement
composer require --dev barryvdh/laravel-debugbar

# Mettre à jour l'autoloader
composer dump-autoload

# Optimiser (production)
composer install --no-dev --optimize-autoloader
Convention PSR-4 Laravel : App\Models\Userapp/Models/User.php. Le namespace suit le chemin de fichier depuis le dossier racine défini dans composer.json.

Composer — Gestionnaire de dépendances

# Créer un projet Laravel
composer create-project laravel/laravel mon-projet

# Packages Laravel courants
composer require laravel/sanctum          # Auth API tokens
composer require spatie/laravel-permission # RBAC
composer require darkaonline/l5-swagger   # Documentation API
composer require predis/predis             # Redis client

# Dev tools
composer require --dev phpstan/phpstan
composer require --dev pestphp/pest
composer require --dev barryvdh/laravel-ide-helper

# Voir les dépendances installées
composer show

# Vérifier les vulnérabilités
composer audit

# Mettre à jour toutes les dépendances
composer update

Scripts Composer utiles

{
    "scripts": {
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-update-cmd": [
            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi",
            "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
            "@php artisan migrate --graceful --ansi"
        ]
    }
}

Gestion d'erreurs

<?php
declare(strict_types=1);

// Exceptions typées — mieux que les codes d'erreur
class UserNotFoundException extends RuntimeException {
    public function __construct(int $userId) {
        parent::__construct("Utilisateur #{$userId} introuvable", 404);
    }
}

class ValidationException extends RuntimeException {
    public function __construct(
        private readonly array $errors,
        string $message = 'Validation échouée'
    ) {
        parent::__construct($message, 422);
    }

    public function getErrors(): array {
        return $this->errors;
    }
}

// try / catch / finally
function findUser(int $id): array {
    if ($id <= 0) {
        throw new \InvalidArgumentException("ID invalide: {$id}");
    }
    // simuler une requête DB
    if ($id === 999) {
        throw new UserNotFoundException($id);
    }
    return ['id' => $id, 'name' => 'Alice'];
}

try {
    $user = findUser(999);
} catch (UserNotFoundException $e) {
    echo "404: " . $e->getMessage();
    // log, retourner une réponse HTTP 404...
} catch (\InvalidArgumentException $e) {
    echo "400: " . $e->getMessage();
} catch (\Throwable $e) {
    // catch-all — attrape aussi les Error
    echo "500: Erreur inattendue";
    error_log($e->getMessage());
} finally {
    // s'exécute toujours
    echo "\nNettoyage des ressources";
}

// SPL Exceptions utiles
// LogicException, RuntimeException, InvalidArgumentException,
// OutOfRangeException, UnderflowException, OverflowException

// set_exception_handler — gestionnaire global (usage rare en Laravel)
set_exception_handler(function(\Throwable $e): void {
    http_response_code($e->getCode() ?: 500);
    echo json_encode(['error' => $e->getMessage()]);
});
Dans Laravel : Tu n'as pas besoin de set_exception_handler. Le handler est dans bootstrap/app.php via ->withExceptions(). Toutes les exceptions non catchées y atterrissent.
✏️ Exercices Module 01 ▶ Mini-projet 🧠 QCM Module 01 Module 02 →