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

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

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

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

class Car
{
	function beep()
	{
		echo 'Bip bip!';
	}
}

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

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

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

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

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

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

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

class Person
{
	private $age;

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

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

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

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

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

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

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

class Person
{
	private $age;

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

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

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

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

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

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

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

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

Пространства имен (или “namespaces” по-английски) позволяют нам организовывать и группировать связанные классы, функции и константы, избегая при этом конфликтов имен. Вы можете представить их как папки на компьютере, где каждая папка содержит файлы, относящиеся к определенному проекту или теме.

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

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

namespace Transport;

class Car
{
	function beep()
	{
		echo 'Bip bip!';
	}
}

Если вы хотите использовать класс 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 printInfo()
	{
		echo "Возраст: {$this->age} лет\n";
	}
}

class Student extends Person
{
	private $major;

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

	function printInfo()
	{
		parent::printInfo();
		echo "Специальность: {$this->major} \n";
	}
}

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

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

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

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

Важно понимать, что основной целью наследования не является предотвращение дублирования кода. Напротив, неправильное использование наследования может привести к сложному и трудно поддерживаемому коду. Если отношения “является” между классами не существует, вместо наследования следует рассмотреть композицию.

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

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

foreach ($persons as $person) {
	$person->printInfo();
}

Композиция

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

Рассмотрим пример. У нас есть класс Engine и класс Car. Вместо того чтобы говорить “Автомобиль является Двигателем”, мы говорим “Автомобиль имеет Двигатель”, что является типичным отношением композиции.

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

class Car
{
	private $engine;

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

	function start()
	{
		$this->engine->turnOn();
		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 = 'Публичное';
	protected $protectedProperty = 'Защищенное';
	private $privateProperty = 'Приватное';

	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
{
}

// Следующий код вызовет ошибку, так как нельзя наследовать от final класса.
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(): int
	{
		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 Guard
{
	function guardHouse();
}

class Dog implements Animal, Guard
{
	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 ListClass
{
	/** @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 Interfaces)

“Текучий интерфейс” (по-английски “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 Beeping
{
	public function beep()
	{
		echo 'Bip bip!';
	}
}

class Car
{
	use Beeping;
}

class Truck
{
	use Beeping;
}

$car = new Car;
$car->beep(); // Выведет 'Bip bip!'

$truck = new Truck;
$truck->beep(); // Также выведет 'Bip bip!'

В этом примере у нас есть трейт Beeping, который содержит один метод beep(). Затем у нас есть два класса: Car и Truck, которые оба используют трейт Beeping. Благодаря этому оба класса “имеют” метод beep(), и мы можем вызывать его на объектах обоих классов.

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

Исключения

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

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

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

function divide(float $a, float $b): float
{
	if ($b === 0.0) { // Сравнение с float
		throw new Exception('Деление на ноль!');
	}
	return $a / $b;
}

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

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

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

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

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

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

class BankException extends Exception {}
class InsufficientFundsException extends BankException {}
class LimitExceededException extends BankException {}

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 LimitExceededException('Превышен дневной лимит снятия средств.');
		}

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

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

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

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

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

Итерация

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

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

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

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

class ListClass implements IteratorAggregate
{
	private array $persons = [];

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

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

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

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

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

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

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