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.x | Python | JavaScript |
|---|---|---|---|
| Typage | int|string $x | x: int | str | TypeScript only |
| Null safe | $obj?->method() | obj?.method() (N/A) | obj?.method() |
| Matching | match($x) {...} | match x: case ... | switch/if |
| Enums | enum Status: string | class Status(Enum) | TypeScript enums |
| Async | Fibers / ReactPHP | async/await natif | async/await natif |
| Packages | Composer / Packagist | pip / PyPI | npm / 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\User → app/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.