Predvoditelev.RU
Заметки

Возвращаемый тип self и static в интерфейсах PHP

Очень популярная история в 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;
    }
}

С финальными классами это действительно гарантированно работает. Но в случае с интерфейсами (а также с не финальными классами, абстрактными классами и трейтами, но в этой статьей рассмотрим только интерфейсы) появляется не очевидная на первый взгляд проблема.

Проблема с self

Допустим у нас есть два интерфейса. Объекты, которые их реализуют, позволяют указать цвет и имя соответственно:

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 ColorableInterface
{
    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();
    }
}

Возвращаемый тип static

В PHP 8.0 появилась возможность указать в качестве результата выполнения метода тип static, который на уровне языка гарантирует, что метод вернёт экземпляр того же самого класса, в котором он вызван.

Теперь мы можем переписать интерфейсы следующим образом:

interface ColorableInterface
{
    public function setColor(string $color): static;
}

interface NameableInterface
{
    public function setName(string $name): static;
}

И это решит описанную выше проблему.

Когда использовать 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 — экземпляр объекта того же класса, что и объект в котором вызван метод.
@sergei_predvoditelev — Авторский канал в Telegram: заметки о веб-разработке, PHP, открытом ПО, развитии и немного о жизни.