Nette Documentation Preview

syntax
Введение в объектно-ориентированное программирование
****************************************************

.[perex]
Сокращение "ООП" означает объектно-ориентированное программирование - способ организовать и построить код. ООП позволяет смотреть на программу как на набор объектов, которые общаются между собой, а не как на последовательность команд и функций.

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

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

```php
class Car
{
	function honk()
	{
		echo 'Бип-бип!';
	}
}
```

В этом примере мы создали класс с именем `Car` с одной функцией (или "методом") под названием `honk`.

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

Классы обычно хранятся в отдельных файлах, чтобы код был упорядочен и в нём было легко ориентироваться. Имя файла должно совпадать с именем класса, так что для класса `Car` именем файла будет `Car.php`.

При именовании классов хорошо придерживаться соглашения "PascalCase": каждое слово в имени начинается с прописной буквы, и между ними нет подчёркиваний или других разделителей. Методы и свойства следуют соглашению "camelCase", то есть начинаются со строчной буквы.

У некоторых методов в PHP особая роль, и они снабжены приставкой `__` (два подчёркивания). Один из важнейших особых методов - "конструктор", обозначаемый как `__construct`. Конструктор - это метод, который вызывается автоматически при создании нового экземпляра класса.

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

Посмотрим, как использовать конструктор в PHP:

```php
class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Выведет: 25
```

В этом примере у класса `Person` есть свойство (переменная) `$age` и конструктор, который это свойство задаёт. Метод `howOldAreYou()` затем даёт доступ к возрасту человека.

Псевдопеременная `$this` используется внутри класса для доступа к свойствам и методам объекта.

Ключевое слово `new` служит для создания нового экземпляра класса. В примере выше мы создали нового человека в возрасте 25 лет.

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

```php
class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // если аргумент не передаётся, скобки можно опустить
echo $person->howOldAreYou(); // Выведет: 20
```

В этом примере, если при создании объекта `Person` вы возраст не укажете, будет использовано значение по умолчанию 20.

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

```php
class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}
```

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


Пространства имён
-----------------

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

Пространства имён особенно полезны в более крупных проектах или при использовании сторонних библиотек, где могут возникнуть конфликты имён классов.

Представьте, что у вас в проекте есть класс с именем `Car` и вы хотите поместить его в пространство имён `Transport`. Сделали бы вы это так:

```php
namespace Transport;

class Car
{
	function honk()
	{
		echo 'Бип-бип!';
	}
}
```

Если вы хотите использовать класс `Car` в другом файле, вам нужно указать, из какого пространства имён этот класс происходит:

```php
$car = new Transport\Car;
```

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

```php
use Transport\Car;

$car = new Car;
```


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

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

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

В PHP наследование реализуется ключевым словом `extends`.

Наш класс `Person` хранит сведения о возрасте. У нас может быть другой класс, `Student`, который расширяет `Person` и добавляет сведения о направлении обучения.

Посмотрим на пример:

```php
class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Возраст: {$this->age} лет\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Направление обучения: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Информатика');
$student->printInformation();
```

Как этот код работает?

- Мы использовали ключевое слово `extends`, чтобы расширить класс `Person`, то есть класс `Student` наследует все методы и свойства от `Person`.

- Ключевое слово `parent::` позволяет вызывать методы родительского класса. В данном случае мы вызвали конструктор из класса `Person` перед тем, как добавить в класс `Student` собственную функциональность. И точно так же метод предка `printInformation()` перед выводом сведений о студенте.

Наследование предназначено для ситуаций, когда между классами есть отношение "является". Например, `Student` является `Person`. Кошка является животным. Оно позволяет нам в местах, где код ожидает один объект (например, "Person"), использовать вместо него производный объект (например, "Student").

Принципиально важно осознавать, что главное назначение наследования **не** в том, чтобы предотвращать дублирование кода. Наоборот, злоупотребление наследованием может привести к сложному и трудному в сопровождении коду. Если между классами нет отношения "является", вместо наследования нам стоит подумать о композиции.

Обратите внимание, что методы `printInformation()` в классах `Person` и `Student` выводят немного разные сведения. И мы можем добавить другие классы (например, `Employee`), которые дадут другие реализации этого метода. Способность объектов разных классов по-разному откликаться на один и тот же метод называется полиморфизмом:

```php
$people = [
	new Person(30),
	new Student(20, 'Информатика'),
	new Employee(45, 'Директор'),
];

foreach ($people as $person) {
	$person->printInformation();
}
```


Композиция
----------

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

