MidТеория4 min

ООП для Symfony

Классы, интерфейсы, абстрактные классы, трейты — всё что спрашивают на экзамене

Symfony построен на принципах объектно-ориентированного программирования. На экзамене проверяют глубокое понимание ООП в PHP, включая тонкие различия между абстрактными классами и интерфейсами, правила наследования и использование трейтов.

Классы и объекты

Базовое объявление класса

<?php

declare(strict_types=1);

namespace App\Entity;

final class Product
{
    public function __construct(
        private readonly string $name,
        private readonly float $price,
        private string $description = '',
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function withDescription(string $description): self
    {
        $clone = clone $this;
        $clone->description = $description;

        return $clone;
    }
}

Ключевые модификаторы классов

Модификатор Описание Symfony-контекст
final Запрет наследования Рекомендуется Symfony для сервисов
abstract Нельзя создать экземпляр Базовые классы (AbstractController)
readonly Все свойства readonly (PHP 8.2+) DTO, Value Objects
<?php

declare(strict_types=1);

// Readonly class — PHP 8.2+
readonly class OrderDTO
{
    public function __construct(
        public string $customerName,
        public float $total,
        public \DateTimeImmutable $createdAt,
    ) {
    }
}

Symfony 8.0: Symfony 8.0 требует PHP 8.2+ и активно использует readonly классы для DTO. В Symfony 7.4 поддержка PHP 8.1+ — readonly классы доступны, но не обязательны.

Интерфейсы

Интерфейсы определяют контракт — набор методов без реализации. Symfony использует интерфейсы повсюду: EventSubscriberInterface, FormTypeInterface, VoterInterface.

<?php

declare(strict_types=1);

namespace App\Contract;

interface PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): PaymentResult;

    public function refund(string $transactionId): bool;

    public function getBalance(): float;
}

Интерфейсы с константами

<?php

declare(strict_types=1);

interface StatusInterface
{
    // Constants in interfaces — allowed since PHP 8.1 with enum backing
    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    public function getStatus(): string;
}

Множественная реализация

<?php

declare(strict_types=1);

// A class can implement MULTIPLE interfaces
final class StripeGateway implements
    PaymentGatewayInterface,
    LoggerAwareInterface,
    \Stringable
{
    public function charge(float $amount, string $currency): PaymentResult
    {
        // Implementation
    }

    public function refund(string $transactionId): bool
    {
        // Implementation
    }

    public function getBalance(): float
    {
        // Implementation
    }

    public function __toString(): string
    {
        return 'Stripe Payment Gateway';
    }
}

Абстрактные классы

Абстрактный класс может содержать как абстрактные методы (без реализации), так и конкретные методы с реализацией. Ключевое отличие от интерфейса.

<?php

declare(strict_types=1);

namespace App\Service;

abstract class AbstractNotifier
{
    // Concrete method — shared implementation
    public function notify(string $recipient, string $message): void
    {
        $formatted = $this->formatMessage($message);
        $this->send($recipient, $formatted);
        $this->log($recipient, $formatted);
    }

    // Abstract method — MUST be implemented by child
    abstract protected function send(string $recipient, string $message): void;

    // Concrete method with default behavior
    protected function formatMessage(string $message): string
    {
        return trim($message);
    }

    private function log(string $recipient, string $message): void
    {
        // Logging logic
    }
}

final class EmailNotifier extends AbstractNotifier
{
    protected function send(string $recipient, string $message): void
    {
        // Send email
    }
}

final class SmsNotifier extends AbstractNotifier
{
    protected function send(string $recipient, string $message): void
    {
        // Send SMS
    }

    // Override default formatting
    protected function formatMessage(string $message): string
    {
        return mb_substr($message, 0, 160);
    }
}

Подвох экзамена: На экзамене часто спрашивают разницу между abstract class и interface. Запомните таблицу ниже — это одна из самых частых тем.

Abstract class vs Interface — полное сравнение

Критерий Interface Abstract class
Реализация методов Нет (до PHP 8.0) Да
Свойства Нет (только константы) Да
Конструктор Нет Да
Множественное наследование Да (implements) Нет (extends)
Модификаторы доступа Только public Любые
Тип-хинтинг Да Да
final методы Нет Да
Состояние (state) Нет Да

