- Что такое объектно-ориентированное программирование (ООП)?

Объектно-ориентированное программирование, обычно называемое ООП - это подход, который вам помогает разрабатывать сложные приложения таким образом, чтобы они легко поддерживались и масштабировались в течение длительного времени. В мире ООП реальные понятия Person, Car или Animal рассматриваются как объекты. В объектно-ориентированном программировании вы взаимодействуете с вашим приложением, используя объекты. Это отличается от процедурного программирования, когда вы, в первую очередь, взаимодействуете с функциями и глобальными переменными.

В ООП существует понятие «class», используемое для моделирования или сопоставления реального понятия с шаблоном данных (свойств) и функциональных возможностей (методов). «Оbject» - это экземпляр класса, и вы можете создать несколько экземпляров одного и того же класса. Например, существует один класс Person, но многие объекты person могут быть экземплярами этого класса - dan, zainab, hector и т. д.

Например, для класса Person могут быть name, age и phoneNumber. Тогда у каждого объекта person для этих свойств будут свои значения.

Вы также можете определить в классе методы, которые позволяют вам манипулировать значениями свойств объекта и выполнять операции над объектами. В качестве примера вы можете определить метод save, сохраняющий информацию об объекте в базе данных.


- Что такое класс PHP?

Класс - это шаблон, который представляет реальное понятие и определяет свойства и методы данного понятия.

Лучший способ понять новые концепции - показать это на примере. Итак, давайте рассмотрим в коде класс Employee, который представляет объект служащего:

class Employee {
	private $first_name;
	private $last_name;
	private $age;
	
	public function __construct($first_name, $last_name, $age) {
		$this->first_name = $first_name;
		$this->last_name = $last_name;
		$this->age = $age;
	}
	
	public function getFirstName() {
		return $this->first_name;
	}
	
	public function getLastName() {
		return $this->last_name;
	}
	
	public function getAge() {
		return $this->age;
	}
}

Оператор class Employee в первой строке определяет класс Employee. Затем мы продолжаем объявлять свойства, конструктор и другие методы класса.

Свойства класса в PHP

Вы можете думать о свойствах класса как о переменных, которые используются для хранения информации об объекте. В приведенном выше примере мы определили три свойства - first_name, last_name и age. В большинстве случаев доступ к свойствам класса осуществляется через созданные объекты.

Эти private свойства могут быть доступны только внутри класса. Данный подход - самый безопасный уровень доступа к свойствам.

Конструкторы для классов PHP

Конструктор - это специальный метод класса, который вызывается автоматически при инстанцинации объекта. В следующих разделах мы увидим, как инстанцировать объекты, но сейчас вам нужно просто знать, что конструктор используется для инициализации свойств объекта при создании объекта.

Вы можете определить конструктор с помощью метода __construct.

Методы для классов PHP

Давайте подумаем о методах класса как о функциях, которые выполняют определенные действия, связанные с объектами. В большинстве случаев они используются для доступа и управления свойствами объекта и выполнения связанных операций.

В приведенном выше примере мы определили метод getLastName, который возвращает фамилию, связанную с объектом.


- Что такое объект в PHP?

В предыдущем разделе мы обсудили базовую структуру PHP класса. Теперь, когда вы хотите использовать класс, вам нужно его инстанцировать, конечным результатом чего будет объект. Таким образом, мы можем думать о классе как о проекте, а объект - это реальная вещь, над которой вы можете работать.

В контексте класса Employee, созданного в предыдущем разделе, давайте посмотрим, как создать понятие объекта этого класса:

$objEmployee = new Employee('Bob', 'Smith', 30);

echo $objEmployee->getFirstName(); // print 'Bob'
echo $objEmployee->getLastName(); // prints 'Smith'
echo $objEmployee->getAge(); // prints '30'

Если вы хотите создать понятие объекта любого класса вместе с его именем, нужно использовать ключевое слово new, и в итоге вы получите новое понятие объекта этого класса.

Если класс определил метод __construct и ему требуются аргументы, вам нужно передать эти аргументы при создании экземпляра объекта. В нашем случае конструктор класса Employee требует три аргумента, и поэтому мы их передали, когда создавали объект $objEmployee. Как мы говорили ранее, метод __construct вызывается автоматически при инстанциации объекта.

Затем мы вызвали методы класса для объекта $objEmployee, чтобы запечатать информацию, инициализированную во время создания объекта. Конечно же, вы можете создать несколько объектов одного класса, как это показано в следующем фрагменте:

$objEmployee = new Employee('Bob', 'Smith', 30);

echo $objEmployee->getFirstName(); // print 'Bob'
echo $objEmployee->getLastName(); // prints 'Smith'
echo $objEmployee->getAge(); // prints '30'

$objEmployeeTwo = new Employee('John', 'Smith', 34);

echo $objEmployeeTwo->getFirstName(); // prints 'John'
echo $objEmployeeTwo->getLastName(); // prints 'Smith'
echo $objEmployeeTwo->getAge(); // prints '34'

Проще говоря, класс - это проект, который вы можете использовать для создания структурированных объектов.


- Инкапсуляция

В предыдущем разделе мы обсуждали, как создавать экземпляры объектов класса Employee. Интересно отметить, что сам объект $objEmployee объединяет свойства и методы класса. Другими словами, он скрывает эти детали от остальной части программы. В мире ООП это называется инкапсуляцией данных.

Инкапсуляция является важным аспектом ООП, позволяющий ограничить доступ к определенным свойствам или методам объекта. А это побуждает нас обсудить другую тему: уровень доступа.

Уровни доступа

