Абстрактные классы в PHP - синтаксис, возможности и паттерны

Раздел: ООП в PHP -> Абстрактные классы

Основы абстрактных классов в PHP

Задача: определить общий интерфейс и частичную реализацию для группы связанных классов, запретив создание экземпляров базового класса.

Абстрактный класс (abstract class) объявляется с ключевым словом abstract. Он может содержать как обычные методы с реализацией, так и абстрактные методы (без тела), которые обязаны быть переопределены в классах-наследниках. Экземпляр абстрактного класса создать нельзя - это гарантирует, что будет использоваться только конкретная реализация.


abstract class Shape {
    protected $color;

    public function setColor(string $color) {
        $this->color = $color;
    }

    abstract public function getArea(): float;
}

class Circle extends Shape {
    private $radius;

    public function __construct(float $radius) {
        $this->radius = $radius;
    }

    public function getArea(): float {
        return pi() * $this->radius ** 2;
    }
}

$circle = new Circle(5);
$circle->setColor('red');
// echo $circle->getArea(); // 78.5398...

Php abstract class (абстрактные классы в php)

Здесь Shape определяет общее свойство $color, метод setColor() и абстрактный метод getArea(). Класс Circle обязан реализовать getArea(), иначе PHP выдаст фатальную ошибку.

Как добавить общую функциональность вместе с абстрактным контрактом?

В абстрактный класс можно включить неабстрактные методы - они наследуются как есть. Это позволяет избежать дублирования кода в наследниках.


abstract class Logger {
    abstract protected function write(string $message): void;

    public function logInfo(string $message): void {
        $this->write('[INFO] ' . $message);
    }

    public function logError(string $message): void {
        $this->write('[ERROR] ' . $message);
    }
}

class FileLogger extends Logger {
    private $fileHandle;

    public function __construct(string $filename) {
        $this->fileHandle = fopen($filename, 'a');
    }

    protected function write(string $message): void {
        fwrite($this->fileHandle, $message . PHP_EOL);
    }

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

Типичная ошибка: забыть реализовать все абстрактные методы. PHP выдаст: Fatal error: Class FileLogger contains 1 abstract method and must be declared abstract or implement the remaining methods (Logger::write).
Решение: проверить, что все абстрактные методы базового класса переопределены в наследнике.

Как комбинировать абстрактный класс с интерфейсом?

Абстрактный класс может реализовать интерфейс, при этом часть методов может быть абстрактными, а часть - реализованными. Наследники обязаны выполнить оставшиеся контракты.


interface Printable {
    public function print(): string;
}

abstract class Document implements Printable {
    protected $title;

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

    abstract public function getContent(): string;

    public function print(): string {
        return $this->title . ': ' . $this->getContent();
    }
}

class PDFDocument extends Document {
    public function getContent(): string {
        return 'PDF binary data...';
    }
}

Ошибка: если класс PDFDocument не реализует getContent(), то он сам должен быть объявлен абстрактным. Если забыть, PHP выбросит ту же фатальную ошибку.

Как использовать абстрактные классы со статическими методами и константами?

Абстрактный класс может содержать статические свойства, методы и константы, доступные наследникам без создания экземпляра.


abstract class Database {
    protected const TABLE_NAME = '';

    abstract public function getRecord(int $id): array;

    public static function getTableName(): string {
        return static::TABLE_NAME;
    }
}

class UserRepository extends Database {
    protected const TABLE_NAME = 'users';

    public function getRecord(int $id): array {
        // реализация
        return ['id' => $id, 'name' => 'John'];
    }
}

echo UserRepository::getTableName(); // users

Распространённая ошибка: попытка вызвать абстрактный статический метод напрямую. Абстрактные методы не могут быть статическими (в PHP 7/8 статический метод не может быть одновременно абстрактным).
Правильно: определять статические методы как неабстрактные, а переопределять их в наследниках.

Как организовать цепочку наследования среди абстрактных классов?

Абстрактный класс может наследовать другой абстрактный класс, не реализуя все абстрактные методы - часть можно оставить абстрактной для следующего уровня.


abstract class Animal {
    abstract public function makeSound(): string;
}

abstract class Mammal extends Animal {
    abstract public function getNumberOfLegs(): int;
}

class Dog extends Mammal {
    public function makeSound(): string {
        return 'Bark';
    }

