Массивы в PHP 8.x: разбор на примере обработки заказов
Берём реальную задачу — свести выгрузку заказов из маркетплейса в отчёт — и разбираем, какие функции работы с массивами дают чистый код, а какие тихо съедают память и время.

Задача простая на словах: пришла выгрузка заказов в виде массива, из неё нужно собрать отчёт — сгруппировать по товарам, посчитать суммы, отфильтровать возвраты и вернуть отсортированный результат. На таком примере видно, где встроенные функции PHP экономят строки, а где привычное решение через foreach и array_merge начинает есть память. Пишу для PHP 8.1+, некоторые приёмы работают и в 7.4, отличия отмечу.
Исходные данные
Работаем с таким массивом. Представьте, что он пришёл из JSON-выгрузки: массив ассоциативных массивов, каждый — строка заказа.
<?php
// PHP 8.1+
$orders = [
['sku' => 'A-100', 'title' => 'Кружка', 'qty' => 2, 'price' => 350, 'status' => 'paid'],
['sku' => 'A-100', 'title' => 'Кружка', 'qty' => 1, 'price' => 350, 'status' => 'paid'],
['sku' => 'B-200', 'title' => 'Термос', 'qty' => 1, 'price' => 1200, 'status' => 'refund'],
['sku' => 'C-300', 'title' => 'Блокнот', 'qty' => 5, 'price' => 200, 'status' => 'paid'],
['sku' => 'B-200', 'title' => 'Термос', 'qty' => 2, 'price' => 1200, 'status' => 'paid'],
];
Цель — получить отчёт по оплаченным заказам: SKU, название, суммарное количество и выручка, отсортированный по выручке по убыванию.
Шаг 1. Отфильтровать возвраты
Первый инстинкт — цикл с if и накоплением в новый массив. Работает, но array_filter выражает намерение короче и не плодит промежуточную переменную-счётчик.
$paid = array_filter(
$orders,
fn(array $row) => $row['status'] === 'paid'
);
Одна тонкость: array_filter сохраняет исходные ключи. После него ключи станут 0, 1, 3, 4 — с дыркой на месте отфильтрованной строки. Если дальше вы обращаетесь к элементам по числовому индексу или отдаёте массив в json_encode и ждёте JSON-массив, а не объект, ключи надо сбросить: $paid = array_values($paid);. Это самая частая причина того, что фронт получает объект {"0":...,"1":...} вместо ожидаемого списка.
Шаг 2. Сгруппировать по SKU и посчитать суммы
Здесь foreach уместнее любых функциональных изысков. Группировка с накоплением — как раз тот случай, где array_reduce формально подходит, но читается хуже обычного цикла.
$report = [];
foreach ($paid as $row) {
$sku = $row['sku'];
// Первая встреча SKU — инициализируем строку отчёта
if (!isset($report[$sku])) {
$report[$sku] = [
'sku' => $sku,
'title' => $row['title'],
'qty' => 0,
'revenue' => 0,
];
}
$report[$sku]['qty'] += $row['qty'];
$report[$sku]['revenue'] += $row['qty'] * $row['price'];
}
Проверка isset, а не array_key_exists, — намеренно: isset быстрее и здесь значение точно не бывает null. Разница между ними всплывёт, только если в массиве встречаются ключи со значением null, — тогда isset вернёт false, а array_key_exists — true.
Функциональный стиль хорош для преобразования «элемент в элемент» и фильтрации. Как только появляется накопление в общий аккумулятор с ветвлением, обычный foreach читается лучше array_reduce — не из-за скорости, а из-за того, что его поймёт следующий разработчик.
Шаг 3. Отсортировать по выручке
Массив ассоциативный по SKU, сортируем по полю revenue. Нужна usort, но она сбрасывает ключи. Если ключи-SKU не важны — а в финальном отчёте они обычно не важны, — это то что нужно.
$rows = array_values($report);
usort(
$rows,
fn(array $a, array $b) => $b['revenue'] <=> $a['revenue']
);
Оператор <=> (космический корабль) вернёт -1, 0 или 1 — ровно то, что ждёт usort. Порядок $b и $a в сравнении задаёт убывание. Если нужно сохранить ключи при сортировке — берите uasort вместо usort.
Результат целиком
<?php
$paid = array_values(array_filter($orders, fn($r) => $r['status'] === 'paid'));
$report = [];
foreach ($paid as $row) {
$sku = $row['sku'];
$report[$sku] ??= ['sku' => $sku, 'title' => $row['title'], 'qty' => 0, 'revenue' => 0];
$report[$sku]['qty'] += $row['qty'];
$report[$sku]['revenue'] += $row['qty'] * $row['price'];
}
$rows = array_values($report);
usort($rows, fn($a, $b) => $b['revenue'] <=> $a['revenue']);
print_r($rows);
Обратите внимание на ??= — оператор присваивания с объединением null, доступный с PHP 7.4. Он заменяет пару if (!isset(...)) одной строкой: присвоит значение, только если ключ не задан или равен null.
Где привычное решение бьёт по памяти
Частый антипаттерн — собирать результат через array_merge в цикле:
// Так делать не надо
$result = [];
foreach ($chunks as $chunk) {
$result = array_merge($result, $chunk); // копия всего $result на каждой итерации
}
Каждый вызов array_merge создаёт новый массив и копирует туда всё, что накопилось. На тысяче итераций это квадратичная сложность по памяти и времени. Правильно — распаковка через spread или прямое добавление:
// Вариант с накоплением плоского списка
$result = [];
foreach ($chunks as $chunk) {
foreach ($chunk as $item) {
$result[] = $item; // добавление в конец, без копирования
}
}
// Или, если структура позволяет, один вызов:
$result = array_merge(...$chunks); // распаковка, один проход
Второй вариант с ... работает, когда все элементы $chunks — массивы и их не десятки тысяч (у распаковки аргументов есть свои лимиты по числу элементов). Для стринговых ключей помните: array_merge перезаписывает совпадающие ключи, а числовые — переиндексирует.
Какую функцию когда брать
| Задача | Функция | Что помнить |
|---|---|---|
| Преобразовать каждый элемент | array_map | Сохраняет ключи; с несколькими массивами ключи сбрасывает |
| Отфильтровать по условию | array_filter | Сохраняет ключи — часто нужен array_values после |
| Свернуть в одно значение | array_reduce | Читается хуже foreach при ветвлении |
| Сортировка по полю | usort / uasort | usort сбрасывает ключи, uasort — сохраняет |
| Есть ли значение | in_array | Третий аргумент true — строгое сравнение, без него ловите баги приведения типов |
| Есть ли ключ | array_key_exists / isset | isset вернёт false для значения null |
Ловушки, которые встречаются чаще всего
- Мягкое сравнение в
in_array. Без третьего аргументаin_array('0', $arr)может совпасть сfalseили0из-за приведения типов. Почти всегда нужен строгий режим:in_array($needle, $arr, true). - Забытый
array_valuesпосле фильтра. JSON превращается из массива в объект, фронт ломается. - Ссылка в
foreach ($arr as &$item). После цикла переменная$itemостаётся ссылкой на последний элемент. Следующее использование той же переменной перезапишет последний элемент массива. Либо делайтеunset($item)после цикла, либо не используйте ссылку. - Числовые строки как ключи. PHP приводит строковый ключ
"5"к целому5. Массив с ключами-SKU это не затронет, но выгрузку с ключами вроде"01"и"1"он может схлопнуть неожиданно —"01"останется строкой, а"1"станет числом.
Что дальше
Если данных много и вы читаете их из файла построчно — переводите обработку на генераторы (yield), чтобы не держать всю выгрузку в памяти сразу; это отдельная тема, здесь мы работали с уже загруженным массивом. Для группировки и агрегаций на больших объёмах часто дешевле переложить работу на SQL, а не тянуть всё в PHP. А для повседневного кода стоит один раз пройтись по разделу Array Functions в официальной документации — там десятки функций, которые заменяют собой циклы, если знать об их существовании.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: PHP Manual — Array Functions, PHP Manual — Comparison Operators (spaceship)
Частые вопросы
А если у меня PHP 7.4, а не 8.1 — код сработает?
Почему после array_filter в JSON вместо массива приходит объект?
usort или array_multisort — что быстрее для сортировки по полю?
Не сработало: после foreach со ссылкой последний элемент массива задваивается. В чём дело?
Стоит ли переписывать все циклы на array_map и array_filter ради стиля?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.