Например, у нас есть класс `Engine` и класс `Car`. Вместо "Автомобиль является двигателем" мы говорим "У автомобиля есть двигатель", а это типичное отношение композиции.

```php
class Engine
{
	function start()
	{
		echo 'Двигатель работает.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'Автомобиль готов к поездке!';
	}
}

$car = new Car;
$car->start();
```

Здесь у `Car` нет всех свойств и методов `Engine`, но у него есть доступ к нему через свойство `$engine`.

Преимущество композиции - большая гибкость проектирования и лучшая приспособляемость к будущим изменениям.


Видимость
---------

В PHP свойствам, методам и константам класса можно задать "видимость". Видимость определяет, откуда к этим элементам можно обращаться.

1. **Public:** если элемент помечен как `public`, это значит, что обращаться к нему можно откуда угодно, в том числе за пределами класса.

2. **Protected:** элемент, помеченный как `protected`, доступен только внутри класса и всех его потомков (классов, которые от него наследуются).

3. **Private:** если элемент `private`, обращаться к нему можно только изнутри класса, где он был определён.

Если видимость не указать, PHP автоматически задаст `public`.

Посмотрим на пример кода:

```php
class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Работает
		echo $this->protectedProperty;  // Работает
		echo $this->privateProperty;    // Работает
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Работает
// echo $object->protectedProperty;   // Выбросит ошибку
// echo $object->privateProperty;     // Выбросит ошибку
```

Продолжим наследованием класса:

```php
class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Работает
		echo $this->protectedProperty;  // Работает
		// echo $this->privateProperty;   // Выбросит ошибку
	}
}
```

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

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


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

В PHP мы можем использовать ключевое слово `final`, если хотим запретить наследование или переопределение класса, метода либо константы. Когда класс помечен как `final`, его нельзя расширить. Когда метод помечен как `final`, его нельзя переопределить в потомке.

Уверенность в том, что определённый класс или метод больше не будет изменяться, позволяет нам легче вносить изменения, не беспокоясь о возможных конфликтах. Например, мы можем добавить новый метод, не опасаясь, что у потомка уже есть метод с тем же именем и возникнет конфликт. Или можем изменить параметры метода, снова без риска вызвать несогласованность с переопределённым методом у потомка.

```php
final class FinalClass
{
}

// Следующий код выбросит ошибку, потому что от финального класса наследоваться нельзя.
class ChildOfFinalClass extends FinalClass
{
}
```

В этом примере попытка унаследоваться от финального класса `FinalClass` приведёт к ошибке.


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

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

Помните: поскольку статические элементы принадлежат классу, а не его экземплярам, внутри статических методов нельзя использовать псевдопеременную `$this`.

Использование статических свойств ведёт к [запутанному коду, полному подводных камней|dependency-injection:global-state], поэтому использовать их никогда не следует, и примера мы здесь не приведём. Зато статические методы полезны. Вот пример:

```php
class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Использование статического метода без создания экземпляра класса
echo Calculator::add(5, 3); // Вывод: 8
echo Calculator::subtract(5, 3); // Вывод: 2
```

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


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

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

```php
class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Вывод: 4
```

В этом примере у нас есть класс `Car` с константой `NumberOfWheels`. При обращении к константе внутри класса вместо имени класса можно использовать ключевое слово `self`.


Объектные интерфейсы
--------------------

Объектные интерфейсы выступают "договорами" для классов. Если класс должен реализовывать объектный интерфейс, он обязан содержать все методы, которые интерфейс определяет. Это отличный способ обеспечить, чтобы определённые классы придерживались одного и того же "договора" или строения.

В PHP интерфейсы определяются ключевым словом `interface`. Все методы, определённые в интерфейсе, публичные (`public`). Когда класс реализует интерфейс, он использует ключевое слово `implements`.

```php
interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Мяу';
	}
}

$cat = new Cat;
$cat->makeSound();
```

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

Класс может реализовывать несколько интерфейсов сразу, и этим он отличается от наследования, где класс может наследоваться только от одного класса:

```php
interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Гав';
	}

	public function guardHouse()
	{
		echo 'Собака прилежно охраняет дом';
	}
}
```


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

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

Для определения абстрактного класса мы используем ключевое слово `abstract`.

```php
abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'Это обычный метод';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'Это реализация абстрактного метода';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();
```

В этом примере у нас есть абстрактный класс с одним обычным и одним абстрактным методом. Затем есть класс `Child`, который наследуется от `AbstractClass` и предоставляет реализацию абстрактного метода.