    public function getNumberOfLegs(): int {
        return 4;
    }
}

Забыть переопределить хотя бы один абстрактный метод на любом уровне - класс Dog станет абстрактным. Решение: реализовать все абстрактные методы в конечном классе.

Как реализовать шаблонный метод (Template Method) через абстрактные классы?

Абстрактный класс определяет скелет алгоритма (неабстрактный метод), а конкретные шаги реализуются в наследниках.


abstract class Task {
    public function run(): void {
        $this->init();
        $this->execute();
        $this->cleanup();
    }

    abstract protected function init(): void;
    abstract protected function execute(): void;
    abstract protected function cleanup(): void;
}

class ImportTask extends Task {
    protected function init(): void {
        echo "Opening file...\n";
    }
    protected function execute(): void {
        echo "Importing data...\n";
    }
    protected function cleanup(): void {
        echo "Closing file...\n";
    }
}

$task = new ImportTask();
$task->run();
// Вывод:
// Opening file...
// Importing data...
// Closing file...

Ошибка: если один из шагов объявлен приватным, его нельзя переопределить. Абстрактные методы должны быть protected или public.

Расширенные примеры использования абстрактных классов

Пример 1: Абстрактная фабрика (Abstract Factory)

Создадим семейство связанных объектов без привязки к конкретным классам.

Пример

interface Button {
    public function render(): string;
}

interface Checkbox {
    public function render(): string;
}

abstract class GUIFactory {
    abstract public function createButton(): Button;
    abstract public function createCheckbox(): Checkbox;
}

class WinButton implements Button {
    public function render(): string {
        return 'Windows style button';
    }
}

class WinCheckbox implements Checkbox {
    public function render(): string {
        return 'Windows style checkbox';
    }
}

class WinFactory extends GUIFactory {
    public function createButton(): Button {
        return new WinButton();
    }
    public function createCheckbox(): Checkbox {
        return new WinCheckbox();
    }
}

class MacButton implements Button {
    public function render(): string {
        return 'Mac style button';
    }
}

class MacCheckbox implements Checkbox {
    public function render(): string {
        return 'Mac style checkbox';
    }
}

class MacFactory extends GUIFactory {
    public function createButton(): Button {
        return new MacButton();
    }
    public function createCheckbox(): Checkbox {
        return new MacCheckbox();
    }
}

function renderUI(GUIFactory $factory) {
    echo $factory->createButton()->render() . PHP_EOL;
    echo $factory->createCheckbox()->render() . PHP_EOL;
}

$factory = new WinFactory();
renderUI($factory);
// Вывод:
// Windows style button
// Windows style checkbox

Каждая конкретная фабрика наследует абстрактную фабрику и реализует создание продуктов своего стиля.

Пример 2: Абстрактный класс с трейтами

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

Пример

trait Timestampable {
    private $createdAt;
    private $updatedAt;

    public function setCreatedAt(DateTimeImmutable $date): void {
        $this->createdAt = $date;
    }

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

    public function touch(): void {
        $this->updatedAt = new DateTimeImmutable('now');
    }
}

abstract class Entity {
    use Timestampable;

    protected $id;

    abstract public function getId(): int;
}

class User extends Entity {
    private $name;

    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
        $this->setCreatedAt(new DateTimeImmutable('now'));
    }

    public function getId(): int {
        return $this->id;
    }

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

$user = new User(1, 'Alice');
$user->touch();
echo $user->getName() . ' created at ' . $user->getCreatedAt()->format('Y-m-d H:i:s');
// Пример вывода: Alice created at 2025-03-15 10:00:00

Трейт Timestampable добавляет функциональность временных меток, а абстрактный класс Entity гарантирует наличие метода getId().

Пример 3: Абстрактный класс с финальными методами

Некоторые методы можно запретить к переопределению, используя final.

Пример

abstract class Account {
    protected float $balance;

    public function __construct(float $initialBalance) {
        $this->balance = $initialBalance;
    }

    final public function deposit(float $amount): void {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Amount must be positive');
        }
        $this->balance += $amount;
        $this->afterDeposit($amount);
    }

