PHP: Разница между self::, static:: и parent::
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::
.