Абстрактные классы в 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 позволяет наследникам изменять константу.