Интерфейс PHP: объявление, реализация и практическое применение

Интерфейс PHP задает контракт для классов: перечисляет доступные методы и их сигнатуры, но не содержит их рабочий код. Класс подключает контракт через implements и пишет свою реализацию. Такой подход позволяет передавать в функцию разные объекты одного типа и менять зависимость без переписывания потребителя.

<?php
interface LoggerInterface
{
    public function log(string $message): void;
}
final class ConsoleLogger implements LoggerInterface
{
    public function log(string $message): void
    {
        echo $message . PHP_EOL;
    }
}
$logger = new ConsoleLogger();
$logger->log('Запуск приложения');

В этом примере LoggerInterface требует метод log, а ConsoleLogger выводит сообщение. Сохраните пример в файл example.php. В терминале из каталога файла выполните php example.php: программа напечатает «Запуск приложения».

Запускайте листинги по отдельности: некоторые имена в них повторяются.

Что такое интерфейс в PHP

Интерфейс в PHP – это контракт без реализации. Он описывает публичные возможности объекта, но оставляет алгоритм самому классу.

Конкретный класс обязан предоставить реализацию каждого метода интерфейса. Он может написать методы самостоятельно или получить их от родительского класса.

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

<?php
interface Printable
{
    public function print(): string;
}
final class Invoice implements Printable
{
    public function print(): string
    {
        return 'Счет подготовлен';
    }
}
$document = new Invoice();
echo $document->print();

Метод интерфейса сообщает потребителю о возможности объекта. В примере Printable обещает метод, который возвращает строку. Класс Invoice выполняет это обещание и возвращает «Счет подготовлен».

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

Как объявить интерфейс

Для объявления интерфейса используйте ключевое слово interface. Методы интерфейса объявляют без тела и с модификатором public. Подробные правила описаны в руководстве PHP об интерфейсах.

<?php
interface UserRepository
{
    public function findById(int $id): ?string;
    public function save(string $name): bool;
}

Типы аргументов и возвращаемых значений фиксируют границу между потребителем и реализацией. Метод findById получает целое число и возвращает строку либо null. Метод save получает строку и возвращает логическое значение.

Ставьте точку с запятой после сигнатуры метода.

Запись public function save(): bool {} недопустима внутри интерфейса: даже пустые фигурные скобки задают тело.

Константы интерфейса

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

<?php
interface ResponseStatus
{
    public const OK = 200;
    public const ERROR = 500;
}

final class ApiResponse
{
    public function status(): int
    {
        return ResponseStatus::OK;
    }
}

echo (new ApiResponse())->status();

Программа выведет 200. Для обращения к константе объект интерфейса не нужен.

Как назвать интерфейс

Суффикс Interface помогает отличать контракты от реализаций: LoggerInterface, CacheInterface. Например, PSR-3 описывает контракт LoggerInterface для журналирования.

PSR-4 регулирует автозагрузку и не устанавливает правило такого именования.

Имя Logger тоже допустимо, если соглашения проекта требуют коротких имен. Выбирайте один стиль для всего проекта: смешанные варианты усложняют поиск контрактов.

Как реализовать интерфейс в классе

Класс подключает интерфейс после имени через implements. Реализация каждого метода должна сохранять совместимую сигнатуру и публичный доступ.

<?php
interface HasName
{
    public function name(): string;
}

interface HasId
{
    public function id(): int;
}

final class Product implements HasName, HasId
{
    public function name(): string
    {
        return 'Клавиатура';
    }

    public function id(): int
    {
        return 7;
    }
}

$product = new Product();
echo $product->name() . ': ' . $product->id();

Запятая отделяет несколько интерфейсов. Класс Product реализует 2 контракта и выводит «Клавиатура: 7».

Если класс также наследует родителя, сначала укажите extends, затем implements.

Совместимость сигнатур

Реализация сохраняет имя метода и совместимые типы. Возвращаемый тип может стать более конкретным, это называют ковариантностью. Тип параметра может стать шире, это называют контравариантностью.

<?php
final class Report
{
    public function title(): string
    {
        return 'Отчет';
    }
}

interface Factory
{
    public function create(): object;
}

final class ReportFactory implements Factory
{
    public function create(): Report
    {
        return new Report();
    }
}

interface ReportPrinter
{
    public function print(Report $report): string;
}

final class UniversalPrinter implements ReportPrinter
{
    public function print(object $report): string
    {
        return get_class($report);
    }
}

echo (new ReportFactory())->create()->title() . PHP_EOL;
echo (new UniversalPrinter())->print(new Report());