Если вы определяете свойство или метод в классе, тогда вы можете объявить, что он имеет один из этих трех уровней доступа - public, private, или protected.

Доступ public

Когда вы объявляете свойство или метод как public, к нему можно получить доступ из любого места вне класса. Значение открытого свойства можно изменить из любого участка вашего кода.

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

class Person {
	public $name;
	
	public function getName() {
		return $this->name;
	}
}

$person = new Person();
$person->name = 'Bob Smith';
echo $person->getName(); // prints 'Bob Smith'

Как вы видете в приведенном выше примере, мы объявили общедоступное свойство name. Следовательно, вы можете установить его из любого места вне класса, что мы и сделали.

Доступ private

В случае если вы объявляете свойство или метод private, доступ к ним можно получить только из класса. Это означает, что вам нужно определить методы получения и установки, чтобы получить и установить значение этого свойства.

Опять же, давайте пересмотрим предыдущий пример, чтобы понять уровень частного доступа:

class Person {
	private $name;
	
	public function getName() {
		return $this->name;
	}
	
	public function setName($name) {
		$this->name = $name;
	}
}

$person = new Person();
$person->name = 'Bob Smith'; // Throws an error
$person->setName('Bob Smith');
echo $person->getName(); // prints 'Bob Smith'

Если вы захотите получить доступ к private свойству вне класса, он выдаст фатальную ошибку Cannot access private property Person::$name. Таким образом, вам нужно установить значение private свойства с помощью метода setter, как мы это cделали с помощью метода setName.

Могут возникать веские причины, из-за которых вы захотите установить private свойство. Например, возможно, для предпринятия какого-то действия (скажем, обновить базу данных или перерисовать шаблон), если это свойство меняется. В этом случае вы можете определить метод установки и управление любой специальной логикой для изменения свойства.

Доступ protected

Наконец, когда вы объявляете свойство или метод protected, к ним может обращаться тот же класс, который их определил, или классы, которые наследуют рассматриваемый класс.


- Наследование

Наследование является важным аспектом парадигмы объектно-ориентированного программирования, которая позволяет наследовать свойства и методы других классов, расширяя их. Класс, который наследуется, называется родительским классом, а класс, который наследует другой класс, называется дочерним классом. Когда вы создаете экземпляр объекта дочернего класса, он также наследует свойства и методы родительского класса.

Давайте попробуем разобраться на реальном примере, чтобы понять, как это работает:

class Person {
	protected $name;
	protected $age;
	
	public function getName() {
		return $this->name;
	}
	
	public function setName($name) {
		$this->name = $name;
	}
	
	private function callToPrivateNameAndAge() {
		return "{$this->name} is {$this->age} years old.";
	}
	
	protected function callToProtectedNameAndAge() {
		return "{$this->name} is {$this->age} years old.";
	}
}

class Employee extends Person {
	private $designation;
	private $salary;
	
	public function getAge() {
		return $this->age;
	}
	
	public function setAge($age) {
		$this->age = $age;
	}
	
	public function getDesignation() {
		return $this->designation;
	}
	
	public function setDesignation($designation) {
		$this->designation = $designation;
	}
	
	public function getSalary() {
		return $this->salary;
	}
	
	public function setSalary($salary) {
		$this->salary = $salary;
	}
	
	public function getNameAndAge() {
		return $this->callToProtectedNameAndAge();
	}
}

$employee = new Employee();

$employee->setName('Bob Smith');
$employee->setAge(30);
$employee->setDesignation('Software Engineer');
$employee->setSalary('30K');

echo $employee->getName(); // prints 'Bob Smith'
echo $employee->getAge(); // prints '30'
echo $employee->getDesignation(); // prints 'Software Engineer'
echo $employee->getSalary(); // prints '30K'
echo $employee->getNameAndAge(); // prints 'Bob Smith is 30 years old.'
echo $employee->callToPrivateNameAndAge(); // produces 'Fatal Error'

Здесь важно отметить, что класс Employee использовал для наследования класса Person ключевое слово extends. Теперь класс Employee может получить доступ ко всем свойствам и методам класса Person, объявленные как public или protected. Он не может получить доступ к свойствам, которые объявлены как private.

В примере выше объект $employee может получить доступ к методам getName и setName, которые определены в классе Person, поскольку они объявлены как public.

Затем мы обратились к методу callToProtectedNameAndAge, используя метод getNameAndAge, определенный в классе Employee, поскольку он объявлен как protected. Наконец, объект $employee не может получить доступ к методу callToPrivateNameAndAge класса Person, поскольку он объявлен как private.

С другой стороны, вы можете использовать объект $employee для установки свойства age класса Person, как мы это делали в методе setAge, который определен в классе Employee, поскольку свойство age объявлено как protected.

И так, это было краткое введение в наследование. Оно помогает сократить дублирование кода и, следовательно, способствует его повторному использованию.


- Полиморфизм

Полиморфизм - еще одна важная концепция в мире объектно-ориентированного программирования, которая относится к способности по-разному обрабатывать объекты в зависимости от их типов данных.

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

class Message {
	public function formatMessage($message) {
		return printf("<i>%s</i>", $message);
	}
}

class BoldMessage extends Message {
	public function formatMessage($message) {
		return printf("<b>%s</b>", $message);
	}
}

$message = new Message();
$message->formatMessage('Hello World'); // prints '<i>Hello World</i>'

$message = new BoldMessage();
$message->formatMessage('Hello World'); // prints '<b>Hello World</b>'

Как видите, мы изменили поведение метода formatMessage, переопределив его в классе BoldMessage. Важно то, что сообщение форматируется по-разному в зависимости от типа объекта, будь то экземпляр родительского или дочернего класса.