Чем интерфейсы отличаются от абстрактных классов? Абстрактные классы могут содержать и абстрактные, и конкретные методы, тогда как интерфейсы только определяют, какие методы класс должен реализовать, но никакой реализации не дают. Класс может наследоваться только от одного абстрактного класса, но может реализовывать сколько угодно интерфейсов.


Проверка типов
--------------

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

Типы, с которыми мы можем столкнуться в PHP:

1. **Базовые типы**: сюда относятся `int` (целые числа), `float` (числа с плавающей точкой), `bool` (логические значения), `string` (строки), `array` (массивы) и `null`.
2. **Классы**: когда мы хотим, чтобы значение было экземпляром конкретного класса.
3. **Интерфейсы**: определяют набор методов, которые класс должен реализовать. Значение, отвечающее интерфейсу, должно иметь эти методы.
4. **Смешанные типы**: мы можем указать, что у переменной может быть несколько допустимых типов.
5. **Void**: этот особый тип обозначает, что функция или метод не возвращает никакого значения.

Посмотрим, как изменить код, чтобы включить типы:

```php
class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "Этому человеку {$this->age} лет.";
	}
}

/**
 * Функция, которая принимает объект Person и выводит возраст человека.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}
```

Так мы обеспечиваем, что наш код ожидает данные нужного типа и работает с ними, а это помогает предотвратить возможные ошибки.

Некоторые типы записать прямо в PHP нельзя. В этом случае их указывают в комментарии phpDoc - стандартном формате документирования PHP-кода, который начинается с `/**` и заканчивается `*/`. Он позволяет добавлять описания классов, методов и так далее. А также указывать сложные типы с помощью так называемых аннотаций `@var`, `@param` и `@return`. Эти типы затем используют инструменты статического анализа кода, но сам PHP их не проверяет.

```php
class Registry
{
	/** @var array<Person>  обозначает, что это массив объектов Person */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}
```


Сравнение и идентичность
------------------------

В PHP объекты можно сравнивать двумя способами:

1. Сравнение по значению `==`: проверяет, одного ли класса объекты и одинаковы ли значения в их свойствах.
2. Идентичность `===`: проверяет, тот ли это самый экземпляр объекта.

```php
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, потому что у них одинаковое значение
var_dump($car1 === $car2);  // false, потому что это не один и тот же экземпляр
var_dump($car1 === $car3);  // true, потому что $car3 - тот же экземпляр, что и $car1
```


Оператор `instanceof`
---------------------

Оператор `instanceof` позволяет выяснить, является ли данный объект экземпляром конкретного класса, потомком этого класса или реализует ли он определённый интерфейс.

Представим, что у нас есть класс `Person` и другой класс `Student`, который является потомком `Person`:

```php
class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Информатика');

// Проверяем, является ли $student экземпляром класса Student
var_dump($student instanceof Student);  // Вывод: bool(true)

// Проверяем, является ли $student экземпляром класса Person (потому что Student - потомок Person)
var_dump($student instanceof Person);   // Вывод: bool(true)
```

Из выводов видно, что объект `$student` считается экземпляром и класса `Student`, и класса `Person`.


Текучие интерфейсы
------------------

"Текучий интерфейс" (fluent interface) - приём в ООП, позволяющий объединять методы в цепочку в одном вызове. Это часто упрощает и проясняет код.

Ключевой элемент текучего интерфейса в том, что каждый метод в цепочке возвращает ссылку на текущий объект. Достигается это использованием `return $this;` в конце метода. Такой стиль программирования часто связывают с методами, называемыми "сеттерами", которые задают значения свойств объекта.

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

```php
public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Привет, это сообщение.')
		  ->send();
}
```

В этом примере методы `setFrom()`, `setRecipient()` и `setMessage()` служат для задания соответствующих значений (отправитель, получатель, содержимое сообщения). После задания каждого из этих значений методы возвращают текущий объект (`$email`), что позволяет нам присоединить следующий метод. Наконец, мы вызываем метод `send()`, который письмо действительно отправляет.

Благодаря текучим интерфейсам мы можем писать код, который интуитивно понятен и легко читается.


Копирование с помощью `clone`
-----------------------------

В PHP мы можем создать копию объекта оператором `clone`. Так мы получим новый экземпляр с идентичным содержимым.

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

```php
class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Клон ' . $this->name;
	}
}

$original = new Sheep('Долли');
echo $original->name . "\n";  // Выведет: Долли

$clone = clone $original;
echo $clone->name . "\n";     // Выведет: Клон Долли
```

