Интерфейсы объектов

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

Интерфейсы определяются так же, как классы, но с ключевым словом interface вместо слова class и с методами, ни один из которых не определяет содержимое тела.

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

Интерфейсы преследуют две взаимодополняющие цели:

  • Разрешают разработчикам создавать объекты разноимённых классов, которые умеют взаимно заменять друг друга, поскольку реализуют один и тот же интерфейс или интерфейсы. Интерфейсы часто внедряют в код, когда требуется создать набор служб доступа к базе данных, платёжных шлюзов или стратегий кеширования. Один класс подменяют другим без изменения кода, который его использует.
  • Разрешают параметру функции или метода принимать и обрабатывать объект, который подчиняется контракту интерфейса, чтобы не заботиться о том, что ещё умеет делать объект или как его реализовали. Интерфейсы часто называют Iterable, Cacheable, Renderable и другими похожими именами, чтобы описать поведение интерфейса.

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

Замечание:

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

Оператор implements

Для реализации интерфейса используется оператор implements. Класс должен реализовать все методы, описанные в интерфейсе, иначе произойдёт фатальная ошибка. При желании классы могут реализовывать более одного интерфейса, разделяя каждый интерфейс запятой.

Внимание

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

Замечание:

Аналогично классам, интерфейсы расширяют оператором extends.

Замечание:

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

Константы

Интерфейсы поддерживают объявления констант. Константы интерфейсов работают так же, как константы классов. До PHP 8.1.0 константы интерфейса нельзя было переопределять в производном классе или интерфейсе.

Properties

Начиная с PHP 8.4.0 в интерфейсах разрешили объявлять свойства. При объявлении свойств потребуется указать, доступно ли свойство для чтения, записи или и того, и другого. Объявление интерфейса применяется только к открытому доступу на чтение и запись.

Класс удовлетворяет свойству интерфейса несколькими способами: класс определяет открытое свойство, виртуальное свойство, которое реализует только тот хук, который соответствует хуку интерфейса, или определяет readonly-свойство, которое удовлетворяет свойству интерфейса для чтения. Однако в классе нельзя ограничивать доступ на запись свойства модификатором readonly, если в интерфейсе свойство объявили доступным для записи.

Пример #1 Пример свойств интерфейса

<?php

interface I
{
    // Класс, в котором реализуется свойство, ДОЛЖЕН объявить открытое для чтения свойство,
    // но объявление свойства в интерфейсе не ограничивает объявление доступа на запись свойства в классе
    public string $readable {
        get;
    }

    // Класс, в котором реализуется свойство, должен объявить открытое для записи свойство,
    // но объявление свойства в интерфейсе не ограничивает объявление доступа на чтение свойства в классе
    public string $writeable {
        set;
    }

    // Класс, в котором реализуется свойство, должен объявить свойство,
    // открытое как для чтения, так и для записи
    public string $both {
        get;
        set;
    }
}

// Класс реализует каждое из трёх свойств традиционно, без хуков.
// Такая реализация свойств допустима
class C1 implements I
{
    public string $readable;

    public string $writeable;

    public string $both;
}

// Класс реализует каждое из трёх свойств и определяет только те хуки,
// которые потребовал интерфейс. Такая реализация свойств тоже допустима
class C2 implements I
{
    private string $written = '';
    private string $all = '';

    // Класс реализует только хук для чтения, чтобы создать виртуальное свойство.
    // Такое определение удовлетворяет требованию «публичной открытости для чтения».
    // Свойство недоступно для записи, но интерфейс и не требует открытого доступа для записи
    public string $readable {
        get => strtoupper($this->writeable);
    }

    // Интерфейс требует только того, чтобы класс определил свойство, открытое для записи,
    // но включение хука для операции чтения тоже допустимо.
    // Пример создаёт виртуальное свойство, и это нормально
    public string $writeable {
        get => $this->written;

        set {
            $this->written = $value;
        }
    }