Некоторые объектно-ориентированные языки также имеют своего рода перезагрузку методов, которая позволяет определять несколько методов класса с одним и тем же именем, но с разным количеством аргументов. Это не поддерживается напрямую в PHP, но существуют несколько обходных путей для достижения аналогичной функциональности.


- ::class

Начиная с PHP 5.5 можно использовать ключевое слово class для разрешения имени класса. С помощью конструкции ClassName::class можно получить строку с абсолютным именем класса ClassName. Обычно это довольно полезно при работе с классами, использующими пространства имен.

namespace NS {
	class ClassName {
		
	}
	
	echo ClassName::class;
}

Результат выполнения данного примера: NS\ClassName

Разрешение имен класса с использованием ::class происходит на этапе компиляции. Это означает, что на момент создания строки с именем класса автозагрузки класса не происходит. Как следствие, имена классов раскрываются, даже если класс не существует. Ошибка в этом случае не выдается.


- Константы классов

Константы также могут быть объявлены в пределах одного класса. Отличие переменных от констант состоит в том, что при объявлении последних или при обращении к ним не используется символ $. Область видимости констант по умолчанию public.

Значение должно быть неизменяемым выражением, а не (к примеру) переменной, свойством или вызовом функции. Интерфейсы также могут содержать константы.

Начиная с PHP 5.3.0, стало возможным обратиться к классу с помощью переменной. Значение переменной не может быть ключевым словом (например, self, parent и static).

Обратите внимание, что константы класса задаются один раз для всего класса, а не отдельно для каждого созданного объекта этого класса.

class MyClass {
	const PI = 3.14;
	
	function showConstant() {
		echo self::PI;
	}
}

echo MyClass::PI; // 3.14

$classname = "MyClass";
echo $classname::PI; // 3.14 - начиная с PHP 5.3.0

$class = new MyClass();
$class->showConstant(); // 3.14

echo $class::PI; // 3.14 - начиная с PHP 5.3.0

Модификаторы видимости констант класса

// Начиная с PHP 7.1.0

class Foo {
	public const BAR = 'bar';
	private const BAZ = 'baz';
}

echo Foo::BAR; // bar
echo Foo::BAZ; // Uncaught Error: Cannot access private const Foo::BAZ

Начиная с PHP 7.1.0 для констант класса можно использовать модификаторы области видимости.


- Автоматическая загрузка классов

Большинство разработчиков объектно-ориентированных приложений используют такое соглашение именования файлов, в котором каждый класс хранится в отдельно созданном для него файле. Одна из самых больших неприятностей - необходимость писать в начале каждого скрипта длинный список подгружаемых файлов (по одному для каждого класса).

В PHP это делать необязательно. Функция spl_autoload_register() позволяет зарегистрировать необходимое количество автозагрузчиков для автоматической загрузки классов и интерфейсов, если они в настоящее время не определены. Регистрируя автозагрузчики, PHP получает последний шанс для интерпретатора загрузить класс прежде, чем он закончит выполнение скрипта с ошибкой.

В то время как функция __autoload() также может быть использована для автоматической загрузки классов и интерфейсов, следует отдать предпочтение spl_autoload_register(), потому, что она предоставляет гораздо более гибкую альтернативу, позволяя регистрировать необходимое количество автозагрузчиков, например, для сторонних библиотек. По этой причине использование __autoload() не рекомендуется, а c PHP 7.2.0 объявлено устаревшим.

В этом примере функция пытается загрузить классы MyClass1 и MyClass2 из файлов MyClass1.php и MyClass2.php соответственно:

spl_autoload_register(function ($class_name) {
    include $class_name . '.php';
});

$obj  = new MyClass1();
$obj2 = new MyClass2(); 

- Конструктор

PHP позволяет объявлять методы-конструкторы. Классы, в которых объявлен метод-конструктор, будут вызывать этот метод при каждом создании нового объекта, так что это может оказаться полезным, например, для инициализации какого-либо состояния объекта перед его использованием.

Конструкторы, определенные в классах-родителях не вызываются автоматически, если дочерний класс определяет собственный конструктор. Чтобы вызвать конструктор, объявленный в родительском классе, требуется вызвать parent::__construct() внутри конструктора дочернего класса. Если в дочернем классе не определен конструктор, то он может быть унаследован от родительского класса как обычный метод (если он не был определен как приватный).

class BaseClass {
	public function __construct() {
		print "Конструктор класса BaseClass";
	}
}

class SubClass extends BaseClass {
	public function __construct() {
		parent::__construct();
		print "Конструктор класса SubClass";
	}
}

class OtherSubClass extends BaseClass {
	// наследует конструктор BaseClass
}

// Конструктор класса BaseClass
$obj = new BaseClass();

// Конструктор класса BaseClass
// Конструктор класса SubClass
$obj = new SubClass();

// Конструктор класса BaseClass
$obj = new OtherSubClass();

- Деструктор

PHP предоставляет концепцию деструктора, аналогичную с той, которая применяется в других ОО-языках, таких как C++. Деструктор будет вызван при освобождении всех ссылок на определенный объект или при завершении скрипта (порядок выполнения деструкторов не гарантируется).

class MyDestructableClass {
	public function __construct() {
		print "Конструктор";
	}
	
	public function __destruct() {
		print "Уничтожается " . __CLASS__;
	}
}

$obj = new MyDestructableClass();

// Конструктор
// Уничтожается MyDestructableClass

