PHP: Разница между self::, static:: и parent::

Источник: «The Difference Between self::, static::, and parent:: in PHP»
В этой статье мы обсудим различия между self::, static:: и parent:: в PHP. Также расскажем, когда и почему вы можете использовать каждый из них в своём коде.

Вступление

При работе с PHP-кодом часто встречаются parent::, static:: и self::. Но если вы начинающий разработчик, то можете не знать, что они делают и как отличаются.

Я признаюсь, что когда был начинающим разработчиком, то долго считал, что static:: и self:: это одно и тоже.

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

Что такое parent::?

Начнём с разговора о parent::.

Чтобы получить представление, что он делает, вероятно, лучше сначала рассмотреть несколько примеров кода.

Давайте представим, что у нас есть класс BaseTestCase с методом setUp:

class BaseTestCase
{
public function setUp(): void
{
echo 'Run base test case set up here...';
}
}

(new BaseTestCase())->setUp();

// Output is: "Run base test case set up here...';

Как мы видим, когда мы вызываем метод setUp, он работает, как и ожидалось, и выводит текст.

Теперь давайте представим, что мы хотим создать новый класс FeatureTest, который наследует класс BaseTestCase. Если бы мы хотели запустить метод setUp класса FeatureTest, мы могли бы сделать это следующим образом:

class FeatureTest extends BaseTestCase
{
//
}

(new FeatureTest())->setUp();

// Output is: "Run base test case set up here...";

Как видите, мы не определили метод setUp в нашем FeatureTest, поэтому вместо него будет запущен метод, определённый в BaseTestCase.

Теперь предположим, что мы хотим запустить дополнительную логику при запуске метода setUp в нашем FeatureTest. Например, если бы эти классы были тестовыми примерами, использующимися как часть PHPUnit теста, мы могли бы захотеть сделать такие вещи, как создание моделей в базе данных или установка тестовых значений.

Сначала вы можете (ошибочно) подумать, что можно просто определить метод setUp в классе FeatureTest и вызвать $this->setUp(). Честно говоря, я всегда попадал в эту ловушку, когда начинал изучать программирование!

Таким образом, наш код может выглядеть так:

class FeatureTest extends BaseTestCase
{
public function setUp(): void
{
$this->setUp();

echo 'Run extra feature test set up here...';
}
}

(new FeatureTest())->setUp();

Но вы обнаружите, что если бы мы запустили этот код, он бы зациклился, что привело бы к сбою вашего приложения. Это связано с тем, что мы просим SetUp снова и снова рекурсивно вызывать себя. Скорее всего вы получите вывод, подобный этому:

Fatal error: Out of memory (allocated 31457280 bytes) (tried to allocate 262144 bytes) in /in/1MXtt on line 15

mmap() failed: [12] Cannot allocate memory

mmap() failed: [12] Cannot allocate memory

Process exited with code 255.

Вместо использования $this->setUp() нам нужно указать PHP использовать метод setUp из BaseTestCase. Для этого мы должны заменить $this->setUp() на parent::setUp():

class FeatureTest extends BaseTestCase
{
public function setUp(): void
{
parent::setUp();

echo 'Run extra feature test set up here...';
}
}

(new FeatureTest())->setUp();

// Output is: "Run base test case set up here... Run extra feature test set up here...";

Теперь, при вызове метода setUp в классе FeatureTest, сначала выполняется код из BaseTestCase, а затем продолжается работа остального кода определённого в дочернем классе.

Стоит отметить, что не всегда нужно размещать вызов parent:: в начале метода. На самом деле его можно разместить там, где хотите, чтобы он лучше соответствовал назначению кода. Например, если хотите сначала запустить свой код в классе FeatureTest, а затем выполнить код из BaseTestCase, вы можете переместить вызов parent::setUp() в конец метода:

class FeatureTest extends BaseTestCase
{
public function setUp(): void
{
echo 'Run extra feature test set up here...';

parent::setUp();
}
}

(new FeatureTest())->setUp();

// Output is: "Run extra feature test set up here... Run base test case set up here...";

Что такое self::?

Теперь давайте взглянем на self::.

Представим, что у нас есть класс Model со статическим свойством connection и методом makeConnection. Мы так же представим, что у нас есть класс User наследующий класс Model и переопределяющий свойство connection.

Эти классы могут выглядеть так:

