Очень популярная история в PHP, когда метод объекта возвращает сам объект или новый экземпляр того же самого объекта. В этом случае мы указываем тип результата self
и всё отлично работает. Например:
final class Car {
public function __construct(
private ?string $color = null,
) {
}
public function withColor(string $color): self
{
$new = clone $this;
$new->color = $color;
return $new;
}
}
С финальными классами это действительно гарантированно работает. Но в случае с интерфейсами (а также с не финальными классами, абстрактными классами и трейтами, но в этой статьей рассмотрим только интерфейсы) появляется не очевидная на первый взгляд проблема.
Допустим у нас есть два интерфейса. Объекты, которые их реализуют, позволяют указать цвет и имя соответственно:
interface ColorableInterface
{
public function setColor(string $color): self;
}
interface NameableInterface
{
public function setName(string $name): self;
}
И мы решили сделать некий конфигуратор для объектов, реализующих оба интерфейса:
final class Configurator {
public function configure(
ColorableInterface&NameableInterface $object,
string $color,
string $name
): void {
$object->setColor($color)->setName($name);
}
}
И вроде бы всё хорошо, но если прогнать этот код, к примеру, через статический анализатор Psalm, то мы получим ошибку "Method ColorableInterface::setName does not exist" («Метод ColorableInterface::setName не существует»).
И действительно, Psalm не ошибся. Интерфейс ColorableInterface
гарантирует, что метод setColor()
вернёт self
, то есть объект, реализующий ColorableInterface
, и ничего более, а метода setName()
в интерфейсе ColorableInterface
нет.
Пример объекта, реализующего оба интерфейса, но при этом не готового для использования в конфигураторе из предыдущего примера:
final class CustomColor implements ColorableInterface
{
private string $color = '';
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
}
final class CustomName implements NameableInterface
{
private string $name = '';
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
}
final class StrangeObject implements ColorableInterface, NameableInterface {
public function setColor(string $color): ColorableInterface
{
return new CustomColor();
}
public function setName(string $name): NameableInterface
{
return new CustomName();
}
}
В PHP 8.0 появилась возможность указать в качестве результата выполнения метода тип static
, который на уровне языка гарантирует, что метод вернёт экземпляр того же самого класса, в котором он вызван.
Теперь мы можем переписать интерфейсы следующим образом:
interface ColorableInterface
{
public function setColor(string $color): static;
}
interface NameableInterface
{
public function setName(string $name): static;
}
И это решит описанную выше проблему.
Как правило, static
используется в следующих ситуациях.
Текучий интерфейс — структурный шаблон проектирования, позволяющий создавать более читабельный код. Фактически, это возможность вызова методов «цепочкой».
interface People {
public function setName(string $name): static;
public function setAge(int $age): static;
}
Неизменяемый (иммутабельный) класс — это класс, который после создания не меняет своего состояния, то есть он не содержит сеттеров и публичных изменяемых свойств. Но такие классы могут содержать методы (обычно они имеют префикс with
), позволяющие получить клон объекта с изменённым состоянием. Как раз для таких методов в интерфейсах необходимо использовать тип static
.
interface ReadableInterface
{
public function withLimit(int $limit): static;
public function read(): iterable;
}
Статические методы, позволяющие создать экземпляр объекта текущего класса.
interface Logger
{
public static function create(): static;
}
Интерфейс, определяющий метод для реализации шаблона проектирования «Одиночка» (метод предоставляющий доступ к единственному экземпляру объекта в приложении и запрещающий повторное создание этого объекта).
interface Something
{
public static function getInstance(): static;
}
Таким образом, при проектировании интерфейсов в PHP мы можем сразу заложить, что ожидаем увидеть в качестве результата выполнения метода:
self
— любой объект, который реализует данный интерфейс;static
— экземпляр объекта того же класса, что и объект в котором вызван метод.