W3docs

Объектно-ориентированное программирование с интерфейсами PHP

Интерфейсы — фундаментальная часть ООП в PHP: они обеспечивают контракты между классами и способствуют слабой связанности кода.

Интерфейс — это контракт, в котором перечислены методы, которые должен предоставлять класс, без указания того, как эти методы работают. Он позволяет несвязанным классам договориться об общем наборе поведений, чтобы остальной код мог опираться на контракт, а не на конкретный класс. В этой главе рассматривается объявление интерфейсов, их реализация, использование констант, расширение и объединение интерфейсов, а также типичные ошибки, которых следует избегать.

Если вы только знакомитесь с объектами, сначала прочитайте Классы и объекты PHP и Что такое ООП в PHP.

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

Интерфейс определяет набор публичных сигнатур методов — имена, параметры и (опционально) возвращаемые типы — но не содержит тел методов. Любой класс, который реализует интерфейс, обязуется предоставить конкретное тело для каждого из этих методов. Если этого не сделать, PHP выбросит фатальную ошибку.

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

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

Ключевые правила для членов интерфейса:

  • Все методы неявно являются public и abstract — им нельзя давать тела, и нельзя помечать их как private или protected.
  • Интерфейс может объявлять константы, но не свойства.
  • Класс сигнализирует о выполнении контракта с помощью ключевого слова implements.

Зачем использовать интерфейсы?

  • Контракты. Они гарантируют, что любой реализующий класс предоставляет согласованный API, поэтому вызывающий код точно знает, какие методы доступны.
  • Слабая связанность. Код может быть написан против интерфейса (Logger), а не конкретного класса (FileLogger), что позволяет свободно менять реализации — реальный файловый логгер в продакшене, фиктивный — в тестах.
  • Множество типов. В отличие от наследования классов, класс может реализовывать несколько интерфейсов, выступая в нескольких ролях одновременно.
  • Полиморфизм. Разные объекты, реализующие один интерфейс, могут использоваться взаимозаменяемо везде, где ожидается этот интерфейс.

Реализация интерфейса

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

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

class ConsoleLogger implements Logger {
  public function log(string $message): void {
    echo "[LOG] {$message}\n";
  }
}

$logger = new ConsoleLogger();
$logger->log('App started');
// Output: [LOG] App started

ConsoleLogger соответствует контракту Logger, поэтому везде, где ваш код ожидает Logger, можно передать ConsoleLogger.

Тип-хинтинг через интерфейс

Наибольшая выгода достигается, когда вы указываете в качестве типа интерфейс, а не конкретный класс. Функция ниже работает с любым Logger, поэтому вы можете менять реализации, не переписывая её.

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

class ConsoleLogger implements Logger {
  public function log(string $message): void {
    echo "[LOG] {$message}\n";
  }
}

class SilentLogger implements Logger {
  public function log(string $message): void {
    // intentionally does nothing
  }
}

function runJob(Logger $logger): void {
  $logger->log('Job finished');
}

runJob(new ConsoleLogger()); // Output: [LOG] Job finished
runJob(new SilentLogger());  // Output: (nothing)

Реализация нескольких интерфейсов

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

<?php
interface Drawable {
  public function draw(): string;
}

interface Serializable {
  public function toArray(): array;
}

class Circle implements Drawable, Serializable {
  public function __construct(private float $radius) {}

  public function draw(): string {
    return "Circle(r={$this->radius})";
  }

  public function toArray(): array {
    return ['type' => 'circle', 'radius' => $this->radius];
  }
}

$c = new Circle(2.5);
echo $c->draw() . "\n";          // Output: Circle(r=2.5)
echo json_encode($c->toArray()); // Output: {"type":"circle","radius":2.5}

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

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

<?php
interface HttpStatus {
  const OK = 200;
  const NOT_FOUND = 404;
}

class Response implements HttpStatus {
  public function notFound(): int {
    return self::NOT_FOUND;
  }
}

$r = new Response();
echo $r->notFound();        // Output: 404
echo HttpStatus::OK;        // Output: 200

Расширение интерфейсов

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

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

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

interface ReadWrite extends Readable, Writable {}

class Memory implements ReadWrite {
  private string $buffer = '';

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

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

$m = new Memory();
$m->write('hello ');
$m->write('world');
echo $m->read(); // Output: hello world

Проверка на наличие интерфейса

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

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

class ConsoleLogger implements Logger {
  public function log(string $message): void {
    echo $message . "\n";
  }
}

$obj = new ConsoleLogger();
var_dump($obj instanceof Logger); // Output: bool(true)

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

Они схожи, но решают разные задачи:

ИнтерфейсАбстрактный класс
Тела методовНет (только сигнатуры)Да (можно смешивать конкретные + абстрактные)
СвойстваНет (только константы)Да
Сколько на классМного (implements A, B)Один (extends A)
Когда использоватьНесвязанные классы разделяют возможностьСвязанные классы разделяют код и состояние

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

Типичные ошибки

  • Пропущенный метод. Если забыть реализовать хотя бы один метод интерфейса, это вызовет фатальную ошибку при объявлении класса.
  • Сужение видимости. Методы интерфейса являются публичными; нельзя реализовать их как protected или private.
  • Изменение сигнатуры. Параметры и возвращаемый тип реализующего метода должны оставаться совместимыми с интерфейсом, иначе PHP выдаст ошибку.
  • Ожидание свойств. Интерфейсы описывают поведение, а не данные — они не могут содержать свойства, только константы.

Заключение

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

Практика

Практика
Каково основное назначение интерфейсов PHP согласно странице W3Docs?
Каково основное назначение интерфейсов PHP согласно странице W3Docs?
Was this page helpful?