Урок 03

Конструкторы и деструкторы

Конструктор

__construct() вызывается автоматически при new.

class User
{
    public function __construct(
        public string $name,
        public int $age,
    ) {
        echo "Создан $name\n";
    }
}

$user = new User('Иван', 25);   // Создан Иван

Задача: инициализация объекта.

Старый синтаксис

До PHP 8.0:

class User
{
    public string $name;
    public int $age;

    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age = $age;
    }
}

С 8.0 — promotion:

class User
{
    public function __construct(
        public string $name,
        public int $age,
    ) {}
}

Значения по умолчанию

class Config
{
    public function __construct(
        public string $env = 'production',
        public bool $debug = false,
        public array $options = [],
    ) {}
}

$c1 = new Config();                      // все дефолты
$c2 = new Config('local', true);         // частично
$c3 = new Config(env: 'dev', debug: true); // named args

Валидация в конструкторе

Хорошая практика — защита инвариантов:

final class Email
{
    public function __construct(
        public readonly string $value,
    ) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Некорректный email: $value");
        }
    }
}

new Email('not-an-email');   // InvalidArgumentException

Value Object не может быть создан с некорректными данными.

Приватный конструктор

Запрещает new снаружи:

class Singleton
{
    private static ?self $instance = null;

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }
}

$s1 = Singleton::getInstance();
$s2 = Singleton::getInstance();

var_dump($s1 === $s2);   // true

⚠️ Singleton — антипаттерн. Используй DI.

Фабричные конструкторы

Именованные конструкторы:

final readonly class Duration
{
    private function __construct(
        public int $seconds,
    ) {}

    public static function fromSeconds(int $seconds): self
    {
        return new self($seconds);
    }

    public static function fromMinutes(int $minutes): self
    {
        return new self($minutes * 60);
    }

    public static function fromHours(int $hours): self
    {
        return new self($hours * 3600);
    }

    public function toMinutes(): int
    {
        return (int) ($this->seconds / 60);
    }
}

$d = Duration::fromMinutes(5);
echo $d->seconds;   // 300

Паттерн — несколько понятных способов создания.

self vs static

self — текущий класс:

class Foo
{
    public function __construct()
    {
        // всегда Foo
    }
}

static — вызывающий класс:

class Base
{
    public static function create(): static
    {
        return new static();
    }
}

class Child extends Base {}

$c = Child::create();   // Child

Наследование конструктора

Если у родителя есть конструктор — вызывать parent::__construct():

class BaseEntity
{
    public function __construct(
        public readonly int $id,
    ) {}
}

class User extends BaseEntity
{
    public function __construct(
        int $id,
        public readonly string $email,
    ) {
        parent::__construct($id);
    }
}

Если не вызвать — родительский не сработает.

Деструктор

__destruct() вызывается при уничтожении объекта.

class File
{
    private $handle;

    public function __construct(string $path)
    {
        $this->handle = fopen($path, 'r');
    }

    public function __destruct()
    {
        if ($this->handle) {
            fclose($this->handle);
        }
    }
}

Когда вызывается:

  • Объект больше не используется — сборщик мусора.
  • Скрипт завершается.
  • unset($obj).

Порядок вызова

Создание:

  1. Родитель __construct.
  2. Ребёнок __construct.

Уничтожение:

  1. Ребёнок __destruct.
  2. Родитель __destruct.

Проблемы с деструктором

⚠️ Не полагайся на порядок — PHP вызывает деструкторы по-разному.

⚠️ Исключения в деструкторе — плохо, могут сломать скрипт.

Рекомендация: не используй деструкторы для важной логики. Только для освобождения ресурсов.

Практика: соединение с БД

final class DatabaseConnection
{
    private \PDO $pdo;

    public function __construct(
        string $host,
        string $database,
        string $user,
        string $password,
    ) {
        $dsn = "pgsql:host=$host;dbname=$database";

        $this->pdo = new \PDO($dsn, $user, $password, [
            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
        ]);
    }

    public function query(string $sql, array $params = []): array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    public function close(): void
    {
        $this->pdo = null;
    }
}

Обычно закрывать явно не нужно — PHP сделает это сам.

Промоушен + валидация

final class Product
{
    public function __construct(
        public readonly string $name,
        public readonly int $price,
    ) {
        if ($price < 0) {
            throw new InvalidArgumentException('Цена не может быть отрицательной');
        }

        if (empty($name)) {
            throw new InvalidArgumentException('Название обязательно');
        }
    }
}

Несколько конструкторов

В PHP нет перегрузки конструктора. Используй статические методы:

final class Money
{
    private function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    public static function rub(int $amount): self
    {
        return new self($amount, 'RUB');
    }

    public static function usd(int $amount): self
    {
        return new self($amount, 'USD');
    }

    public static function zero(string $currency = 'RUB'): self
    {
        return new self(0, $currency);
    }
}

$price = Money::rub(100000);

Практика: заказ

<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
}

final class Order
{
    private array $items = [];

    public function __construct(
        public readonly string $id,
        public readonly string $customerId,
        private OrderStatus $status = OrderStatus::Pending,
        private int $totalAmount = 0,
    ) {
        if ($totalAmount < 0) {
            throw new InvalidArgumentException('Сумма не может быть отрицательной');
        }
    }

    public static function create(string $customerId): self
    {
        return new self(
            id: bin2hex(random_bytes(8)),
            customerId: $customerId,
        );
    }

    public function addItem(string $productId, int $price, int $qty = 1): void
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new RuntimeException('Нельзя изменить оплаченный заказ');
        }

        $this->items[] = compact('productId', 'price', 'qty');
        $this->totalAmount += $price * $qty;
    }

    public function getTotalAmount(): int
    {
        return $this->totalAmount;
    }

    public function pay(): void
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new RuntimeException('Заказ нельзя оплатить');
        }

        if (empty($this->items)) {
            throw new RuntimeException('Заказ пустой');
        }

        $this->status = OrderStatus::Paid;
    }

    public function getStatus(): OrderStatus
    {
        return $this->status;
    }
}

$order = Order::create('user-1');
$order->addItem('iphone', 100000, 2);
$order->pay();

echo $order->getStatus()->value;   // paid
echo $order->getTotalAmount();     // 200000

Итог

  • __construct — при создании.
  • Promotion — короче.
  • Валидация в конструкторе — защита инвариантов.
  • Приватный конструктор — для Singleton/Factory.
  • Статические фабрики — несколько способов создания.
  • parent::__construct() — для наследников.
  • __destruct — редко, только для ресурсов.
  • Порядок: родитель → ребёнок при создании, наоборот при удалении.

Дальше — видимость: public, protected, private.