В этом примере у нас есть класс `Sheep` с одним свойством `$name`. Когда мы клонируем экземпляр этого класса, метод `__clone()` обеспечивает, что имя клонированной овцы получает приставку "Клон".


Трейты
------

Трейты в PHP - инструмент, позволяющий делиться методами, свойствами и константами между классами и предотвращающий дублирование кода. Можно представлять их себе как механизм "копировать и вставить" (Ctrl-C и Ctrl-V), при котором содержимое трейта "вставляется" в классы. Это позволяет повторно использовать код без необходимости создавать сложные иерархии классов.

Посмотрим на простой пример использования трейтов в PHP:

```php
trait Honking
{
	public function honk()
	{
		echo 'Бип-бип!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Выведет 'Бип-бип!'

$truck = new Truck;
$truck->honk(); // Тоже выведет 'Бип-бип!'
```

В этом примере у нас есть трейт с именем `Honking`, который содержит один метод `honk()`. Затем у нас есть два класса, `Car` и `Truck`, оба они используют трейт `Honking`. В итоге оба класса "имеют" метод `honk()`, и мы можем вызвать его у объектов обоих классов.

Трейты позволяют легко и эффективно делиться кодом между классами. В иерархию наследования они не входят, то есть `$car instanceof Honking` вернёт `false`.


Исключения
----------

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

В PHP есть встроенный класс `Exception`, который служит основой для всех исключений. У него есть несколько методов, позволяющих получить больше сведений об исключении, например сообщение об ошибке, файл и строку, где ошибка произошла, и т. д.

Когда в коде возникает ошибка, мы можем "выбросить" исключение ключевым словом `throw`.

```php
function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Деление на ноль!');
	}
	return $a / $b;
}
```

Когда функция `division()` получит вторым аргументом ноль, она выбросит исключение с сообщением об ошибке `'Деление на ноль!'`. Чтобы программа при выбросе исключения не рухнула, мы перехватываем его в блоке `try/catch`:

```php
try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Перехвачено исключение: '. $e->getMessage();
}
```

Код, который может выбросить исключение, оборачивают в блок `try`. Если исключение выброшено, выполнение кода переходит в блок `catch`, где мы можем исключение обработать (например, вывести сообщение об ошибке).

После блоков `try` и `catch` можно добавить необязательный блок `finally`, который выполняется всегда, независимо от того, было исключение выброшено или нет (даже если мы используем в блоке `try` или `catch` `return`, `break` либо `continue`):

```php
try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Перехвачено исключение: '. $e->getMessage();
} finally {
	// Код, который выполняется всегда, независимо от того, было исключение выброшено или нет
}
```

Мы можем создавать и собственные классы исключений (их иерархию), наследующиеся от класса Exception. В качестве примера рассмотрим простое банковское приложение, позволяющее вносить и снимать деньги:

```php
class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('На счету недостаточно средств.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Превышен дневной лимит снятия.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}
```

Для одного блока `try` можно указать несколько блоков `catch`, если вы ожидаете разные типы исключений.

```php
$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'При операции произошла ошибка.';
}
```

В этом примере важно обратить внимание на порядок блоков `catch`. Поскольку все исключения наследуются от `BankingException`, если бы этот блок стоял первым, все исключения были бы перехвачены в нём и код до следующих блоков `catch` не дошёл бы. Поэтому важно, чтобы более конкретные исключения (то есть те, которые наследуются от других) стояли в порядке блоков `catch` выше своих родительских исключений.


Обход
-----

В PHP объекты можно обходить циклом `foreach`, во многом так же, как вы обходите массив. Чтобы это работало, объект должен реализовать особый интерфейс.

Первая возможность - реализовать интерфейс `Iterator`, у которого есть методы `current()`, возвращающий текущее значение, `key()`, возвращающий ключ, `next()`, переходящий к следующему значению, `rewind()`, переходящий в начало, и `valid()`, проверяющий, не дошли ли мы до конца.

Вторая возможность - реализовать интерфейс `IteratorAggregate`, у которого есть только один метод `getIterator()`. Он либо возвращает объект-заместитель, который обеспечит обход, либо может быть генератором - особой функцией, которая с помощью `yield` последовательно возвращает ключи и значения:

```php
class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

	public function getIterator(): Generator
	{
		foreach ($this->people as $person) {
			yield $person;
		}
	}
}

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Возраст: {$person->age} лет\n";
}
```


Лучшие практики
---------------

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