    // Свойство требует как операции чтения, так и операции записи,
    // поэтому потребуется либо реализовать оба хука, либо разрешить операциям чтения и записи
    // поведение по умолчанию
    public string $both {
        get => $this->all;

        set {
            $this->all = strtoupper($value);
        }
    }
}

?>

Примеры

Пример #2 Пример интерфейса

<?php

// Объявляем интерфейс 'Template'
interface Template
{
    public function setVariable($name, $var);
    public function getHtml($template);
}

// Реализуем интерфейс
// Это сработает
class WorkingTemplate implements Template
{
    private $vars = [];

    public function setVariable($name, $var)
    {
        $this->vars[$name] = $var;
    }

    public function getHtml($template)
    {
        foreach ($this->vars as $name => $value) {
            $template = str_replace('{' . $name . '}', $value, $template);
        }

        return $template;
    }
}

// Это не сработает
// Fatal error: Class BadTemplate contains 1 abstract methods
// and must therefore be declared abstract (Template::getHtml)
// (Фатальная ошибка: Класс BadTemplate содержит 1 абстрактный метод
// и поэтому требуется объявить класс абстрактным (Template::getHtml))
class BadTemplate implements Template
{
    private $vars = [];

    public function setVariable($name, $var)
    {
        $this->vars[$name] = $var;
    }
}

?>

Пример #3 Наследование интерфейсов

<?php

interface A
{
    public function foo();
}

interface B extends A
{
    public function baz(Baz $baz);
}

// Это сработает
class C implements B
{
    public function foo() {}

    public function baz(Baz $baz) {}
}

// Это не сработает и выдаст фатальную ошибку
class D implements B
{
    public function foo() {}

    public function baz(Foo $foo) {}
}

?>

Пример #4 Совместимость с несколькими интерфейсами

<?php

class Foo {}
class Bar extends Foo {}

interface A
{
    public function myfunc(Foo $arg): Foo;
}

interface B
{
    public function myfunc(Bar $arg): Bar;
}

class MyClass implements A, B
{
    public function myfunc(Foo $arg): Bar
    {
        return new Bar();
    }
}

?>

Пример #5 Множественное наследование интерфейсов

<?php

interface A
{
    public function foo();
}

interface B
{
    public function bar();
}

interface C extends A, B
{
    public function baz();
}

class D implements C
{
    public function foo() {}

    public function bar() {}

    public function baz() {}

}
?>

Пример #6 Интерфейсы с константами

<?php

interface A
{
    const B = 'Константа интерфейса';
}

// Выведет: Константа интерфейса
echo A::B;


class B implements A
{
    const B = 'Константа класса';
}

// Выведет: Константа класса
// До PHP 8.1.0 этот код не сработает,
// поскольку запрещалось переопределять константы
echo B::B;

?>

Пример #7 Интерфейсы с абстрактными классами

<?php

interface A
{
    public function foo(string $s): string;

    public function bar(int $i): int;
}

// Абстрактному классу можно реализовывать только часть интерфейса.
// Классы, которыми расширяется абстрактный класс, должны реализовать остальные требования интерфейса
abstract class B implements A
{
    public function foo(string $s): string
    {
        return $s . PHP_EOL;
    }
}

class C extends B
{
    public function bar(int $i): int
    {
        return $i * 2;
    }
}

?>

Пример #8 Одновременное расширение класса и реализация интерфейсов

<?php

class One
{
    /* ... */
}

interface Usable
{
    /* ... */
}

interface Updatable
{
    /* ... */
}

// Порядок ключевых слов здесь важен. Слово extends должно идти первым
class Two extends One implements
    Usable,
    Updatable
{
    /* ... */
}

?>

Интерфейс вместе с объявлениями типов предоставляет надёжный способ проверки того, что конкретный объект содержит конкретные методы. Смотрите также описание оператора instanceof и раздел «Объявления типов».