Modern PHP Engineering
Chapitre 2
02 — PHP Objets
02 — PHP Objets
Cours 02 — PHP Objets
02.1 Classes, propriétés, méthodes
Syntaxe de base
<?php
declare(strict_types=1);
class User
{
// Propriétés typées (PHP 7.4+)
public readonly int $id; // PHP 8.1+ : readonly
private string $name;
protected ?string $email = null;
// Constructor promotion (PHP 8.0+)
public function __construct(
readonly int $id,
private string $name,
private ?string $email = null,
) {
// Le constructeur est optionnel avec promotion
}
// Méthodes
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static // PHP 8.0+ : static return type
{
$this->name = $name;
return $this; // fluent interface
}
}
$user = new User(id: 1, name: 'Alice', email: 'alice@example.com');
echo $user->getName(); // Alice
Constructor promotion (PHP 8.0+)
Élimine la redondance de déclaration + assignation :
<?php
// AVANT PHP 8.0
class User {
private string $name;
private 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(
private string $name,
private int $age,
) {}
}
Readonly properties (PHP 8.1+) et Readonly classes (PHP 8.2+)
<?php
// Propriétés readonly
class Config {
public function __construct(
public readonly string $key,
public readonly mixed $value,
) {}
}
// Classe readonly — toutes les propriétés sont readonly (PHP 8.2+)
readonly class ValueObject {
public function __construct(
public string $name,
public int $version,
) {}
}
02.2 Héritage et polymorphisme
<?php
declare(strict_types=1);
class Animal {
public function __construct(
protected string $name,
protected int $age,
) {}
public function speak(): string
{
return '...';
}
final public function getId(): string // Ne peut pas être surchargée
{
return spl_object_id($this);
}
}
class Dog extends Animal {
// Surcharge
public function speak(): string
{
return 'Woof!';
}
public function fetch(): string
{
return "{$this->name} rapporte la balle";
}
}
// Polymorphisme
function makeSound(Animal $animal): string {
return $animal->speak();
}
echo makeSound(new Dog('Rex', 3)); // Woof!
// Vérification de type
$animals = [new Dog('Rex', 3), new Animal('Generic', 1)];
foreach ($animals as $animal) {
if ($animal instanceof Dog) {
echo $animal->fetch();
}
}
02.3 Interfaces
<?php
declare(strict_types=1);
interface LoggerInterface
{
public function log(string $message, LogLevel $level = LogLevel::Info): void;
}
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): void;
public function delete(string $key): bool;
}
// Implémentation multiple
class FileLogger implements LoggerInterface, CacheInterface
{
public function log(string $message, LogLevel $level = LogLevel::Info): void
{
file_put_contents(
'app.log',
sprintf('[%s] %s: %s', date('c'), $level->value, $message) . "\n",
FILE_APPEND
);
}
public function get(string $key): mixed
{
$file = "cache/$key.cache";
if (!file_exists($file)) return null;
return unserialize(file_get_contents($file));
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
file_put_contents("cache/$key.cache", serialize($value));
}
public function delete(string $key): bool
{
$file = "cache/$key.cache";
if (!file_exists($file)) return false;
return unlink($file);
}
}
// Interface avec constants (PHP 8.1+ dans les interfaces)
interface HttpStatusInterface {
public const int OK = 200;
public const int NOT_FOUND = 404;
public const int SERVER_ERROR = 500;
}
// Interface avec types génériques simulés
interface RepositoryInterface
{
/** @return array<object> */
public function findAll(): array;
public function findById(int|string $id): ?object;
public function save(object $entity): void;
public function delete(object $entity): void;
}
02.4 Traits
<?php
declare(strict_types=1);
// Trait — réutilisation horizontale
trait Timestampable
{
private \DateTimeImmutable $createdAt;
private ?\DateTimeImmutable $updatedAt = null;
public function initializeTimestamps(): void
{
$this->createdAt = new \DateTimeImmutable();
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function markAsUpdated(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
}
// Constantes dans traits (PHP 8.2+)
trait HttpStatusTrait {
public const int OK = 200;
public const int CREATED = 201;
}
// Résolution de conflits
trait A {
public function foo(): string { return 'A'; }
public function bar(): string { return 'A.bar'; }
}
trait B {
public function foo(): string { return 'B'; }
public function baz(): string { return 'B.baz'; }
}
class ConflictDemo {
use A, B {
A::foo insteadof B; // Utilise foo() de A
B::baz insteadof A; // Utilise baz() de B
B::foo as fooB; // foo() de B devient fooB()
}
}
// Utilisation
class Entity {
use Timestampable;
public function __construct(
private int $id,
private string $data,
) {
$this->initializeTimestamps();
}
public function update(string $data): void {
$this->data = $data;
$this->markAsUpdated();
}
}
Priorité de résolution (méthodes)
Trait > Classe parente
Classe courante > Trait
<?php
class Base {
public function foo(): string { return 'Base'; }
}
trait FooTrait {
public function foo(): string { return 'Trait'; }
}
class Child extends Base {
use FooTrait;
// foo() du trait remplace celle de Base
public function bar(): string { return 'Child'; }
// bar() de la classe prime sur tout
}
02.5 Classes abstraites
<?php
declare(strict_types=1);
abstract class Database
{
protected \PDO $pdo;
abstract public function connect(): void;
abstract public function query(string $sql): array;
public function beginTransaction(): bool
{
return $this->pdo->beginTransaction();
}
public function commit(): bool
{
return $this->pdo->commit();
}
public function rollback(): bool
{
return $this->pdo->rollBack();
}
}
class MySQLDatabase extends Database
{
public function connect(): void
{
$this->pdo = new \PDO(
'mysql:host=localhost;dbname=app',
'root',
'',
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
}
public function query(string $sql): array
{
$stmt = $this->pdo->query($sql);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
}
02.6 Late Static Binding (LSB)
<?php
declare(strict_types=1);
class BaseModel
{
protected static string $table = 'base';
public static function getTable(): string
{
// self:: retourne la classe où c'est ÉCRIT
// static:: retourne la classe APPELANTE (late binding)
return static::$table;
}
public static function find(int $id): ?static
{
// Retourne une instance de la classe appelante
$sql = "SELECT * FROM " . static::getTable() . " WHERE id = ?";
// ... requête
return null;
}
}
class UserModel extends BaseModel
{
protected static string $table = 'users';
}
class PostModel extends BaseModel
{
protected static string $table = 'posts';
}
echo UserModel::getTable(); // 'users' (static::)
echo PostModel::getTable(); // 'posts' (static::)
02.7 Magic Methods
<?php
declare(strict_types=1);
class DynamicObject
{
private array $data = [];
// __get — intercepte la lecture de propriétés inaccessibles
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
// __set — intercepte l'écriture de propriétés inaccessibles
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
// __isset — intercepte isset()/empty() sur propriétés inaccessibles
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
// __unset — intercepte unset() sur propriétés inaccessibles
public function __unset(string $name): void
{
unset($this->data[$name]);
}
// __call — intercepte les appels de méthodes inaccessibles
public function __call(string $name, array $arguments): mixed
{
$method = 'handle' . ucfirst($name);
if (method_exists($this, $method)) {
return $this->$method(...$arguments);
}
throw new \BadMethodCallException("Method $name not found");
}
// __callStatic — intercepte les appels statiques inaccessibles
public static function __callStatic(string $name, array $arguments): mixed
{
echo "Static call: $name\n";
return null;
}
// __invoke — rend l'objet "callable"
public function __invoke(mixed ...$args): string
{
return 'Called with: ' . implode(', ', $args);
}
// __toString
public function __toString(): string
{
return json_encode($this->data);
}
// __debugInfo — contrôle la sortie de var_dump()
public function __debugInfo(): array
{
return [
'data' => $this->data,
'class' => static::class,
];
}
// __serialize / __unserialize (PHP 7.4+) — préféré à __sleep/__wakeup
public function __serialize(): array
{
return $this->data;
}
public function __unserialize(array $data): void
{
$this->data = $data;
}
}
// Utilisation
$obj = new DynamicObject();
$obj->name = 'Alice'; // __set
echo $obj->name; // __get
echo isset($obj->name); // __isset
unset($obj->name); // __unset
$result = $obj('a', 'b', 'c'); // __invoke
echo $result; // 'Called with: a, b, c'
echo $obj; // __toString
02.8 Namespaces et autoloading
<?php
// src/Models/User.php
namespace App\Models;
use App\Enums\UserRole;
class User {
public function __construct(
private string $name,
private UserRole $role = UserRole::User,
) {}
}
// Alias et imports
use App\Models\User;
use App\Enums\UserRole as Role;
use function App\Helpers\formatDate;
use const App\Config\VERSION;
// Grouped imports (PHP 7.0+)
use App\Models\{User, Post, Comment};
use App\Exceptions\{ValidationException, NotFoundException};
PSR-4 Autoloading
{
"autoload": {
"psr-4": {
"App\\": "src/",
"Tests\\": "tests/"
}
}
}
02.9 instanceof et type declarations
<?php
declare(strict_types=1);
// instanceof
if ($entity instanceof User) {
echo "C'est un utilisateur";
}
// instanceof avec union types
class TypeChecker {
public function check(object $value): string {
return match (true) {
$value instanceof User => 'User',
$value instanceof Post => 'Post',
$value instanceof Comment => 'Comment',
default => 'Unknown',
};
}
}
// Type declarations avancées
class Service {
// self, parent, static
public function create(): static {
return new static();
}
// nullable
public function find(int $id): ?User {
// retourne User ou null
}
// union
public function save(User|array $data): void {}
// intersection (PHP 8.1+)
public function process(Countable&ArrayAccess $collection): void {}
}
02.10 Covariance et contravariance
<?php
declare(strict_types=1);
// Covariance — le type de retour peut être plus spécifique
abstract class Animal {
abstract public function makeSound(): string;
}
class Dog extends Animal {
public function makeSound(): string {
return 'Woof';
}
}
// Contravariance — le type de paramètre peut être plus général
interface Feedable {
public function feed(Animal $animal): void; // Animal (général)
}
class DogFeeder implements Feedable {
public function feed(Dog $dog): void { // Erreur ! Paramètre plus spécifique
// ...
}
}
// Correct :
class AnimalFeeder implements Feedable {
public function feed(Animal $animal): void { // Même type
// ...
}
}
// PHP 8.0+ : la contravariance est partiellement supportée
// pour les types nullable et union
interface Converter {
public function convert(string $input): int|string;
}
class StrictConverter implements Converter {
// OK : retour plus spécifique (covariance)
public function convert(string $input): int {
return (int) $input;
}
}
02.11 PHP 8.1+ Readonly et évolutions
Readonly properties (8.1)
<?php
declare(strict_types=1);
class UserDTO {
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email,
) {}
// Pas de setter possible pour les propriétés readonly
// Sauf dans le constructeur et __clone
public function __clone(): void {
$this->id = 0; // OK dans __clone
}
}
$dto = new UserDTO(1, 'Alice', 'alice@example.com');
// $dto->id = 2; // Error: Cannot modify readonly property
Readonly classes (8.2)
<?php
// Toutes les propriétés sont readonly
readonly class ConfigValue {
public function __construct(
public string $key,
public mixed $value,
) {}
}
Lazy objects (8.4)
<?php
$initializer = function (User $user): void {
$user->__construct(1, 'Alice', 'alice@example.com');
};
$proxy = ReflectionClass::newLazyGhost(User::class, $initializer);
// L'objet est créé mais pas initialisé
// L'initialisation se produit à la première lecture de propriété
Exercices
- Créez une classe
CollectionavecArrayAccess,Countable,IteratorAggregate - Implémentez un système de validation avec une interface
ValidatorInterface - Utilisez un trait
SoftDeletabledans un modèle - Écrivez une hiérarchie de classes avec covariance
- Créez un active record simple avec late static binding
Références
- php.net/manual/fr/language.oop5.php
- php.net/manual/fr/language.namespaces.php
- php.net/manual/fr/language.types.declarations.php