Перейти к основному содержимому

Работа с составными типами в PHP

Разработчику Архитектору

См. также: Переменные и типы данных в PHP · Работа со скалярными типами · Справочник 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']
]
];

Разбор:

  • Пример показывает составной ассоциативный массив с вложенными структурами.
  • Ключи верхнего уровня (id, name, roles, metadata) описывают разные аспекты одной сущности пользователя.
  • roles хранит список ролей, а metadata — вложенный словарь с дополнительными данными.
  • Такой формат удобен для передачи JSON-подобных структур между слоями приложения.
  • Важно документировать ожидаемую схему массива, чтобы избежать ошибок по ключам.

Здесь $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);
}
}

Разбор:

  • class User переносит данные из "свободного массива" в типизированную модель с полями и поведением.
  • Свойства public int $id, public string $name, public array $roles задают базовые инварианты типа.
  • Метод hasRole(...) инкапсулирует проверку прав и убирает дублирование по проекту.
  • in_array ищет значение в коллекции ролей, возвращая true/false.
  • Такой подход улучшает автодополнение IDE и делает код более предсказуемым.

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


Типизация объектов​

В сигнатурах функций и свойств можно указывать конкретные классы:

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 $filter позволяет передать любую вызываемую логику (функцию, метод, замыкание).
  • array_map($filter, $data) применяет фильтр к каждому элементу массива и возвращает новый массив.
  • fn($x) => $x * 2 — короткое стрелочное замыкание, здесь оно удваивает каждое значение.
  • Сигнатура : array фиксирует ожидаемый тип результата и упрощает сопровождение функции.
  • Это базовый паттерн функциональной обработки коллекций в PHP.

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', 'Проектирование 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​

Код ITЗагрузка примера кода…

Теперь 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).

Код ITЗагрузка примера кода…


3. Коллекции​

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Обёртка над массивом с типизированными методами:

Код ITЗагрузка примера кода…

Такие коллекции делают код выразительнее и безопаснее.


Ошибки типизации и проверка типов во время выполнения​

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

Теперь данные нельзя случайно изменить после создания.


Сводка операций по типам​

Эта сводка помогает быстро выбрать структуру под задачу и сразу видеть основные операции.


Упорядоченная изменяемая коллекция — list на базе array​

ДействиеПример
Добавить в конец$items[] = $value;
Вставить по индексуarray_splice($items, $index, 0, [$value]);
Прочитать$item = $items[$index] ?? null;
Заменить$items[$index] = $newValue;
Удалитьarray_splice($items, $index, 1);

Словарь — map на базе ассоциативного array​

ДействиеПример
Добавить или заменить$map["key"] = $value;
Прочитать$value = $map["key"] ?? null;
Удалитьunset($map["key"]);

Множество — set через ключи массива​

ДействиеПример
Добавить$set[$value] = true;
Удалитьunset($set[$value]);
Проверить наличие$exists = isset($set[$value]);

Очередь и стек — SplQueue и SplStack​

ТипОсновные операции
Queue (FIFO)enqueue(), dequeue(), bottom()
Stack (LIFO)push(), pop(), top()

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.

Содержание