1) **Разделение ответственности**: у каждого класса должна быть чётко определённая ответственность, и он должен решать только одну основную задачу. Если класс делает слишком много вещей, возможно, стоит разделить его на меньшие, специализированные классы.
2) **Инкапсуляция**: данные и методы должны быть как можно более скрыты и доступны только через определённый интерфейс. Это позволяет менять внутреннюю реализацию класса, не затрагивая остальной код.
3) **Внедрение зависимостей**: вместо того чтобы создавать зависимости прямо внутри класса, вам следует "внедрять" их снаружи. Для более глубокого понимания этого принципа мы рекомендуем [главы о Dependency Injection|dependency-injection:introduction].

Введение в объектно-ориентированное программирование

Сокращение „ООП“ означает объектно-ориентированное программирование – способ организовать и построить код. ООП позволяет смотреть на программу как на набор объектов, которые общаются между собой, а не как на последовательность команд и функций.

В ООП „объект“ – это единица, содержащая данные и функции, которые с этими данными работают. Объекты создаются по „классам“, которые можно понимать как чертежи или шаблоны объектов. Когда у нас есть класс, мы можем создать его „экземпляр“ – конкретный объект, сделанный по этому классу.

Посмотрим, как создать простой класс в PHP. При определении класса мы используем ключевое слово „class“, за которым следует имя класса, а затем фигурные скобки, охватывающие функции класса (их называют „методами“) и переменные класса (их называют „свойствами“ или „атрибутами“):

class Car
{
	function honk()
	{
		echo 'Бип-бип!';
	}
}

В этом примере мы создали класс с именем Car с одной функцией (или „методом“) под названием honk.

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

Классы обычно хранятся в отдельных файлах, чтобы код был упорядочен и в нём было легко ориентироваться. Имя файла должно совпадать с именем класса, так что для класса Car именем файла будет Car.php.

При именовании классов хорошо придерживаться соглашения „PascalCase“: каждое слово в имени начинается с прописной буквы, и между ними нет подчёркиваний или других разделителей. Методы и свойства следуют соглашению „camelCase“, то есть начинаются со строчной буквы.

У некоторых методов в PHP особая роль, и они снабжены приставкой __ (два подчёркивания). Один из важнейших особых методов – „конструктор“, обозначаемый как __construct. Конструктор – это метод, который вызывается автоматически при создании нового экземпляра класса.

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

Посмотрим, как использовать конструктор в PHP:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Выведет: 25

В этом примере у класса Person есть свойство (переменная) $age и конструктор, который это свойство задаёт. Метод howOldAreYou() затем даёт доступ к возрасту человека.

Псевдопеременная $this используется внутри класса для доступа к свойствам и методам объекта.

Ключевое слово new служит для создания нового экземпляра класса. В примере выше мы создали нового человека в возрасте 25 лет.

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

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // если аргумент не передаётся, скобки можно опустить
echo $person->howOldAreYou(); // Выведет: 20

В этом примере, если при создании объекта Person вы возраст не укажете, будет использовано значение по умолчанию 20.

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

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

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

Пространства имён

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

Пространства имён особенно полезны в более крупных проектах или при использовании сторонних библиотек, где могут возникнуть конфликты имён классов.

Представьте, что у вас в проекте есть класс с именем Car и вы хотите поместить его в пространство имён Transport. Сделали бы вы это так:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Бип-бип!';
	}
}

Если вы хотите использовать класс Car в другом файле, вам нужно указать, из какого пространства имён этот класс происходит:

$car = new Transport\Car;

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

use Transport\Car;

$car = new Car;

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

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

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

В PHP наследование реализуется ключевым словом extends.

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

Посмотрим на пример:

class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Возраст: {$this->age} лет\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Направление обучения: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Информатика');
$student->printInformation();

Как этот код работает?

  • Мы использовали ключевое слово extends, чтобы расширить класс Person, то есть класс Student наследует все методы и свойства от Person.
  • Ключевое слово parent:: позволяет вызывать методы родительского класса. В данном случае мы вызвали конструктор из класса Person перед тем, как добавить в класс Student собственную функциональность. И точно так же метод предка printInformation() перед выводом сведений о студенте.

Наследование предназначено для ситуаций, когда между классами есть отношение „является“. Например, Student является Person. Кошка является животным. Оно позволяет нам в местах, где код ожидает один объект (например, „Person“), использовать вместо него производный объект (например, „Student“).

Принципиально важно осознавать, что главное назначение наследования не в том, чтобы предотвращать дублирование кода. Наоборот, злоупотребление наследованием может привести к сложному и трудному в сопровождении коду. Если между классами нет отношения „является“, вместо наследования нам стоит подумать о композиции.