Как и в случае с конструкторами, деструкторы, объявленные в родительском классе, не будут вызываться автоматически. Для вызова деструктора родительского класса, требуется вызвать parent::__destruct() в теле деструктора дочернего класса. Подобно конструкторам, дочерний класс может унаследовать деструктор из родительского класса, если он не определен в нем.


- Оператор разрешения области видимости (::)

Оператор разрешения области видимости (также называемый "Paamayim Nekudotayim") или просто "двойное двоеточие" - это лексема, позволяющая обращаться к статическим свойствам, константам и переопределенным свойствам или методам класса.

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

class MyClass {
    const CONST_VALUE = 'Значение константы';
}

$classname = 'MyClass';

echo $classname::CONST_VALUE; // Начиная с PHP 5.3.0 - Значение константы
echo MyClass::CONST_VALUE; // Значение константы

Для обращения к свойствам и методам внутри самого класса используются ключевые слова self, parent и static.

class OtherClass extends MyClass {
    public static $my_static = 'статическая переменная';

    public static function doubleColon() {
        echo parent::CONST_VALUE;
        echo self::$my_static;
    }
}

$classname = 'OtherClass';
$classname::doubleColon();

OtherClass::doubleColon();

- Ключевое слово static

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

В целях совместимости с PHP 4, если определение области видимости не используется, то свойство или метод будут обрабатываться так, как если бы он был объявлен как public.

Статические методы

Так как статические методы вызываются без создания экземпляра класса, то псевдопеременная $this недоступна внутри метода, объявленного как статический.

В PHP 7 вызов нестатических методов статически объявлен устаревшим и вызовет ошибку уровня E_DEPRECATED. Поддержка вызова нестатических методов статически может быть удалена в будущем.

class Foo {
    public static function aStaticMethod() {
        // ...
    }
}

Foo::aStaticMethod();

$classname = 'Foo';
$classname::aStaticMethod();

Статические свойства

Статические свойства не могут быть доступны через объект с помощью оператора "->".

Как и любая другая статическая переменная PHP, статические свойства могут инициализироваться только используя литерал или константу до PHP 5.6; выражения не допускается. В PHP 5.6 и более новых версиях применяются те же правила, что и для выражений const: возможны некоторые выражения, если они могут быть вычислены во время компиляциии.

class Foo {
    public static $my_static = 'foo';
	
    public function staticValue() {
        return self::$my_static;
    }
}

class Bar extends Foo {
    public function fooStatic() {
        return parent::$my_static;
    }
}

print Foo::$my_static;
// foo

$foo = new Foo();

print $foo->staticValue();
// foo

print $foo->my_static;
// Accessing static property Foo::$my_static as non static
// Undefined property: Foo::$my_static

print $foo::$my_static;
// foo

$classname = 'Foo';
print $classname::$my_static;
// foo

print Bar::$my_static;
// foo

$bar = new Bar();
print $bar->fooStatic();
// foo

- Абстрактные классы

PHP поддерживает определение абстрактных классов и методов. Объекты классов, определенные, как абстрактные, не могут быть созданы, и любой класс, который содержит по крайней мере один абстрактный метод, должен быть определен, как абстрактный. Методы, объявленные абстрактными, несут, по существу, лишь описательный смысл и не могут включать реализацию.

При наследовании от абстрактного класса, все методы, помеченные абстрактными в родительском классе, должны быть определены в дочернем классе; кроме того, область видимости этих методов должна совпадать (или быть менее строгой). Например, если абстрактный метод объявлен как protected, то реализация этого метода должна быть protected или public, но не private. Более того, объявления методов должны совпадать, то есть контроль типов (type hint) и количество обязательных аргументов должно быть одинаковым. К примеру, если в дочернем классе указан необязательный параметр, которого нет в объявлении абстрактного класса, то в данном случае конфликта объявлений методов не будет.

abstract class AbstractClass {
	/* Данный метод должен быть определён в дочернем классе */
    abstract protected function getValue();
    abstract protected function prefixValue($prefix);
	
	/* Общий метод */
    public function printOut() {
        print $this->getValue();
    }
}

class ConcreteClass1 extends AbstractClass {
    protected function getValue() {
        return "ConcreteClass1";
    }
	
    public function prefixValue($prefix) {
        return "{$prefix}ConcreteClass1";
    }
}

class ConcreteClass2 extends AbstractClass {
    public function getValue() {
        return "ConcreteClass2";
    }
	
    public function prefixValue($prefix) {
        return "{$prefix}ConcreteClass2";
    }
}

$class1 = new ConcreteClass1;
$class1->printOut(); // ConcreteClass1
echo $class1->prefixValue('FOO_'); // FOO_ConcreteClass1

$class2 = new ConcreteClass2;
$class2->printOut(); // ConcreteClass2
echo $class2->prefixValue('FOO_'); // FOO_ConcreteClass2

- Интерфейсы объектов

Интерфейсы объектов позволяют создавать код, который указывает, какие методы должен реализовать класс, без необходимости определять, как именно они должны быть реализованы.

Интерфейсы объявляются так же, как и обычные классы, но с использованием ключевого слова interface вместо class. Тела методов интерфейсов должны быть пустыми.

Все методы, определенные в интерфейсах должны быть общедоступными, что следует из самой природы интерфейса.

Обратите внимание, что возможно объявить конструктор в интерфейсе, который может быть полезен для некоторых задач, например, при реализации фабрик.

- implements

Для реализации интерфейса используется оператор implements. Класс должен реализовать все методы, описанные в интерфейсе, иначе произойдет фатальная ошибка. При желании классы могут реализовывать более одного интерфейса, разделяя каждый интерфейс запятой.

- Константы