Метод ReportFactory::create возвращает Report, который конкретнее типа object. Метод UniversalPrinter::print принимает любой объект, поэтому принимает и Report. Обе реализации сохраняют обещания своих интерфейсов.

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

Сохраняйте также имена параметров: вызывающий код может использовать именованные аргументы.

Если класс пропустил обязательный метод

Объявление class MissingLogger implements LoggerInterface {} не выполняет контракт из первого примера. PHP остановит программу с сообщением:

Fatal error: Class MissingLogger contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (LoggerInterface::log)

Добавьте публичный метод log(string $message): void, как в ConsoleLogger.

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

Интерфейс как тип: полиморфизм на примере

Тип интерфейса принимает любую его реализацию. Благодаря этому одна функция работает с несколькими способами оплаты.

<?php
interface PaymentGateway
{
    public function pay(int $amount): string;
}

final class CardGateway implements PaymentGateway
{
    public function pay(int $amount): string
    {
        return "Карта: {$amount} ₽";
    }
}

final class CashGateway implements PaymentGateway
{
    public function pay(int $amount): string
    {
        return "Наличные: {$amount} ₽";
    }
}

function checkout(PaymentGateway $gateway, int $amount): string
{
    return $gateway->pay($amount);
}

function createGateway(bool $cash): PaymentGateway
{
    return $cash ? new CashGateway() : new CardGateway();
}

$gateway = createGateway(false);
echo checkout($gateway, 1200) . PHP_EOL;
echo checkout(createGateway(true), 1200) . PHP_EOL;
var_dump($gateway instanceof PaymentGateway);

Программа выведет «Карта: 1200 ₽», затем «Наличные: 1200 ₽» и bool(true).

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

Функция checkout зависит от контракта PaymentGateway. Она не проверяет конкретный класс и не содержит условие для каждого способа оплаты. Функция createGateway использует интерфейс как возвращаемый тип и выбирает реализацию.

Оператор instanceof проверяет принадлежность объекта интерфейсу. Он возвращает true для объекта любого класса, который реализует контракт.

Как интерфейс связан с внедрением зависимостей

При внедрении зависимостей потребитель получает готовую реализацию извне. В конструкторе укажите интерфейс, чтобы потребитель не зависел от одного класса.

<?php
interface Notifier
{
    public function send(string $message): void;
}

final class ConsoleNotifier implements Notifier
{
    public function send(string $message): void
    {
        echo $message;
    }
}

final class OrderService
{
    public function __construct(private Notifier $notifier)
    {
    }

    public function create(): void
    {
        $this->notifier->send('Заказ создан');
    }
}

(new OrderService(new ConsoleNotifier()))->create();

Конструктор сохраняет переданный объект в свойстве $notifier. Метод create вызывает его и выводит «Заказ создан». Для другого канала уведомлений передайте другой класс с интерфейсом Notifier; код OrderService останется прежним.

Наследование интерфейсов

Интерфейс наследуется от другого интерфейса через extends. Дочерний интерфейс получает все методы родителей и может добавить собственные требования.

<?php
interface Readable
{
    public function read(): string;
}

interface Writable
{
    public function write(string $value): void;
}

interface Storage extends Readable, Writable
{
    public function clear(): void;
}

final class MemoryStorage implements Storage
{
    private string $value = '';

    public function read(): string
    {
        return $this->value;
    }

    public function write(string $value): void
    {
        $this->value = $value;
    }

    public function clear(): void
    {
        $this->value = '';
    }
}

$storage = new MemoryStorage();
$storage->write('Данные');
echo $storage->read() . PHP_EOL;
$storage->clear();
var_dump($storage->read());

Интерфейс Storage объединяет 2 родительских контракта и добавляет очистку. Класс MemoryStorage реализует все 3 метода. После очистки программа покажет пустую строку: string(0) "".

Разделение полезно потребителям с разными правами. Функция с параметром Readable требует только чтение; функция с параметром Storage получает полный контракт.

Термины различаются: класс реализует интерфейс, а интерфейс наследует другие интерфейсы.

Интерфейс или абстрактный класс

Выбирайте интерфейс для общего контракта разных объектов. Берите абстрактный класс, когда классам нужна общая реализация, состояние или защищенные служебные методы.