Обратите внимание, что методы printInformation() в классах Person и Student выводят немного разные сведения. И мы можем добавить другие классы (например, Employee), которые дадут другие реализации этого метода. Способность объектов разных классов по-разному откликаться на один и тот же метод называется полиморфизмом:

$people = [
	new Person(30),
	new Student(20, 'Информатика'),
	new Employee(45, 'Директор'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Композиция

Композиция – приём, при котором вместо наследования свойств и методов другого класса мы просто используем в своём классе его экземпляр. Это позволяет сочетать функциональность и свойства нескольких классов, не создавая сложных структур наследования.

Например, у нас есть класс Engine и класс Car. Вместо „Автомобиль является двигателем“ мы говорим „У автомобиля есть двигатель“, а это типичное отношение композиции.

class Engine
{
	function start()
	{
		echo 'Двигатель работает.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'Автомобиль готов к поездке!';
	}
}

$car = new Car;
$car->start();

Здесь у Car нет всех свойств и методов Engine, но у него есть доступ к нему через свойство $engine.

Преимущество композиции – большая гибкость проектирования и лучшая приспособляемость к будущим изменениям.

Видимость

В PHP свойствам, методам и константам класса можно задать „видимость“. Видимость определяет, откуда к этим элементам можно обращаться.

  1. Public: если элемент помечен как public, это значит, что обращаться к нему можно откуда угодно, в том числе за пределами класса.
  2. Protected: элемент, помеченный как protected, доступен только внутри класса и всех его потомков (классов, которые от него наследуются).
  3. Private: если элемент private, обращаться к нему можно только изнутри класса, где он был определён.

Если видимость не указать, PHP автоматически задаст public.

Посмотрим на пример кода:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Работает
		echo $this->protectedProperty;  // Работает
		echo $this->privateProperty;    // Работает
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Работает
// echo $object->protectedProperty;   // Выбросит ошибку
// echo $object->privateProperty;     // Выбросит ошибку

Продолжим наследованием класса:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Работает
		echo $this->protectedProperty;  // Работает
		// echo $this->privateProperty;   // Выбросит ошибку
	}
}

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

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

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

В PHP мы можем использовать ключевое слово final, если хотим запретить наследование или переопределение класса, метода либо константы. Когда класс помечен как final, его нельзя расширить. Когда метод помечен как final, его нельзя переопределить в потомке.

Уверенность в том, что определённый класс или метод больше не будет изменяться, позволяет нам легче вносить изменения, не беспокоясь о возможных конфликтах. Например, мы можем добавить новый метод, не опасаясь, что у потомка уже есть метод с тем же именем и возникнет конфликт. Или можем изменить параметры метода, снова без риска вызвать несогласованность с переопределённым методом у потомка.

final class FinalClass
{
}

// Следующий код выбросит ошибку, потому что от финального класса наследоваться нельзя.
class ChildOfFinalClass extends FinalClass
{
}

В этом примере попытка унаследоваться от финального класса FinalClass приведёт к ошибке.

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

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

Помните: поскольку статические элементы принадлежат классу, а не его экземплярам, внутри статических методов нельзя использовать псевдопеременную $this.

Использование статических свойств ведёт к запутанному коду, полному подводных камней, поэтому использовать их никогда не следует, и примера мы здесь не приведём. Зато статические методы полезны. Вот пример:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Использование статического метода без создания экземпляра класса
echo Calculator::add(5, 3); // Вывод: 8
echo Calculator::subtract(5, 3); // Вывод: 2

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

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

Внутри классов у нас есть возможность определять константы. Константы – это значения, которые за время выполнения программы никогда не меняются. В отличие от переменных, значение константы остаётся тем же.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Вывод: 4

В этом примере у нас есть класс Car с константой NumberOfWheels. При обращении к константе внутри класса вместо имени класса можно использовать ключевое слово self.

Объектные интерфейсы

Объектные интерфейсы выступают „договорами“ для классов. Если класс должен реализовывать объектный интерфейс, он обязан содержать все методы, которые интерфейс определяет. Это отличный способ обеспечить, чтобы определённые классы придерживались одного и того же „договора“ или строения.

В PHP интерфейсы определяются ключевым словом interface. Все методы, определённые в интерфейсе, публичные (public). Когда класс реализует интерфейс, он использует ключевое слово implements.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Мяу';
	}
}

$cat = new Cat;
$cat->makeSound();

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