class Model
{
public static string $connection = 'mysql';

public function makeConnection(): void
{
echo 'Making connection to: '.self::$connection;
}
}

class User extends Model
{
public static string $connection = 'postgres';
}

Теперь давайте вызовем метод makeConnection для обоих классов и посмотрим, что получим на выходе:

(new Model())->makeConnection();

// Output is: "Making connection to mysql"

(new User())->makeConnection();

// Output is: "Making connection to mysql";

Как видим, оба вызова в результате использовали свойство connection класса Model. Это связано с тем, что self:: использует свойство, определённое в классе, в котором существует метод. В обоих случаях метод вызывается makeConnection класса Model, поскольку он не определён в классе User.

Чтобы нагляднее продемонстрировать это, мы продублируем метод makeConnection в классе User:

class Model
{
public static string $connection = 'mysql';

public function makeConnection(): void
{
echo 'Making connection to: '.self::$connection;
}
}

class User extends Model
{
public static string $connection = 'postgres';

public function makeConnection(): void
{
echo 'Making connection to: '.self::$connection;
}
}

Теперь, если снова вызвать оба этих метода, мы получим следующий результат:

(new Model())->makeConnection();

// Output is: "Making connection to mysql"

(new User())->makeConnection();

// Output is: "Making connection to postgres";

Как видите, вызов метода makeConnection в классе User будет использовать свойство connection класса User, потому что в теперь в нём существует этот метод.

Что такое static::?

Теперь, когда у нас есть представление, что делает метод self::, давайте взглянем на static::.

Чтобы лучше понять, что он делает, давайте обновим код, используя static:: вместо self:::

class Model
{
public static $connection = 'mysql';

public function makeConnection()
{
echo 'Making connection to: '.static::$connection;
}
}

class User extends Model
{
public static $connection = 'postgres';
}

Если вызвать метод makeConnection для обоих классов, получим следующий результат:

new Model())->makeConnection();

// Output is: "Making connection to mysql"

(new User())->makeConnection();

// Output is: "Making connection to postgres";

Как видите этот результат отличается от того, когда использовался self::$connection ранее. Вызов метода makeConnection в классе User использует свойство connection класса User, а не класса Model (где этот метод фактически размещён). Это связано с функцией PHP называемой позднее статическое связывание.

Если вам интересно узнать больше о позднем статическом связывании, вы можете почитать о ней в документации по PHP.

Согласно документации PHP:

Само название "позднее статическое связывание" отражает в себе внутреннюю реализацию этой особенности. "Позднее связывание" отражает тот факт, что обращения через static:: не будут вычисляться по отношению к классу, в котором вызываемый метод определён, а будут вычисляться на основе информации в ходе исполнения. Также эта особенность была названа "статическое связывание" потому, что она может быть использована (но не обязательно) в статических методах.

Таким образом, в нашем примере используется свойство connection класса User, потому, что мы вызываем метод makeConnection в том же самом классе.

Следует отметить, что если бы свойство connection не существовало в классе User, вместо этого было бы использовано свойство класса Model.

Что использовать self:: или static::

Теперь, когда у нас есть общее представление о разнице между self:: и static::, давайте быстро рассмотрим, как решить, что из них использовать в вашем собственном коде.

На самом деле всё сводится к случаю использования кода, который вы пишите.

В общем, я бы использовал static:: вместо self::, потому что хотел бы, чтобы мои классы были расширяемыми и обеспечивали поддержку, если они унаследованы.

Предположим я хочу написать класс, от которого я намерен полностью наследовать дочерний класс (например, класс BaseTestCase из ранее приведённого примера). Если бы я действительно не хотел предотвратить, чтобы дочерний класс переопределял свойство или метод, я бы использовал static::.

Это означало бы, что можно быть уверенным, что если я переопределю любой из статических методов или свойств, мой дочерний класс будет использовать мои переопределения. Я не могу сказать, сколько раз сталкивался с ошибками в своём коде, когда использовал self:: в родительском классе, а затем не мог понять, почему мой дочерний класс не использует мой переопределение!

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

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

В общем, лучше всего в каждом конкретном случае во время написания кода решать, следует использовать static:: или self::.

Заключение

Надеюсь эта статья дала вам представление о разнице между static::, self:: и parent::.

Дополнительные материалы

Предыдущая Статья

Laravel: Что такое Collection / Коллекции

Следующая Статья

Laravel: Как работают транзакции базы данных