КритерийИнтерфейсАбстрактный класс
Реализация методовОбъявляет методы без телаМожет содержать обычные и абстрактные методы
Свойства и состояниеНе хранит состояние; с PHP 8.4 может требовать публичные свойства через контракт доступаОбъявляет свойства и хранит состояние объектов
Модификаторы методовМетоды контракта публичныеОбычные методы поддерживают public, protected и private; абстрактные – public и protected
МножественностьКласс может реализовать несколько интерфейсовКласс наследует только 1 класс
КонструкторМожет объявить сигнатуру публичного конструктора, но не его реализациюМожет иметь конструктор для общей инициализации
КонстантыМожет объявлять константыМожет объявлять константы

Абстрактный класс может не содержать ни одного абстрактного метода.

Ключевое слово abstract само запрещает создавать его экземпляры напрямую.

<?php
abstract class BaseReport
{
    public function heading(): string
    {
        return 'Отчет о продажах';
    }
}

final class SalesReport extends BaseReport
{
    public function render(): string
    {
        return $this->heading() . ': готов';
    }
}

echo (new SalesReport())->render();

Класс BaseReport содержит только готовый метод. Наследник использует его и выводит «Отчет о продажах: готов».

Как выбрать подход под задачу

  1. Определите операции, которые вызывает потребитель. Для заменяемых реализаций, как в примере оплаты, объявите интерфейс.
  2. Выделите общий код и состояние. Если наследникам нужна единая основа, используйте абстрактный класс.
  3. Учтите существующее наследование. Если класс уже наследует другую основу, добавьте интерфейс, а повторяющиеся методы вынесите в трейт.

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

Интерфейс вместе с трейтом

Трейт добавляет готовые методы без наследования общего класса. Интерфейс при этом остается типом, который принимает потребитель.

<?php
interface Formatter
{
    public function format(string $value): string;
}

trait AddsPrefix
{
    protected function prefix(string $value): string
    {
        return '[log] ' . $value;
    }
}

final class TextFormatter implements Formatter
{
    use AddsPrefix;

    public function format(string $value): string
    {
        return $this->prefix($value);
    }
}

echo (new TextFormatter())->format('Готово');

Связка «интерфейс плюс трейт» дает общий контракт и повторное поведение. В примере трейт добавляет префикс, а класс возвращает «[log] Готово».

Сам трейт не заменяет интерфейс как тип параметра.

Встроенные интерфейсы PHP

PHP предоставляет интерфейсы для стандартных операций с объектами. Их описание собрано в разделах предопределенных интерфейсов PHP и интерфейсов SPL.

  • Countable – позволяет передать объект в count().
  • IteratorAggregate – возвращает итератор для обхода объекта через foreach.
  • Iterator – задает методы управления текущим элементом и позицией итератора.
  • ArrayAccess – задает доступ к элементам объекта через квадратные скобки.
  • JsonSerializable – задает значение для json_encode().
  • Stringable – описывает объект с методом __toString() для строкового представления.
  • Throwable – объединяет исключения и ошибки для обработки через catch.
<?php
final class Cart implements Countable, JsonSerializable
{
    public function __construct(private array $items)
    {
    }

    public function count(): int
    {
        return count($this->items);
    }

    public function jsonSerialize(): array
    {
        return $this->items;
    }
}

$cart = new Cart(['book', 'pen']);
echo count($cart) . PHP_EOL;
echo json_encode($cart);

Класс Cart поддерживает 2 стандартные операции. Программа выведет 2, затем ["book","pen"]. Функции используют методы интерфейсов, поэтому отдельные преобразования объекта не нужны.

Класс не может напрямую реализовать Throwable.

Для собственного исключения наследуйте Exception или его потомка. Такой объект уже поддерживает контракт Throwable.

Что нового в интерфейсах в PHP 8.x

Ветки PHP 8.x расширили возможности контрактов. У конструкций различаются минимальные версии: обычные примеры статьи не требуют свойств интерфейсов или типизированных констант.

  • PHP 8.0 автоматически добавляет классу интерфейс Stringable, если класс объявляет метод __toString().
  • PHP 8.1 разрешает классу переопределять константу реализуемого интерфейса, если контракт не запрещает переопределение.
  • PHP 8.1 поддерживает final-константы интерфейса, которые нельзя переопределить.
  • PHP 8.1 позволяет enum реализовать интерфейс.
  • PHP 8.3 поддерживает типизированные константы классов и интерфейсов, например public const int OK = 200;.
  • PHP 8.4 позволяет интерфейсу требовать публичное свойство через объявление с хуками, например public string $name { get; }.

Enum с собственным интерфейсом

Перечисление реализует контракт через implements, как обычный класс. Этот пример требует PHP 8.1 или новее.

