Типы данных в PHP: self и parent

Начиная с PHP 5.0, мы можем указывать типы данных аргументов функции, а с выходом новых версий PHP количество возможных type hints увеличилось. Краткий ликбез в PHP self:

<?php

public function foo(
    // Since 5.0
    self $self,
    parent $parent,
    FooInterface $foo,
    // Since 5.1
    array $array,
    // Since 5.4
    callable $callable,
    // Since 7.0
    bool $bool,
    float $float,
    int $int,
    string $string
) {
    ...
}

В PHP 5.0 мы можем указывать типы параметров при определении функции, а начиная с PHP 7.0, можем использовать для этого скалярные типы данных. Также, начиная с PHP 7.0, можно указывать типы возвращаемых функцией значений. Давайте подробнее рассмотрим типы self и parent. Они всегда были доступны, но мы редко видим их использование. Почему так?

self

PHP self означает объект того же типа (текущего класса или подкласса). Для каждой переменной должно выполняться условие instanceof по отношению к текущему классу.

Использование в качестве аргумента функции

<?php

interface Person {
    public function addSibling(self $sibling)
    {
        // ...
    }
}

Бывает необходимо спроектировать связь между объектами одного типа. В данном случае тип self может быть полезен.
Когда мы создаем реализацию этого интерфейса (или рефакторим код без соблюдения SOLID принципов) следует заменить тип self оригинальным названием класса или интерфейса:

<?php

use Person as PersonContract;

class Person implements PersonContract {
    public function addSibling(PersonContract $sibling)
    {
        // ...
    }
}

Использование в качестве типа возвращаемого значения

Давайте разберемся, в каких ситуациях можно использовать new self PHP в качестве типа возвращаемого значения.

Сеттеры

Один из самых очевидных примеров использования - это сеттеры (или мутаторы) с возможностью сцепления (chaining):

<?php

$foo->setBar($bar)->setBaz($baz);

Когда вы используете self, тип возвращаемого значения не важен, если метод возвращает клонированный объект (это тот случай, когда вы имеете дело с неизменяемыми объектами). Возвращаемый объект того же типа, что и объект, метод которого был вызван:

<?php

interface Foo {
    /**
     * @param Bar $bar
     * @return self
     */
    public function setBar(Bar $bar) : self;
}

При расширении или реализации метода в дочернем классе необходимо явно указать тип, чтобы объявление было совместимым:

<?php

use Foo as FooContract;

class Foo implements FooContract
{
    /**
     * {@inheritdoc}
     */
    public function setBar(Bar $bar) : FooContract
    {
        // ...
        return $this;
    }
}

Если вы будете использовать этот объект, то он сообщит IDE, что все, что возвращается, будет иметь тип Foo (интерфейс). В итоге он не будет автоматически заполнять этим методом имена в реализации класса Foo:

<?php

/* @var Foo $foo */
$foo->setBar(new Bar())->setBaz(new Baz());

В приведенном выше примере, если setBaz не является методом интерфейса Foo, он не будет признан IDE. Таким образом, для построения цепочки методов тип возвращаемого значения self PHP class не особенно полезен.

До PHP 7.0 мы обычно объявляли только типы возвращаемых значений в DocBlock comments с @return $this, @return self или @return static. Для цепочечных методов, которые я до сих пор использую, @return static и @return $this в docblocks.

Фабричные методы

Нелегко найти пример метода, который не является сеттером и возвращает объект того же класса. Но вот он:

<?php

interface Foo
{
    public function wrapWithLogDecorator(PsrLogLoggerInterface $logger) : self;
}

Реализация будет выглядеть так:

<?php

trait LoggerWrapping
{
    public function wrapWithLogDecorator(PsrLogLoggerInterface $logger) : Foo
    {
         return new LogWrapper($this);
    }
}

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

parent

Документация PHP говорит, что parent допустимый тип данных. Давайте разберемся, что он из себя представляет:

<?php

class Foo extends Bar {
    public function setBar(parent $bar)
    {
        // ...
    }
}

parent всегда указывает на класс, который вы расширяете. Для примера выше: Bar или один из его дочерних классов. То есть передаваемый объект может быть другим дочерним от Bar.

В данном случае parent не может указывать на интерфейс. И схема работы примерна такая же, когда вы вызываете метод родительского класса (parent::setBar например). Другими словами, parent можно использовать только тогда, когда текущий класс расширяет какой-то другой класс.

Есть несколько причин, почему тип parent не используется:

  • Когда при разработке кода соблюдаются SOLID принципы, то почти все - это либо интерфейс, либо класс реализации (конечный узел в дереве наследования). Количество абстрактных классов очень мало, а расширенных классов почти нет. Таким образом, использование типа parent бесполезно за исключением некоторых очень редких случаев;
  • Использование интерфейсов или реализующих классов (PHP self) для type hints предпочтительней, чему соответствуют различные принципы и методики программирования - SOLID, принцип подстановки Барбары Лисков (объекты должны быть заменяемы объектами подтипов), принцип разделения интерфейсов и принцип инверсии зависимостей (зависимость от абстракций, в данном случае интерфейсов).

ВЛВиктория Лебедеваавтор-переводчик статьи «PHP type hints: self and parent»

Пожалуйста, опубликуйте свои отзывы по текущей теме статьи. За комментарии, лайки, отклики, дизлайки, подписки низкий вам поклон!