Класс может реализовывать несколько интерфейсов сразу, и этим он отличается от наследования, где класс может наследоваться только от одного класса:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Гав';
	}

	public function guardHouse()
	{
		echo 'Собака прилежно охраняет дом';
	}
}

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

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

Для определения абстрактного класса мы используем ключевое слово abstract.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'Это обычный метод';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'Это реализация абстрактного метода';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

В этом примере у нас есть абстрактный класс с одним обычным и одним абстрактным методом. Затем есть класс Child, который наследуется от AbstractClass и предоставляет реализацию абстрактного метода.

Чем интерфейсы отличаются от абстрактных классов? Абстрактные классы могут содержать и абстрактные, и конкретные методы, тогда как интерфейсы только определяют, какие методы класс должен реализовать, но никакой реализации не дают. Класс может наследоваться только от одного абстрактного класса, но может реализовывать сколько угодно интерфейсов.

Проверка типов

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

Типы, с которыми мы можем столкнуться в PHP:

  1. Базовые типы: сюда относятся int (целые числа), float (числа с плавающей точкой), bool (логические значения), string (строки), array (массивы) и null.
  2. Классы: когда мы хотим, чтобы значение было экземпляром конкретного класса.
  3. Интерфейсы: определяют набор методов, которые класс должен реализовать. Значение, отвечающее интерфейсу, должно иметь эти методы.
  4. Смешанные типы: мы можем указать, что у переменной может быть несколько допустимых типов.
  5. Void: этот особый тип обозначает, что функция или метод не возвращает никакого значения.

Посмотрим, как изменить код, чтобы включить типы:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "Этому человеку {$this->age} лет.";
	}
}

/**
 * Функция, которая принимает объект Person и выводит возраст человека.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Так мы обеспечиваем, что наш код ожидает данные нужного типа и работает с ними, а это помогает предотвратить возможные ошибки.

Некоторые типы записать прямо в PHP нельзя. В этом случае их указывают в комментарии phpDoc – стандартном формате документирования PHP-кода, который начинается с /** и заканчивается */. Он позволяет добавлять описания классов, методов и так далее. А также указывать сложные типы с помощью так называемых аннотаций @var, @param и @return. Эти типы затем используют инструменты статического анализа кода, но сам PHP их не проверяет.

class Registry
{
	/** @var array<Person>  обозначает, что это массив объектов Person */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Сравнение и идентичность

В PHP объекты можно сравнивать двумя способами:

  1. Сравнение по значению ==: проверяет, одного ли класса объекты и одинаковы ли значения в их свойствах.
  2. Идентичность ===: проверяет, тот ли это самый экземпляр объекта.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, потому что у них одинаковое значение
var_dump($car1 === $car2);  // false, потому что это не один и тот же экземпляр
var_dump($car1 === $car3);  // true, потому что $car3 - тот же экземпляр, что и $car1

Оператор instanceof

Оператор instanceof позволяет выяснить, является ли данный объект экземпляром конкретного класса, потомком этого класса или реализует ли он определённый интерфейс.

Представим, что у нас есть класс Person и другой класс Student, который является потомком Person:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Информатика');

// Проверяем, является ли $student экземпляром класса Student
var_dump($student instanceof Student);  // Вывод: bool(true)

// Проверяем, является ли $student экземпляром класса Person (потому что Student - потомок Person)
var_dump($student instanceof Person);   // Вывод: bool(true)

Из выводов видно, что объект $student считается экземпляром и класса Student, и класса Person.

Текучие интерфейсы

„Текучий интерфейс“ (fluent interface) – приём в ООП, позволяющий объединять методы в цепочку в одном вызове. Это часто упрощает и проясняет код.

Ключевой элемент текучего интерфейса в том, что каждый метод в цепочке возвращает ссылку на текущий объект. Достигается это использованием return $this; в конце метода. Такой стиль программирования часто связывают с методами, называемыми „сеттерами“, которые задают значения свойств объекта.

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

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Привет, это сообщение.')
		  ->send();
}

В этом примере методы setFrom(), setRecipient() и setMessage() служат для задания соответствующих значений (отправитель, получатель, содержимое сообщения). После задания каждого из этих значений методы возвращают текущий объект ($email), что позволяет нам присоединить следующий метод. Наконец, мы вызываем метод send(), который письмо действительно отправляет.

Благодаря текучим интерфейсам мы можем писать код, который интуитивно понятен и легко читается.

Копирование с помощью clone

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

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

class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Клон ' . $this->name;
	}
}

$original = new Sheep('Долли');
echo $original->name . "\n";  // Выведет: Долли

