Работа с составными типами в PHP
Работа с составными типами в PHP
PHP — язык с динамической типизацией, но при этом он поддерживает строгую типизацию на уровне объявления функций, свойств классов и переменных. Одной из ключевых особенностей языка является богатая система составных типов — то есть типов, которые объединяют в себе несколько значений или другие типы данных. Эти типы позволяют моделировать сложные структуры: от простых списков до полноценных объектов предметной области.
Составные типы в PHP — это не просто удобство, а фундаментальный инструмент проектирования программ. Они обеспечивают читаемость кода, предсказуемость поведения и возможность эффективного управления данными.
Что такое составные типы
Составной тип — это тип данных, значение которого состоит из нескольких элементов. В отличие от скалярных типов (int, float, string, bool), составные типы могут содержать внутри себя другие значения, организованные определённым образом.
В PHP к составным типам относятся:
array— упорядоченная коллекция пар «ключ–значение»;object— экземпляр класса, содержащий свойства и методы;callable— ссылка на функцию или метод, который можно вызвать;iterable— обобщённый тип, описывающий всё, по чему можно итерироваться (arrayили объект, реализующийTraversable);- начиная с PHP 8.0 —
mixed(хотя технически это union-тип, он часто используется как составной контейнер); - начиная с PHP 8.1 —
enum(перечисления, которые также считаются составными, так как объединяют набор констант с поведением).
Эти типы позволяют описывать структуры, близкие к реальному миру: список пользователей, заказ с позициями, дерево категорий, конфигурационный объект и многое другое.
Массивы как основа составных данных
Массив (array) — самый универсальный и часто используемый составной тип в PHP. Он может хранить любые значения: скалярные, другие массивы, объекты, даже замыкания.
Особенности массивов в PHP
Массив в PHP — это не просто список, а упорядоченный хеш-мап, где каждый элемент имеет ключ и значение. Ключ может быть целым числом (int) или строкой (string). Это делает массив одновременно:
- списком (если ключи — последовательные целые числа);
- словарём или ассоциативным массивом (если ключи — строки или произвольные числа);
- стеком, очередью, множеством — в зависимости от способа использования.
Пример:
$user = [
'id' => 42,
'name' => 'Анна',
'roles' => ['admin', 'editor'],
'metadata' => [
'last_login' => '2026-03-10',
'preferences' => ['theme' => 'dark']
]
];
Здесь $user — составной массив, содержащий скалярные значения, вложенные массивы и даже вложенные структуры.
Типизация массивов
Начиная с PHP 7.4, появились типизированные свойства, а с PHP 8.0 — union-типы и обобщённые псевдонимы (через @template в DocBlock, хотя нативных дженериков пока нет). Тем не менее, сам язык не позволяет указать тип элементов массива напрямую в сигнатуре функции:
// Так нельзя (ошибка синтаксиса):
function process(array<string> $tags) { }
// Но можно через DocBlock (для статического анализа):
/**
* @param string[] $tags
*/
function process(array $tags) { }
Это ограничение компенсируется использованием объектов-коллекций или строгой договорённостью в команде.
Объекты как составные структуры
Объект (object) — это экземпляр класса. Он объединяет данные (свойства) и поведение (методы). Объекты позволяют моделировать сущности предметной области с высокой степенью абстракции и инкапсуляции.
Преимущества объектов перед массивами
Хотя массивы гибки, объекты дают:
- типобезопасность: можно точно указать, что ожидается объект конкретного класса;
- инкапсуляцию: скрыть внутреннее состояние и предоставить контролируемый интерфейс;
- поведение: методы, которые оперируют внутренними данными;
- наследование и полиморфизм: расширение и переопределение функциональности.
Пример:
class User {
public int $id;
public string $name;
public array $roles;
public function hasRole(string $role): bool {
return in_array($role, $this->roles);
}
}
Теперь вместо ассоциативного массива мы работаем с типизированным объектом, который гарантирует наличие нужных полей и предоставляет методы для работы с ними.
Типизация объектов
В сигнатурах функций и свойств можно указывать конкретные классы:
function greetUser(User $user): string {
return "Привет, {$user->name}!";
}
Такой подход делает код более надёжным и понятным.
Callable и функциональные составные типы
Callable (callable) — это тип, описывающий всё, что можно вызвать как функцию. Это может быть:
- имя функции (
'strlen'); - массив вида
[$object, 'method']; - замыкание (
Closure); - объект с магическим методом
__invoke.
Пример:
function applyFilter(array $data, callable $filter): array {
return array_map($filter, $data);
}
$result = applyFilter([1, 2, 3], fn($x) => $x * 2);
callable — составной тип, потому что он объединяет разные формы вызываемого кода под единым интерфейсом. Это особенно полезно при работе с коллбэками, middleware, стратегиями и событиями.
Iterable — обобщённый составной тип
Iterable (iterable) — это псевдотип, введённый в PHP 7.1. Он означает: «либо массив, либо объект, реализующий интерфейс Traversable».
Этот тип полезен, когда функция должна принимать любую итерируемую структуру, не привязываясь к конкретной реализации.
Пример:
function sum(iterable $numbers): int {
$total = 0;
foreach ($numbers as $num) {
$total += $num;
}
return $total;
}
echo sum([1, 2, 3]); // массив
echo sum(new ArrayIterator([4, 5, 6])); // объект Traversable
iterable — это составной тип, потому что он описывает контейнер, содержащий множество значений, доступных по одному.
Enumerations (перечисления) — составные типы с фиксированным набором значений
Начиная с PHP 8.1, появились перечисления (enum). Они позволяют определить тип с ограниченным набором допустимых значений.
Пример:
enum Status: string {
case DRAFT = 'draft';
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
public function label(): string {
return match($this) {
self::DRAFT => 'Черновик',
self::PUBLISHED => 'Опубликовано',
self::ARCHIVED => 'Архив'
};
}
}
Перечисление — это составной тип, потому что:
- оно объединяет несколько констант;
- каждая константа — полноценный объект типа
Status; - перечисление может иметь методы и состояния (в случае backed-enum — привязку к скалярному значению).
Использование:
function setStatus(Post $post, Status $status): void {
$post->status = $status;
}
$post = new Post();
setStatus($post, Status::PUBLISHED);
echo $post->status->label(); // "Опубликовано"
Это повышает безопасность и выразительность кода по сравнению с использованием строковых констант.
Union-типы и составные сигнатуры
Начиная с PHP 8.0, появились union-типы, которые позволяют указывать, что переменная может принадлежать к одному из нескольких типов:
function parseValue(string|int|float $input): mixed {
if (is_string($input)) {
return json_decode($input, true);
}
return $input;
}
Union-тип — это тоже форма составного описания, потому что он объединяет несколько возможных типов в один логический контейнер.
Особенно важен union-тип string|int для ключей массивов, или null|object для опциональных значений (хотя для последнего лучше использовать nullable-синтаксис: ?User).
Сравнение составных типов: когда что использовать
| Сценарий | Рекомендуемый тип |
|---|---|
| Простой список значений | array |
| Ассоциативные данные без поведения | array или stdClass |
| Данные с поведением, валидацией, инвариантами | object (класс) |
| Коллекция однородных элементов с методами | объект-коллекция (UserCollection) |
| Фиксированный набор состояний | enum |
| Передача функции как параметра | callable |
| Универсальная итерация | iterable |
| Неопределённое значение (редко) | mixed |
Выбор правильного составного типа — это часть архитектурного решения. Он влияет на читаемость, тестируемость и поддерживаемость кода.
Вложенные составные типы и рекурсивные структуры
Составные типы в PHP могут быть вложенными: массив может содержать другие массивы, объекты — другие объекты, а перечисления — ссылаться на другие перечисления или даже содержать свойства, которые сами являются составными типами (начиная с PHP 8.1, если использовать backed-enum с методами).
Пример вложенного массива
$catalog = [
'electronics' => [
'phones' => ['iPhone', 'Samsung'],
'laptops' => ['MacBook', 'ThinkPad']
],
'books' => [
'fiction' => ['1984', 'Dune'],
'tech' => ['Clean Code', 'Design Patterns']
]
];
Такая структура моделирует древовидную иерархию. Обработка подобных структур требует рекурсии или итераторов.
Рекурсивные объекты
Объекты могут ссылаться на другие объекты того же типа. Это часто используется при построении деревьев, связанных списков или графов.
class TreeNode {
public string $value;
public ?TreeNode $left = null;
public ?TreeNode $right = null;
public function __construct(string $value) {
$this->value = $value;
}
}
$root = new TreeNode('A');
$root->left = new TreeNode('B');
$root->right = new TreeNode('C');
Здесь TreeNode содержит свойства, типизированные как ?TreeNode — это union-тип null|TreeNode, позволяющий моделировать несбалансированное дерево.
Составные перечисления
Хотя перечисления в PHP не могут напрямую содержать другие перечисления как значения, они могут иметь статические методы, возвращающие составные структуры:
enum UserRole: string {
case ADMIN = 'admin';
case EDITOR = 'editor';
case VIEWER = 'viewer';
public static function permissions(): array {
return match(self::ADMIN) {
self::ADMIN => ['read', 'write', 'delete'],
self::EDITOR => ['read', 'write'],
self::VIEWER => ['read'],
};
}
}
Это позволяет инкапсулировать логику, связанную с каждым значением перечисления, внутри самого enum.
Передача составных типов: по значению или по ссылке
PHP обрабатывает составные типы по-разному в зависимости от их природы:
- Массивы передаются по значению. При присваивании или передаче в функцию создаётся копия.
- Объекты передаются по ссылке на объект (технически — по значению указателя). Изменения внутри функции влияют на исходный объект.
- Перечисления — скалярные объекты, передаются по значению, но поскольку они immutable, изменения невозможны.
Пример с массивом:
function addElement(array $list): array {
$list[] = 'new';
return $list;
}
$original = ['a', 'b'];
$modified = addElement($original);
// $original остаётся ['a', 'b']
// $modified — ['a', 'b', 'new']
Пример с объектом:
function renameUser(User $user, string $name): void {
$user->name = $name;
}
$user = new User();
$user->name = 'Иван';
renameUser($user, 'Анна');
// $user->name теперь 'Анна'
Это поведение важно учитывать при проектировании функций: если нужно избежать побочных эффектов с объектами, следует клонировать их (clone) или использовать неизменяемые (immutable) объекты.
Сериализация и десериализация составных типов
Часто возникает необходимость сохранить составной тип в файл, базу данных или передать по сети. Для этого используется сериализация.
Сериализация массивов и объектов
PHP предоставляет встроенные функции:
serialize()/unserialize()json_encode()/json_decode()
JSON-сериализация
$data = [
'user' => new User(id: 1, name: 'Мария', roles: ['admin']),
'timestamp' => time()
];
$json = json_encode($data, JSON_THROW_ON_ERROR);
// Обратно:
$restored = json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
Однако объекты при json_encode() преобразуются в ассоциативные массивы. Чтобы восстановить объект, требуется ручная десериализация или реализация интерфейса JsonSerializable.
Реализация JsonSerializable
class User implements JsonSerializable {
public function __construct(
public int $id,
public string $name,
public array $roles
) {}
public function jsonSerialize(): array {
return [
'id' => $this->id,
'name' => $this->name,
'roles' => $this->roles
];
}
}
Теперь json_encode(new User(...)) вернёт корректный JSON, а при необходимости можно написать статический метод fromArray() для обратного преобразования.
Ограничения сериализации
- Замыкания (
Closure) нельзя сериализовать черезserialize()(выбросит исключение). - Ресурсы (например, открытый файловый дескриптор) не сериализуются.
- Циклические ссылки в объектах приведут к ошибке при
json_encode()или бесконечной рекурсии без контроля глубины.
Производительность работы с составными типами
Хотя PHP оптимизирован для работы с массивами, чрезмерное вложение или создание больших объектных графов может повлиять на производительность.
Copy-on-write для массивов
PHP использует механизм copy-on-write: при присваивании массива копия не создаётся сразу, а только тогда, когда один из массивов изменяется. Это экономит память при чтении.
Объекты легче массивов?
Не всегда. Объекты имеют накладные расходы на хранение метаданных класса, но при большом количестве операций доступа к свойствам они могут быть быстрее благодаря строгой структуре.
Советы по оптимизации:
- Используйте объекты вместо ассоциативных массивов, если структура фиксирована и используется многократно.
- Избегайте глубокой вложенности без необходимости.
- Для коллекций с большим числом элементов рассмотрите использование генераторов (
yield) вместо полной загрузки в память. - При работе с API — преобразуйте сырые данные в объекты как можно раньше, чтобы обеспечить типобезопасность.
Практические паттерны использования составных типов
1. DTO (Data Transfer Object)
Используется для передачи структурированных данных между слоями приложения.
class OrderDto {
public function __construct(
public int $id,
public string $status,
public array $items // массив ItemDto
) {}
}
2. Value Object
Неизменяемый объект, представляющий значение (например, Email, Money, Coordinates).
class Email {
private function __construct(private string $address) {}
public static function fromString(string $address): self {
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
return new self($address);
}
public function toString(): string {
return $this->address;
}
}
3. Коллекции
Обёртка над массивом с типизированными методами:
class UserCollection implements IteratorAggregate {
private array $users = [];
public function add(User $user): void {
$this->users[] = $user;
}
public function getAdmins(): UserCollection {
$admins = new UserCollection();
foreach ($this->users as $user) {
if ($user->hasRole('admin')) {
$admins->add($user);
}
}
return $admins;
}
public function getIterator(): Traversable {
return new ArrayIterator($this->users);
}
}
Такие коллекции делают код выразительнее и безопаснее.
Ошибки типизации и проверка типов во время выполнения
PHP — язык с динамической типизацией, но современные версии (начиная с 7.0) активно поддерживают строгую типизацию через объявления типов в сигнатурах функций, свойствах классов и возвращаемых значениях.
Строгая и слабая типизация
По умолчанию PHP использует слабую типизацию: если передать строку "42" туда, где ожидается int, PHP попытается привести тип автоматически. Однако при включении строгого режима (declare(strict_types=1);) такое поведение запрещено, и будет выброшено исключение TypeError.
Пример:
// strict_types.php
declare(strict_types=1);
function add(int $a, int $b): int {
return $a + $b;
}
add("10", "20"); // TypeError: Argument 1 must be of type int, string given
Строгий режим применяется на уровне файла, а не глобально. Это позволяет постепенно внедрять типобезопасность в проект.
Проверка типов вручную
Иногда необходимо проверить тип переменной вручную, особенно при работе с внешними данными (HTTP-запросы, файлы, API). Для этого используются функции:
is_array($var)— проверяет, является ли переменная массивом;is_object($var)— проверяет объект;is_callable($var)— проверяет вызываемость;is_iterable($var)— проверяет итерируемость (работает с PHP 7.1+).
Пример:
function processInput($data): void {
if (!is_array($data)) {
throw new InvalidArgumentException('Ожидался массив');
}
foreach ($data as $item) {
// обработка
}
}
Такой подход обеспечивает защиту от некорректных данных и делает код более надёжным.
Миграция с массивов на объекты: практические рекомендации
Многие проекты начинаются с использования ассоциативных массивов для передачи данных. Со временем это приводит к проблемам:
- отсутствие гарантий наличия ключей;
- сложность поддержки (что означает ключ
'status'? строка? число?); - невозможность добавить поведение.
Шаг 1: Введение DTO
Замените массив на простой класс с публичными свойствами или геттерами/сеттерами.
Было:
$user = ['id' => 1, 'name' => 'Анна', 'email' => 'anna@example.com'];
Стало:
class UserDto {
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}
Теперь IDE подсказывает поля, статический анализатор проверяет типы, а компилятор (виртуальная машина PHP) — гарантирует корректность.
Шаг 2: Добавление валидации
В конструкторе можно добавить проверки:
public function __construct(int $id, string $name, string $email) {
if ($id <= 0) {
throw new InvalidArgumentException('ID должен быть положительным');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Некорректный email');
}
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
Это обеспечивает инварианты объекта — он всегда находится в корректном состоянии.
Шаг 3: Иммутабельность (опционально)
Для повышения предсказуемости сделайте объект неизменяемым:
class UserDto {
public function __construct(
private int $id,
private string $name,
private string $email
) {}
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
}
Теперь данные нельзя случайно изменить после создания.