Интерфейсы могут содержать константы. Константы интерфейсов работают точно так же, как и константы классов, за исключением того, что они не могут быть переопределены наследующим классом или интерфейсом.

// Объявим интерфейс 'iTemplate'
interface iTemplate {
    public function setVariable($name, $var);
    public function getHtml($template);
}

// Реализация интерфейса
// Это будет работать
class Template implements iTemplate {
    private $vars = array();
	
    public function setVariable($name, $var) {
        $this->vars[$name] = $var;
    }
	
    public function getHtml($template) {
        foreach($this->vars as $name => $value) {
            $template = str_replace('{' . $name . '}', $value, $template);
        }
		
        return $template;
    }
}

// Это не будет работать
// Fatal error: Class BadTemplate contains 1 abstract methods
// and must therefore be declared abstract (iTemplate::getHtml)
// (Фатальная ошибка: Класс BadTemplate содержит 1 абстрактный метод
// и поэтому должен быть объявлен абстрактным (iTemplate::getHtml))
class BadTemplate implements iTemplate {
    private $vars = array();
	
    public function setVariable($name, $var) {
        $this->vars[$name] = $var;
    }
}

- Трейты

Начиная с версии 5.4.0, PHP реализует метод для повторного использования кода под названием трейт (trait).

Трейт - это механизм обеспечения повторного использования кода в языках с поддержкой только одиночного наследования, таких как PHP. Трейт предназначен для уменьшения некоторых ограничений одиночного наследования, позволяя разработчику повторно использовать наборы методов свободно, в нескольких независимых классах и реализованных с использованием разных архитектур построения классов. Семантика комбинации трейтов и классов определена таким образом, чтобы снизить уровень сложности, а также избежать типичных проблем, связанных с множественным наследованием и смешиванием (mixins).

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

trait ezcReflectionReturnInfo {
    function getReturnType() { /*1*/ }
    function getReturnDescription() { /*2*/ }
}

class ezcReflectionMethod extends ReflectionMethod {
    use ezcReflectionReturnInfo;
    /* ... */
}

class ezcReflectionFunction extends ReflectionFunction {
    use ezcReflectionReturnInfo;
    /* ... */
}

- Анонимные классы

Поддержка анонимных классов была добавлена в PHP 7. Анонимные классы полезны, когда нужно создать простые, одноразовые объекты.

// До PHP 7
class Logger {
    public function log($msg) {
        echo $msg;
    }
}

$util->setLogger(new Logger());

// С PHP 7+
$util->setLogger(new class {
    public function log($msg) {
        echo $msg;
    }
});

Они могут передавать аргументы в конструкторы, расширять другие классы, реализовывать интерфейсы и использовать трейты как обычный класс:

class SomeClass {}
interface SomeInterface {}
trait SomeTrait {}

var_dump(new class(10) extends SomeClass implements SomeInterface {
    private $num;
	
    public function __construct($num) {
        $this->num = $num;
    }

    use SomeTrait;
});

Результат выполнения данного примера:

object(class@anonymous)#1 (1) {
	["Command line code0x104c5b612":"class@anonymous":private]=>
	int(10)
}

Вложение анонимного класса в другой класс не дает ему доступ к закрытым или защищенным методам и свойствам этого внешнего класса. Для того, чтобы использовать защищенные свойства и методы внешнего класса, анонимный класс может расширить внешний класс. Чтобы использовать закрытые свойства внешнего класса в анонимном классе, их нужно передать в конструктор.


- Перегрузка

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

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

Все методы перегрузки должны быть объявлены как public.

Ни один из аргументов этих магических методов не может быть передан по ссылке.

Интерпретация перегрузки в PHP отличается от большинства объектно-ориентированных языков. Традиционно перегрузка означает возможность иметь несколько одноименных методов с разным количеством и типами аргументов.

- Перегрузка свойств

public __set ( string $name , mixed $value ) : void

public __get ( string $name ) : mixed

public __isset ( string $name ) : bool

public __unset ( string $name ) : void

Метод __set() будет выполнен при записи данных в недоступные (защищенные или приватные) или несуществующие свойства.

Метод __get() будет выполнен при чтении данных из недоступных (защищенных или приватных) или несуществующих свойств.

Метод __isset() будет выполнен при использовании isset() или empty() на недоступных (защищенных или приватных) или несуществующих свойствах.

Метод __unset() будет выполнен при вызове unset() на недоступном (защищенном или приватном) или несуществующем свойстве.

Аргумент $name представляет собой имя вызываемого свойства. Метод __set() содержит аргумент $value, представляющий собой значение, которое будет записано в свойство с именем $name.

Перегрузка свойств работает только в контексте объекта. Данные магические методы не будут вызваны в статическом контексте. Поэтому эти методы не должны объявляться статическими. Начиная с версии PHP 5.3.0, при объявлении любого магического метода как static будет выдано предупреждение.

- Перегрузка методов

public __call ( string $name , array $arguments ) : mixed

public static __callStatic ( string $name , array $arguments ) : mixed

__call() запускается при вызове недоступных методов в контексте объект.

__callStatic() запускается при вызове недоступных методов в статическом контексте.

Аргумент $name представляет собой имя вызываемого метода. Аргумент $arguments представляет собой нумерованный массив, содержащий параметры, переданные в вызываемый метод $name.


- Итераторы объектов

PHP 5 предоставляет такой способ объявления объектов, который дает возможность пройти по списку элементов данного объекта, например, с помощью оператора foreach. По умолчанию, в этом обходе (итерации) будут участвовать все видимые свойства объекта.

class MyClass {
    public $var1 = 'значение 1';
    public $var2 = 'значение 2';
    public $var3 = 'значение 3';
	