$clone = clone $original;
echo $clone->name . "\n";     // Выведет: Клон Долли

В этом примере у нас есть класс Sheep с одним свойством $name. Когда мы клонируем экземпляр этого класса, метод __clone() обеспечивает, что имя клонированной овцы получает приставку „Клон“.

Трейты

Трейты в PHP – инструмент, позволяющий делиться методами, свойствами и константами между классами и предотвращающий дублирование кода. Можно представлять их себе как механизм „копировать и вставить“ (Ctrl-C и Ctrl-V), при котором содержимое трейта „вставляется“ в классы. Это позволяет повторно использовать код без необходимости создавать сложные иерархии классов.

Посмотрим на простой пример использования трейтов в PHP:

trait Honking
{
	public function honk()
	{
		echo 'Бип-бип!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Выведет 'Бип-бип!'

$truck = new Truck;
$truck->honk(); // Тоже выведет 'Бип-бип!'

В этом примере у нас есть трейт с именем Honking, который содержит один метод honk(). Затем у нас есть два класса, Car и Truck, оба они используют трейт Honking. В итоге оба класса „имеют“ метод honk(), и мы можем вызвать его у объектов обоих классов.

Трейты позволяют легко и эффективно делиться кодом между классами. В иерархию наследования они не входят, то есть $car instanceof Honking вернёт false.

Исключения

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

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

Когда в коде возникает ошибка, мы можем „выбросить“ исключение ключевым словом throw.

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Деление на ноль!');
	}
	return $a / $b;
}

Когда функция division() получит вторым аргументом ноль, она выбросит исключение с сообщением об ошибке 'Деление на ноль!'. Чтобы программа при выбросе исключения не рухнула, мы перехватываем его в блоке try/catch:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Перехвачено исключение: '. $e->getMessage();
}

Код, который может выбросить исключение, оборачивают в блок try. Если исключение выброшено, выполнение кода переходит в блок catch, где мы можем исключение обработать (например, вывести сообщение об ошибке).

После блоков try и catch можно добавить необязательный блок finally, который выполняется всегда, независимо от того, было исключение выброшено или нет (даже если мы используем в блоке try или catch return, break либо continue):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Перехвачено исключение: '. $e->getMessage();
} finally {
	// Код, который выполняется всегда, независимо от того, было исключение выброшено или нет
}

Мы можем создавать и собственные классы исключений (их иерархию), наследующиеся от класса Exception. В качестве примера рассмотрим простое банковское приложение, позволяющее вносить и снимать деньги:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('На счету недостаточно средств.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Превышен дневной лимит снятия.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

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

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'При операции произошла ошибка.';
}

В этом примере важно обратить внимание на порядок блоков catch. Поскольку все исключения наследуются от BankingException, если бы этот блок стоял первым, все исключения были бы перехвачены в нём и код до следующих блоков catch не дошёл бы. Поэтому важно, чтобы более конкретные исключения (то есть те, которые наследуются от других) стояли в порядке блоков catch выше своих родительских исключений.

Обход

В PHP объекты можно обходить циклом foreach, во многом так же, как вы обходите массив. Чтобы это работало, объект должен реализовать особый интерфейс.

Первая возможность – реализовать интерфейс Iterator, у которого есть методы current(), возвращающий текущее значение, key(), возвращающий ключ, next(), переходящий к следующему значению, rewind(), переходящий в начало, и valid(), проверяющий, не дошли ли мы до конца.

Вторая возможность – реализовать интерфейс IteratorAggregate, у которого есть только один метод getIterator(). Он либо возвращает объект-заместитель, который обеспечит обход, либо может быть генератором – особой функцией, которая с помощью yield последовательно возвращает ключи и значения:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

	public function getIterator(): Generator
	{
		foreach ($this->people as $person) {
			yield $person;
		}
	}
}

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Возраст: {$person->age} лет\n";
}

Лучшие практики

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

  1. Разделение ответственности: у каждого класса должна быть чётко определённая ответственность, и он должен решать только одну основную задачу. Если класс делает слишком много вещей, возможно, стоит разделить его на меньшие, специализированные классы.
  2. Инкапсуляция: данные и методы должны быть как можно более скрыты и доступны только через определённый интерфейс. Это позволяет менять внутреннюю реализацию класса, не затрагивая остальной код.
  3. Внедрение зависимостей: вместо того чтобы создавать зависимости прямо внутри класса, вам следует „внедрять“ их снаружи. Для более глубокого понимания этого принципа мы рекомендуем главы о Dependency Injection.