    abstract protected function afterDeposit(float $amount): void;

    final public function getBalance(): float {
        return $this->balance;
    }
}

class SavingsAccount extends Account {
    protected function afterDeposit(float $amount): void {
        // начисление процентов после пополнения
        $this->balance += $amount * 0.001;
    }
}

$account = new SavingsAccount(1000);
$account->deposit(500);
echo $account->getBalance(); // 1500.5 (1000 + 500 + 0.5)

Метод deposit() помечен final, поэтому его нельзя переопределить в наследниках - это защищает логику пополнения счёта.

Пример 4: Абстрактный класс с варьирующимся числом аргументов в абстрактном методе

Сигнатура абстрактного метода может содержать вариативные параметры и типы, накладывающие ограничения.

Пример

abstract class Filter {
    abstract public function apply(array $data, array $options = []): array;
}

class UpperCaseFilter extends Filter {
    public function apply(array $data, array $options = []): array {
        $keys = $options['keys'] ?? array_keys($data);
        $result = [];
        foreach ($data as $key => $value) {
            $result[$key] = in_array($key, $keys) ? strtoupper($value) : $value;
        }
        return $result;
    }
}

$filter = new UpperCaseFilter();
$result = $filter->apply(['name' => 'john', 'city' => 'london'], ['keys' => ['name']]);
print_r($result);
/*
Array
(
    [name] => JOHN
    [city] => london
)
*/

Абстрактный метод позволяет передавать дополнительные параметры через массив $options, не нарушая контракт.

Пример 5: Абстрактный класс с использованием статического полиморфизма (late static binding)

С помощью static:: в абстрактном классе можно обращаться к переопределённым константам и методам наследников.

Пример

abstract class Model {
    protected static string $table = '';

    public static function getTable(): string {
        return static::$table;
    }

    abstract public static function find(int $id): ?static;
}

class UserModel extends Model {
    protected static string $table = 'users';

    public static function find(int $id): ?static {
        // эмуляция запроса
        return new static();
    }
}

echo UserModel::getTable(); // users
$user = UserModel::find(1);
var_dump($user); // object(UserModel)#...

Важно: static в возвращаемом типе (?static) доступен начиная с PHP 8.0. В более старых версиях придётся использовать self или подсказки PHPDoc.

Пример 6: Абстрактный класс с генериками (через PHPDoc и проверки)

PHP не имеет нативной поддержки дженериков, но можно использовать абстрактные классы с аннотациями и приведением типов.

Пример

/** @template T */
abstract class Collection {
    /** @var array */
    protected array $items = [];

    /** @param T $item */
    abstract public function add($item): void;

    /** @return T|null */
    abstract public function get(int $index);
}

/** @implements Collection */
class IntCollection extends Collection {
    public function add($item): void {
        if (!is_int($item)) {
            throw new InvalidArgumentException('Only integers allowed');
        }
        $this->items[] = $item;
    }

    public function get(int $index): ?int {
        return $this->items[$index] ?? null;
    }
}

$coll = new IntCollection();
$coll->add(10);
$coll->add(20);
echo $coll->get(1); // 20

Аннотации @template и @implements помогают статическим анализаторам (Psalm, PhpStan) проверять типы.

Пример 7: Абстрактный класс с переопределением констант и методов класса

В PHP константы и статические методы наследуются и могут быть переопределены - это удобно для конфигурируемых компонентов.

Пример

abstract class ThresholdValidator {
    protected const THRESHOLD = 0;

    public function validate(float $value): bool {
        return $value >= static::THRESHOLD;
    }

    abstract public function getUnit(): string;
}

class PositiveNumberValidator extends ThresholdValidator {
    protected const THRESHOLD = 1; // минимальное положительное число

    public function getUnit(): string {
        return 'units';
    }
}

class AgeValidator extends ThresholdValidator {
    protected const THRESHOLD = 18; // минимальный возраст

    public function getUnit(): string {
        return 'years';
    }
}

$validator = new AgeValidator();
echo $validator->validate(25) ? 'Valid' : 'Invalid'; // Valid

Использование static::THRESHOLD позволяет наследникам изменять константу.

Абстрактные классы в PHP - comments

En
Php abstract class (php)