Выбирай подходящий инструмент проектирования - контракт, скелет алгоритма или горизонтальное переиспользование - понимая компромиссы каждого.
Открыть этот урок в KodokonPHP даёт тебе четыре инструмента проектирования, которые частично перекрываются: конкретное наследование, абстрактные классы, интерфейсы и трейты. Выбор сводится к одному вопросу: что именно ты хочешь разделить? Контракт: интерфейс. Контракт плюс скелет реализации: абстрактный класс. Код, общий для классов без иерархической связи: трейт. А конкретное наследование оправдать всё сложнее: оно жёстко связывает, и родитель в PHP допускается только один.
<?php
declare(strict_types=1);
abstract class Exporter
{
public function export(array $rows): string
{
return $this->head() . $this->body($rows);
}
abstract protected function head(): string;
abstract protected function body(
array $rows
): string;
}
final class CsvExporter extends Exporter
{
protected function head(): string
{
return "id;name\n";
}
protected function body(array $rows): string
{
$lines = array_map(
fn (array $r): string => implode(';', $r),
$rows,
);
return implode("\n", $lines);
}
}
$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);Интерфейс - это чистый контракт: сигнатуры и константы, без состояния и без реализации. Его решающее преимущество перед абстрактным классом: класс может реализовать несколько интерфейсов, и любой уже существующий класс может принять контракт задним числом. Абстрактный класс, наоборот, несёт состояние и общий код, но забирает твой единственный слот наследования. На практике: типизируй параметры интерфейсами (Cache, а не ArrayCache), чтобы менять реализации - как в тестах, так и в продакшене.
<?php
declare(strict_types=1);
interface Cache
{
public function get(string $key): ?string;
public function set(
string $key,
string $value
): void;
}
final class ArrayCache implements Cache
{
private array $items = [];
public function get(string $key): ?string
{
return $this->items[$key] ?? null;
}
public function set(
string $key,
string $value
): void {
$this->items[$key] = $value;
}
}
$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');Трейты решают горизонтальное переиспользование: они внедряют одни и те же методы в классы без общего предка. Когда два трейта дают метод с одинаковым именем, PHP отказывается выбирать за тебя и требует явного разрешения: insteadof указывает, какую версию оставить, as создаёт псевдоним, чтобы вторая осталась доступной. Многословность здесь намеренная - конфликт должен оставаться видимым в коде.
<?php
declare(strict_types=1);
trait FileLogger
{
public function log(string $message): void
{
echo "[file] $message\n";
}
}
trait AuditLogger
{
public function log(string $message): void
{
echo "[audit] $message\n";
}
}
final class PaymentService
{
use FileLogger, AuditLogger {
FileLogger::log insteadof AuditLogger;
AuditLogger::log as audit;
}
}
$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');log(). Что произойдёт?insteadof