    protected $protected = 'защищенная переменная';
    private   $private   = 'закрытая переменная';
	
    function iterateVisible() {
		echo "MyClass::iterateVisible:";
		
		foreach ($this as $key => $value) {
			print "$key => $value";
		}
    }
}

$class = new MyClass();

foreach($class as $key => $value) {
    print "$key => $value";
}

/*
	var1 => значение 1
	var2 => значение 2
	var3 => значение 3
*/

$class->iterateVisible();

/*
	MyClass::iterateVisible:
	
	var1 => значение 1
	var2 => значение 2
	var3 => значение 3
	
	protected => защищенная переменная
	private => закрытая переменная
*/

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

Кроме того, вы можете развить эту концепцию и реализовать встроенный в PHP 5 интерфейс Iterator. Это позволит самому объекту решать как он будет итерироваться и какие данные будут доступны на каждой итерации.


- Ключевое слово final

PHP 5 предоставляет ключевое слово final, разместив которое перед объявлениями методов класса, можно предотвратить их переопределение в дочерних классах. Если же сам класс определяется с этим ключевым словом, то он не сможет быть унаследован.

class BaseClass {
	public function test() {
		echo "Вызван метод BaseClass::test()";
	}
	
	final public function moreTesting() {
		echo "Вызван метод BaseClass::moreTesting()";
	}
}

class ChildClass extends BaseClass {
	public function moreTesting() {
		echo "Вызван метод ChildClass::moreTesting()";
	}
}

// Выполнение заканчивается фатальной ошибкой: Cannot override final method BaseClass::moreTesting()
// (Метод BaseClass::moretesting() не может быть переопределён)

Свойства и константы не могут быть объявлены финальными, только классы и методы.


- Объекты и ссылки

Одним из ключевых моментов объектно-ориентированной парадигмы PHP 5, который часто обсуждается, является "передача объектов по ссылке по умолчанию". Это не совсем верно. Этот раздел уточняет это понятие используя некоторые примеры.

Ссылка в PHP - это псевдоним (алиас), который позволяет присвоить двум переменным одинаковое значение. Начиная с PHP 5 объектная переменная больше не содержит сам объект как значение. Такая переменная содержит только идентификатор объекта, который позволяет найти конкретный объект при обращении к нему. Когда объект передается как аргумент функции, возвращается или присваивается другой переменной, то эти разные переменные не являются псевдонимами (алиасами): они содержат копию идентификатора, который указывает на один и тот же объект.

class A {
	public $foo = 1;
}

$a = new A;
$b = $a;

// $a и $b копии одного идентификатора
// ($a) = ($b) = <id>

$b->foo = 2;
echo $a->foo; // 2

$c = new A;
$d = &$c;

// $c и $d ссылки
// ($c,$d) = <id>

$d->foo = 2;
echo $c->foo; // 2

$e = new A;

function foo($obj) {
    // ($obj) = ($e) = <id>
    $obj->foo = 2;
}

foo($e);

echo $e->foo; // 2

- Магические методы

Имена функций __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone() и __debugInfo() являются магическими в PHP. Не стоит называть свои методы этими именами, если вы не хотите использовать их магическую функциональность.

Все магические методы ДОЛЖНЫ быть объявлены как public.

PHP оставляет за собой право все методы, начинающиеся с __, считать магическими. Не рекомендуется использовать имена методов с __ в PHP, если вы не желаете использовать соответствующую магическую функциональность.

__sleep() и __wakeup()

public __sleep ( void ) : array

public __wakeup ( void ) : void

Функция serialize() проверяет, присутствует ли в классе метод с магическим именем __sleep(). Если это так, то этот метод выполняется до любой операции сериализации. Он может очистить объект и должен возвращать массив с именами всех переменных этого объекта, которые должны быть сериализованы. Если метод ничего не возвращает, то сериализуется NULL и выдается предупреждение E_NOTICE.

Предполагаемое использование __sleep() состоит в завершении работы над данными, ждущими обработки или других подобных задач очистки. Кроме того, этот метод может полезен, когда есть очень большие объекты, которые нет необходимости полностью сохранять.

С другой стороны, функция unserialize() проверяет наличие метода с магическим именем __wakeup(). Если она имеется, эта функция может восстанавливать любые ресурсы, которые может иметь объект.

Предполагаемое использование __wakeup() заключается в восстановлении любых соединений с базой данных, которые могли быть потеряны во время операции сериализации и выполнения других операций повторной инициализации.

class Connection {
    protected $link;
    private $dsn, $username, $password;
    
    public function __construct($dsn, $username, $password) {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->connect();
    }
	
    private function connect() {
        $this->link = new PDO($this->dsn, $this->username, $this->password);
    }
	
    public function __sleep() {
        return array('dsn', 'username', 'password');
    }
    
    public function __wakeup() {
        $this->connect();
    }
}

__serialize() и __unserialize()

public __serialize ( void ) : array

public __unserialize ( array $data ) : void

serialize() проверяет, есть ли в классе функция с магическим именем __serialize(). Если да, функция выполняется перед любой сериализацией. Она должна создать и вернуть ассоциативный массив пар ключ/значение, которые представляют сериализованную форму объекта. Если массив не возвращен, будет выдано TypeError.

Если и __serialize() и __sleep() определены в одном и том же объекте, будет вызван только метод __serialize(). __sleep() будет игнорироваться. Если объект реализует интерфейс Serializable, метод serialize() интерфейса будет игнорироваться, а вместо него будет использован __serialize().

