MFormations
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

  1. Créez une classe Collection avec ArrayAccess, Countable, IteratorAggregate
  2. Implémentez un système de validation avec une interface ValidatorInterface
  3. Utilisez un trait SoftDeletable dans un modèle
  4. Écrivez une hiérarchie de classes avec covariance
  5. 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