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

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

Интерфейсы определяются так же, как классы, но с ключевым словом 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 и раздел «Объявления типов».