Предполагаемое использование __serialize() для определения удобного для сериализации произвольного представления объекта. Элементы массива могут соответствовать свойствам объекта, но это не обязательно.

И наоборот, unserialize() проверяет наличие магической функции __unserialize(). Если функция присутствует, ей будет передан восстановленный массив, который был возвращен из __serialize(). Затем он может восстановить свойства объекта из этого массива соответствующим образом.

Если и __unserialize() и __wakeup() определены в одном и том же объекте, только будет вызван метод __unserialize(). __wakeup() будет игнорироваться.

class Connection {
    protected $link;
    private $dsn, $username, $password;
	
    public function __construct($dsn, $username, $password) {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->connect();
    }
	
    private function connect() {
        $this->link = new PDO($this->dsn, $this->username, $this->password);
    }

    public function __serialize(): array {
        return [
			'dsn' => $this->dsn,
			'user' => $this->username,
			'pass' => $this->password,
        ];
    }

    public function __unserialize(array $data): void {
		$this->dsn = $data['dsn'];
        $this->username = $data['user'];
        $this->password = $data['pass'];

        $this->connect();
    }
}

__toString()

public __toString ( void ) : string

Метод __toString() позволяет классу решать, как он должен реагировать при преобразовании в строку. Например, что вывести при выполнении echo $obj;. Этот метод должен возвращать строку, иначе произойдёт фатальная ошибка уровня E_RECOVERABLE_ERROR.

class TestClass {
    public $foo;
	
    public function __construct($foo) {
        $this->foo = $foo;
    }

    public function __toString() {
        return $this->foo;
    }
}

$class = new TestClass('Привет');

echo $class; // Привет

__invoke()

__invoke ([ $... ] ) : mixed

Метод __invoke() вызывается, когда скрипт пытается выполнить объект как функцию.

class CallableClass {
    public function __invoke($x) {
        var_dump($x);
    }
}

$obj = new CallableClass;
$obj(5);

var_dump(is_callable($obj));

// int(5) bool(true)

__set_state()

static __set_state ( array $properties ) : object

Этот статический метод вызывается для тех классов, которые экспортируются функцией var_export() начиная с PHP 5.1.0.

Единственный параметр этого метода является массив, содержащий экспортируемые свойства в виде array('property' => value, ...).

class A {
    public $var1;
    public $var2;

    public static function __set_state($an_array) {
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        
		return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';

eval('$b = ' . var_export($a, true) . ';');

// $b = A::__set_state(array(
//    'var1' => 5,
//    'var2' => 'foo',
// ));

var_dump($b);

// object(A)#2 (2) { ["var1"]=> int(5) ["var2"]=> string(3) "foo" }

__debugInfo()

__debugInfo ( void ) : array

Этот метод вызывается функцией var_dump(), когда необходимо вывести список свойств объекта. Если этот метод не определен, тогда будут выведены все свойства объекта c модификаторами public, protected и private.

class C {
    private $prop;

    public function __construct($val) {
        $this->prop = $val;
    }

    public function __debugInfo() {
        return [
            'propSquared' => $this->prop ** 2,
        ];
    }
}

var_dump(new C(42));

// object(C)#1 (1) { ["propSquared"]=> int(1764) }

- Клонирование объектов

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

$copy_of_object = clone $object;

При клонировании объекта, PHP выполняет неполную копию всех свойств объекта. Любые свойства, являющиеся ссылками на другие переменные, останутся ссылками.

__clone ( void ) : void

После завершения клонирования, если метод __clone() определен, тогда будет вызываться метод __clone() вновь созданного объекта, для возможного изменения всех необходимых свойств.

class SubObject {
    static $instances = 0;
    public $instance;

    public function __construct() {
        $this->instance = ++self::$instances;
    }

    public function __clone() {
        $this->instance = ++self::$instances;
    }
}

class MyCloneable {
    public $object1;
    public $object2;

    public function __clone() {
        // Принудительно копируем this->object, иначе
        // он будет указывать на один и тот же объект.
        $this->object1 = clone $this->object1;
    }
}

$obj = new MyCloneable();

$obj->object1 = new SubObject();
$obj->object2 = new SubObject();

$obj2 = clone $obj;

print("Оригинальный объект:");
print_r($obj);

print("Клонированный объект:");
print_r($obj2);

Результат выполнения данного примера:

Оригинальный объект:
MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 1
        )

    [object2] => SubObject Object
        (
            [instance] => 2
        )

)
Клонированный объект:
MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 3
        )

    [object2] => SubObject Object
        (
            [instance] => 2
        )

)

- Сравнение объектов

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

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

Следующий пример пояснит эти правила:

function bool2str($bool) {
    return (string) $bool;
}

function compareObjects(&$o1, &$o2) {
    echo 'o1 == o2 : ' . bool2str($o1 == $o2) . "\n";
    echo 'o1 != o2 : ' . bool2str($o1 != $o2) . "\n";
    echo 'o1 === o2 : ' . bool2str($o1 === $o2) . "\n";
    echo 'o1 !== o2 : ' . bool2str($o1 !== $o2) . "\n";
}

class Flag {
    public $flag;

    public function __construct($flag = true) {
        $this->flag = $flag;
    }
}

class OtherFlag {
    public $flag;

    function __construct($flag = true) {
        $this->flag = $flag;
    }
}

$o = new Flag();
$p = new Flag();
$q = $o;
$r = new OtherFlag();

echo "Два экземпляра одного и того же класса";
compareObjects($o, $p);

echo "Две ссылки на один и тот же экземпляр";
compareObjects($o, $q);

echo "Экземпляры двух разных классов";
compareObjects($o, $r);

