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:: .