Подвох экзамена: С PHP 8.0 интерфейсы НЕ могут содержать реализацию методов по умолчанию (в отличие от Java). Если на экзамене предложат вариант с default method в интерфейсе — это неправильный ответ.

Когда использовать что

<?php

declare(strict_types=1);

// USE INTERFACE when:
// - Defining a contract (what object CAN DO)
// - Multiple unrelated classes share the same contract
// - You need multiple implementation

interface CacheInterface     // Symfony: PSR-6/16 caching
{
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl = 0): void;
}

// USE ABSTRACT CLASS when:
// - Sharing code between related classes
// - You need constructor / state / protected methods
// - Template Method pattern

abstract class AbstractCommand  // Symfony: console commands base
{
    private string $name;

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

    abstract protected function execute(): int;
}

Трейты

Трейты — механизм повторного использования кода в PHP. Symfony использует трейты для сервисов, которые нуждаются в общей функциональности без наследования.

<?php

declare(strict_types=1);

namespace App\Trait;

trait TimestampableTrait
{
    private \DateTimeImmutable $createdAt;
    private ?\DateTimeImmutable $updatedAt = null;

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

    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function markAsUpdated(): void
    {
        $this->updatedAt = new \DateTimeImmutable();
    }

    private function initTimestamps(): void
    {
        $this->createdAt = new \DateTimeImmutable();
    }
}

trait SoftDeletableTrait
{
    private ?\DateTimeImmutable $deletedAt = null;

    public function softDelete(): void
    {
        $this->deletedAt = new \DateTimeImmutable();
    }

    public function isDeleted(): bool
    {
        return $this->deletedAt !== null;
    }

    public function restore(): void
    {
        $this->deletedAt = null;
    }
}

// Using multiple traits
final class Article
{
    use TimestampableTrait;
    use SoftDeletableTrait;

    public function __construct(
        private string $title,
        private string $content,
    ) {
        $this->initTimestamps();
    }
}

Конфликт трейтов

<?php

declare(strict_types=1);

trait A
{
    public function hello(): string
    {
        return 'Hello from A';
    }
}

trait B
{
    public function hello(): string
    {
        return 'Hello from B';
    }
}

class MyClass
{
    use A, B {
        A::hello insteadof B;  // Use A's version
        B::hello as helloB;     // Alias B's version
    }
}

Подвох экзамена: Конфликт трейтов без insteadof — это fatal error. На экзамене могут показать код с двумя трейтами, имеющими одинаковый метод, без разрешения конфликта и спросить результат. Ответ: Fatal error.

Перечисление приоритета при конфликтах

При конфликте методов в PHP действует строгий порядок приоритетов:

  1. Текущий класс — методы класса всегда побеждают
  2. Трейт — методы трейта переопределяют унаследованные
  3. Родительский класс — методы родителя имеют наименьший приоритет
<?php

declare(strict_types=1);

class Base
{
    public function method(): string
    {
        return 'Base';
    }
}

trait MyTrait
{
    public function method(): string
    {
        return 'Trait';
    }
}

class Child extends Base
{
    use MyTrait;

    // Without own method: returns 'Trait' (trait > parent)
    // With own method: returns own (class > trait > parent)
}

$child = new Child();
echo $child->method(); // 'Trait'

Ковариантность и контравариантность (PHP 7.4+)

Важная тема для Symfony — правила замещения типов при наследовании.

<?php

declare(strict_types=1);

// Covariance — return type can be MORE specific in child
interface AnimalShelter
{
    public function adopt(): Animal;
}

class CatShelter implements AnimalShelter
{
    public function adopt(): Cat  // Cat extends Animal — VALID
    {
        return new Cat();
    }
}

// Contravariance — parameter type can be LESS specific in child
interface EventHandler
{
    public function handle(ClickEvent $event): void;
}

class WideHandler implements EventHandler
{
    public function handle(Event $event): void  // Event is parent of ClickEvent — VALID
    {
    }
}

Подвох экзамена: Ковариантность работает для возвращаемых типов (можно сужать), контравариантность — для параметров (можно расширять). Если перепутать направление — код не скомпилируется. На экзамене проверяют именно понимание направления.