Сравнение self и static в объектно-ориентированном PHP

Раздел: PHP -> Объектно-ориентированное программирование в PHP

Понимание self и static в PHP: различия, примеры и практическое применение

В объектно-ориентированном PHP ключевое слово self ссылается на класс, в котором оно записано (раннее статическое связывание), а static - на класс, из которого был вызван метод (позднее статическое связывание). Это различие становится критичным при наследовании и переопределении статических методов и свойств.

Наиболее эффективное решение для полиморфизма статических методов - использование static::. Оно позволяет дочерним классам переопределять статические методы, сохраняя корректное связывание при вызовах из родительского класса.

class ParentClass {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        self::who(); // всегда вызовет ParentClass::who()
    }
    public static function testStatic() {
        static::who(); // вызовет метод класса, который вызвал testStatic
    }
}
class ChildClass extends ParentClass {
    public static function who() {
        echo __CLASS__;
    }
}
ChildClass::test();     // выведет "ParentClass"
ChildClass::testStatic(); // выведет "ChildClass"

Php self static (self и static в php)

ParentClass
ChildClass

Static classes php (статические классы в php)

Если заменить self:: на static:: в родительском методе test(), то вызов из дочернего класса будет обращаться к переопределённому методу. Это и есть позднее статическое связывание.

Типичная ошибка: использование self:: в статическом методе, который должен вести себя полиморфно. В результате даже при вызове из наследника выполняется код родителя. Решение: заменить self на static. Следует помнить, что static не работает для констант и свойств, объявленных как const или static - для них при переопределении в наследнике static:: также даст корректный результат.

Как использовать self для доступа к константам и свойствам текущего класса?

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

class Config {
    const VERSION = '1.0';
    public static function getVersion() {
        return self::VERSION;
    }
}
echo Config::getVersion(); // 1.0

Php static method (статические методы в php)

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

Как реализовать синглтон, поддерживающий наследование?

В классическом синглтоне метод getInstance() возвращает new self(), что не позволяет наследникам иметь собственные экземпляры. Замена на new static() решает проблему.

class SingletonParent {
    protected static $instance = null;
    public static function getInstance() {
        if (self::$instance === null) {
            // self::$instance = new self(); // всегда экземпляр SingletonParent
            self::$instance = new static(); // экземпляр того класса, который вызвал
        }
        return self::$instance;
    }
}
class SingletonChild extends SingletonParent {}
$a = SingletonParent::getInstance();
$b = SingletonChild::getInstance();
echo get_class($a) . ' ' . get_class($b);

Php 8 class (классы php 8 (нововведения))

SingletonParent SingletonChild

Php class method (метод класса php)

Проблема: свойство $instance объявлено в родителе. Если наследник не переопределит его, то все наследники будут использовать одно и то же хранилище. Для полноценного наследования синглтона нужно переопределить $instance в каждом дочернем классе.

Как получить имя вызываемого класса в статическом контексте?

Функция get_called_class() возвращает имя класса, который был вызван во время выполнения (аналог static::class). self::class возвращает имя класса, где написано выражение.

class A {
    public static function className() {
        echo 'self::class: ' . self::class . PHP_EOL;
        echo 'static::class: ' . static::class . PHP_EOL;
        echo 'get_called_class(): ' . get_called_class() . PHP_EOL;
    }
}
class B extends A {}
B::className();

Class string php (класс для работы со строками php)

self::class: A
static::class: B
get_called_class(): B

Include class php (подключение класса в php (include/require))

Если в коде используется self::class для логирования или идентификации, то при наследовании будет всегда выдаваться имя родительского класса, что может привести к ошибкам идентификации.

Трейты и позднее статическое связывание

Внутри трейта self ссылается на класс, использующий трейт, а static - на класс, который вызвал метод (может быть наследником). Разница невелика, но static полезен для создания методов трейта, которые должны быть полиморфными.

trait Logger {
    public static function log() {
        echo 'Logged from ' . static::class . PHP_EOL;
    }
}
class Base {
    use Logger;
}
class Derived extends Base {}
Derived::log(); // выведет 'Logged from Derived'

Php class var (свойство (поле) класса php)

Использование static для фабричных методов

Фабричный метод, объявленный в родительском классе, с new static() создаёт объекты того класса, который вызвал этот метод. Это позволяет строить иерархии без дублирования кода.