Результат выполнения данного примера:

Два экземпляра одного и того же класса
o1 == o2 : TRUE
o1 != o2 : FALSE
o1 === o2 : FALSE
o1 !== o2 : TRUE

Две ссылки на один и тот же экземпляр
o1 == o2 : TRUE
o1 != o2 : FALSE
o1 === o2 : TRUE
o1 !== o2 : FALSE

Экземпляры двух разных классов
o1 == o2 : FALSE
o1 != o2 : TRUE
o1 === o2 : FALSE
o1 !== o2 : TRUE

- Позднее статическое связывание

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

Если говорить более точно, позднее статическое связывание сохраняет имя класса указанного в последнем "неперенаправленном вызове". В случае статических вызовов это явно указанный класс (обычно слева от оператора ::); в случае не статических вызовов это класс объекта. "Перенаправленный вызов" - это статический вызов, начинающийся с self::, parent::, static::, или, если двигаться вверх по иерархии классов, forward_static_call(). Функция get_called_class() может быть использована для получения строки с именем вызванного класса, а static:: представляет ее область действия.

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

- Ограничения self::

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

class A {
    public static function who() {
        echo __CLASS__;
    }
	
    public static function test() {
        self::who();
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test();
// A

- Использование позднего статического связывания

Позднее статическое связывание пытается устранить это ограничение, предоставляя ключевое слово, которое ссылается на класс, вызванный непосредственно в ходе выполнения. Попросту говоря, ключевое слово, которое позволит вам ссылаться на B из test() в предыдущем примере. Было решено не вводить новое ключевое слово, а использовать static, которое уже зарезервировано.

class A {
    public static function who() {
        echo __CLASS__;
    }
	
    public static function test() {
        static::who(); // Здесь действует позднее статическое связывание
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test();
// B

В нестатическом контексте вызванным классом будет тот, к которому относится экземпляр объекта. Поскольку $this-> будет пытаться вызывать закрытые методы из той же области действия, использование static:: может дать разные результаты. Другое отличие в том, что static:: может ссылаться только на статические поля класса.

Использование static:: в нестатическом контексте:

class A {
    private function foo() {
        echo "success!";
    }
	
    public function test() {
        $this->foo();
        static::foo();
    }
}

class B extends A {
	/*
		foo() будет скопирован в В,
		следовательно его область действия по прежнему А,
		и вызов будет успешным
	*/
}

class C extends A {
    private function foo() {
        /*
			исходный метод заменен;
			область действия нового метода - С
		*/
    }
}

$b = new B();
$b->test();
// success! success!

$c = new C();
$c->test();
// Fatal error: Uncaught Error: Call to private method C::foo() from context 'A'

- Ковариантность и Контравариантность

В PHP 7.2.0 была добавлена частичная противоположность путем устранения ограничений типа для параметров в дочернем методе. Начиная с PHP 7.4.0, добавлена полная поддержка ковариации и контравариантности.

Ковариантность позволяет дочернему методу возвращать более конкретный тип, чем тип возврата его родительского метода. Принимая во внимание, что противоречивость позволяет типу параметра быть менее специфичным в дочернем методе, чем его родительский метод.

- Ковариантность

Чтобы проиллюстрировать, как работает ковариация, создан простой абстрактный родительский класс Animal. Animal будет расширен за счет дочерних классов Cat и Dog:

abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    abstract public function speak();
}

class Dog extends Animal {
    public function speak() {
        echo $this->name . " лает";
    }
}

class Cat extends Animal  {
    public function speak() {
        echo $this->name . " мяукает";
    }
}

Обратите внимание, что в примере нет методов, которые возвращают значения. Будет добавлено несколько фабрик, которые возвращают новый объект типа класса Animal, Cat или Dog:

interface AnimalShelter {
    public function adopt(string $name): Animal;
}

class CatShelter implements AnimalShelter {
    // Возвращаем класс Cat вместо  Animal
	public function adopt(string $name): Cat {
        return new Cat($name);
    }
}

class DogShelter implements AnimalShelter {
    // Возвращаем класс Dog вместо  Animal
	public function adopt(string $name): Dog {
        return new Dog($name);
    }
}

$kitty = (new CatShelter)->adopt("Рыжик");
$kitty->speak();

$doggy = (new DogShelter)->adopt("Бобик");
$doggy->speak();

Результат выполнения данного примера:

Рыжик мяукает
Бобик лает

- Контравариантность

В продолжение предыдущего примера, где мы использовали классы Animal, Cat и Dog, мы введем новые классы Food и AnimalFood и добавим в абстрактный класс Animal новый метод eat(AnimalFood $food):

class Food {}

class AnimalFood extends Food {}

abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function eat(AnimalFood $food) {
        echo $this->name . " ест " . get_class($food);
    }
}

Чтобы увидеть суть контравариантности, мы переопределим метод eat класса Dog таким образом, чтобы он мог принимать любой объект класса Food. Класс Cat оставим без изменений:

class Dog extends Animal {
    public function eat(Food $food) {
        echo $this->name . " ест " . get_class($food);
    }
}

Следующий пример покажет поведение контравариантности:

$kitty = (new CatShelter)->adopt("Рыжик");
$catFood = new AnimalFood();
$kitty->eat($catFood);

$doggy = (new DogShelter)->adopt("Бобик");
$banana = new Food();
$doggy->eat($banana);

Результат выполнения данного примера:

Рыжик ест AnimalFood
Бобик ест Food

Но что случится, если $kitty попробует съесть (eat) банан ($banana)?

$kitty->eat($banana);

Результат выполнения данного примера:

Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given