<?php
interface HasLabel
{
    public function label(): string;
}

enum PaymentStatus implements HasLabel
{
    case Paid;
    case Failed;

    public function label(): string
    {
        return match ($this) {
            self::Paid => 'Оплачено',
            self::Failed => 'Ошибка оплаты',
        };
    }
}

echo PaymentStatus::Paid->label();

Перечисление выведет «Оплачено». Enum не может объявить __toString(), поэтому для строковой подписи используйте собственный метод, как label().

Свойство в интерфейсе через property hooks

В PHP 8.4 интерфейс может потребовать чтение или запись свойства. Контракт get требует доступного чтения, но не обязывает класс писать собственный хук.

<?php
interface Named
{
    public string $name { get; }
}

final class User implements Named
{
    public function __construct(public string $name)
    {
    }
}

echo (new User('Анна'))->name;

Пример требует PHP 8.4 или новее и выводит «Анна». Обычное публичное свойство класса удовлетворяет требованию чтения.

Интерфейс не хранит значение: состояние остается в объекте User.

Подробности синтаксиса приведены в руководстве об интерфейсах, а правила перечислений – в разделе о методах enum.

Ошибки при объявлении и реализации интерфейсов

Ошибки интерфейсов обычно указывают на конкретное нарушение контракта. Сообщения приведены без пути файла, номера строки и стека вызовов. В объявлениях с ошибкой имена совпадают с именами в сообщениях.

Тело метода в интерфейсе

Объявление public function run(): string { return 'Готово'; } внутри интерфейса Worker вызывает ошибку:

Fatal error: Interface function Worker::run() cannot contain body

Уберите тело из интерфейса и перенесите код в класс. Исправленный пример выведет «Задача выполнена».

<?php
interface Worker
{
    public function run(): string;
}

final class Job implements Worker
{
    public function run(): string
    {
        return 'Задача выполнена';
    }
}

echo (new Job())->run();

Не-public метод

Объявление protected function run(): string; внутри интерфейса Worker вызывает сообщение:

Fatal error: Access type for interface method Worker::run() must be public

Замените protected на public. В классе-реализации метод тоже должен быть публичным, иначе потребитель не получит обещанный доступ.

Несовместимая сигнатура

Пусть интерфейс Worker требует run(int $value): string. Если класс Job объявит run(string $value): string, PHP сообщит:

Fatal error: Declaration of Job::run(string $value): string must be compatible with Worker::run(int $value): string

Замените тип параметра реализации на int. Следующий пример принимает число и выводит «Задача 7».

<?php
interface Worker
{
    public function run(int $value): string;
}

final class Job implements Worker
{
    public function run(int $value): string
    {
        return 'Задача ' . $value;
    }
}

echo (new Job())->run(7);

Не реализован обязательный метод

Если интерфейс Worker объявляет run(): string, пустой класс class Job implements Worker {} вызывает ошибку:

Fatal error: Class Job contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Worker::run)

Добавьте публичный метод run(): string с реализацией. Для запускаемой задачи подойдет класс Job из примера исправления тела метода. Объявление abstract class Job implements Worker {} тоже допустимо, но объект такого класса создать нельзя.

Попытка создать объект интерфейса

Выражение new Worker() при объявленном интерфейсе Worker вызывает сообщение:

Fatal error: Uncaught Error: Cannot instantiate interface Worker

Создайте объект конкретной реализации: $worker = new Job();. Переменную затем можно передать в функцию с параметром типа Worker.

Свойство в интерфейсе до PHP 8.4

В PHP 8.0–8.3 объявление public string $name; внутри интерфейса вызывает ошибку:

Fatal error: Interfaces may not include properties

Для этих версий объявите метод чтения значения, а состояние храните в классе. Пример выведет «Анна».

<?php
interface Named
{
    public function name(): string;
}

final class User implements Named
{
    public function __construct(private string $value)
    {
    }

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

echo (new User('Анна'))->name();

В PHP 8.4 контракт свойства записывают с требуемыми хуками: public string $name { get; }. Обычное объявление поля без хуков внутри интерфейса по-прежнему недопустимо.

Начинайте с небольшого интерфейса из операций, которые используют потребители. Если два способа оплаты уже подходят одной функции checkout, контракт решает задачу. Добавляйте новые методы, когда вызывающему коду действительно нужны новые операции.

Комментарии

Оставьте свой комментарий
R
Roman

Синтаксис из статьи значительно устарел.

Например метод в интерфейсе указывается вот так:
public function motion();
А не вот так:
public function motion(){}