class Product {
    public static function create() {
        return new static();
    }
}
class Book extends Product {}
class Car extends Product {}
$book = Book::create();
$car = Car::create();
echo get_class($book) . ' ' . get_class($car);
Book Car
Если бы использовалось new self(), то оба вызова вернули бы объект Product.

Ошибки статического связывания при использовании self в наследовании

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

class ParentClass {
    protected static $value = 'parent';
    public static function getValue() {
        return self::$value;
    }
}
class ChildClass extends ParentClass {
    protected static $value = 'child';
}
echo ChildClass::getValue(); // выведет 'parent'

Ожидание: 'child'. Решение: заменить self на static.

Расширенные примеры использования self и static

Пример 1: Многоуровневое наследование и цепочка вызовов

Пример
class A {
    public static function test() {
        static::who();
    }
    public static function who() {
        echo 'A';
    }
}
class B extends A {
    public static function who() {
        echo 'B';
    }
}
class C extends B {
    public static function who() {
        echo 'C';
    }
}
C::test(); // выведет 'C'
C

Здесь static::who() в методе test класса A связывается с классом C, поскольку вызов идёт от C. Если заменить на self::who(), то всегда будет выводиться 'A'.

Пример 2: Абстрактный синглтон с поддержкой наследования

Пример
abstract class Singleton {
    protected static array $instances = [];
    public static function getInstance(): static {
        $class = static::class;
        if (!isset(self::$instances[$class])) {
            self::$instances[$class] = new static();
        }
        return self::$instances[$class];
    }
}
class Database extends Singleton {
    // дополнительная логика
}
class Logger extends Singleton {
    // дополнительная логика
}
$db1 = Database::getInstance();
$db2 = Database::getInstance();
$log1 = Logger::getInstance();
var_dump($db1 === $db2); // true
var_dump($db1 instanceof Database); // true
var_dump($log1 instanceof Logger); // true
bool(true)
bool(true)
bool(true)

Используется static::class для ключа массива и new static() для создания экземпляра. Каждый наследник получает свой одиночный объект.

Пример 3: Статическая фабрика с настройками дочерних классов

Пример
class Vehicle {
    protected static $wheels = 0;
    public static function create(): static {
        $obj = new static();
        $obj->configure();
        return $obj;
    }
    protected function configure() {
        // пусто, переопределяется в потомках
    }
}
class Bicycle extends Vehicle {
    protected static $wheels = 2;
    protected function configure() {
        echo 'Created bicycle with ' . static::$wheels . ' wheels';
    }
}
class Car extends Vehicle {
    protected static $wheels = 4;
    protected function configure() {
        echo 'Created car with ' . static::$wheels . ' wheels';
    }
}
$bike = Bicycle::create();
$car = Car::create();
Created bicycle with 2 wheels
Created car with 4 wheels

static::$wheels в методе configure берёт значение из класса, который был создан благодаря позднему связыванию.

Пример 4: Изменение статических свойств в дочерних классах с сохранением общего счётчика

Пример
class Counter {
    protected static int $count = 0;
    public static function increment() {
        self::$count++;
        echo 'Total: ' . self::$count . ', Class: ' . static::class . PHP_EOL;
    }
}
class ChildA extends Counter {}
class ChildB extends Counter {}
Counter::increment(); // Total: 1, Class: Counter
ChildA::increment(); // Total: 2, Class: ChildA
ChildB::increment(); // Total: 3, Class: ChildB
Total: 1, Class: Counter
Total: 2, Class: ChildA
Total: 3, Class: ChildB

Здесь self::$count обращается к одному и тому же свойству класса Counter (единственное статическое свойство). Если бы требовалось раздельное хранение, нужно было бы объявить свойство в каждом наследнике и использовать static::$count.

Пример 5: Комбинация static с instanceof и get_class

Пример
class Base {
    public static function whoIs() {
        return static::class;
    }
}
class Sub extends Base {}
$class = Sub::whoIs();
$obj = new Sub();
echo $class . ' ' . ($obj instanceof Sub ? 'да' : 'нет');
Sub да

Метод whoIs возвращает имя вызывающего класса, что удобно для логирования или создания объектов с проверкой.

self и static в PHP - comments

En
Php self static (php)