app(); } } if (!function_exists('config')) { /** * Возвращает значение из конфигурации приложения. * * @param string $key ключ конфигурации * @param mixed $default значение по умолчанию, если ключ не найден * @return mixed значение конфигурации или значение по умолчанию */ function config(string $key, mixed $default = null): mixed { return Kernel::instance()->config($key, $default); } } if (!function_exists('redis')) { /** * Возвращает синглтон-экземпляр Redis-клиента. * * @return Redis экземпляр Redis-клиента */ function redis(): Redis { return Kernel::instance()->redis(); } } if (!function_exists('ini')) { /** * Возвращает синглтон-экземпляр IniFile. * * @return IniFile экземпляр IniFile */ function ini(): IniFile { return Kernel::instance()->ini(); } } // ------------------ // Common purpose // ------------------ if (!function_exists('int')) { /** * Приводит значение к целому числу или возвращает null * * @param mixed $value Входное значение * @param bool $strict Строгая валидация (только целые числа) * @return int|null Целое число или null * @throws InvalidArgumentException Если значение не соответствует ограничениям */ function int(mixed $value, bool $strict = false): ?int { if (is_blank($value)) { return null; } if (is_int($value) && !$strict) { return $value; } return filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); } } if (!function_exists('float')) { /** * Приводит значение к числу с плавающей точкой или возвращает null * * @param mixed $value Входное значение * @return float|null Число с плавающей точкой или null */ function float(mixed $value): ?float { if (is_blank($value)) { return null; } $filtered = filter_var($value, FILTER_VALIDATE_FLOAT); return $filtered === false ? (float) $value : $filtered; } } if (!function_exists('bool')) { /** * Приводит любое значение к булевому типу * * @param mixed $value Входное значение * @return bool Булево представление значения */ function bool(mixed $value): bool { if ($value === null || $value === '') { return false; } if (is_bool($value)) { return $value; } if (is_numeric($value)) { return (float) $value !== 0.0; } if (is_string($value)) { $value = strtolower(trim($value)); $positives = ['1', '+', 'yes', 'on', 'true', 'enable', 'enabled', 'да', 'включено', 'истина']; $negatives = ['0', '-', 'no', 'off', 'false', 'disable', 'disabled', 'нет', 'выключено', 'ложь']; if (in_array($value, $positives, true)) { return true; } if (in_array($value, $negatives, true)) { return false; } } if (is_array($value) || is_object($value)) { return !empty($value); } return (bool) $value; } } if (!function_exists('string')) { /** * Приводит любое значение к строке * * @param mixed $value Входное значение * @return string Строковое представление значения */ function string(mixed $value): string { if (is_object($value)) { return match (true) { $value instanceof Stringable => (string) $value, $value instanceof JsonSerializable => json_encode($value), default => json_encode(arr($value)), }; } return match (true) { is_resource($value) => stream_get_contents($value), is_iterable($value) => json_encode(iterator_to_array($value)), default => (string) $value, }; } } if (!function_exists('arr')) { /** * Рекурсивно приводит объект или массив объектов к массиву * * @param mixed $value Входное значение * @return mixed Массив */ function arr(mixed $value): mixed { if (is_object($value)) { $value = match (true) { $value instanceof JsonSerializable => $value->jsonSerialize(), $value instanceof Iterator => iterator_to_array($value), default => (array) $value, }; } if (is_array($value)) { return array_map(static fn ($value) => arr($value), $value); } return $value; } } if (!function_exists('env')) { /** * Возвращает значение переменной окружения. * * @param string $key ключ переменной окружения * @param mixed $default значение по умолчанию, если переменная не найдена * @param bool $required бросать исключение, если переменная обязательна и не найдена * @return mixed значение переменной окружения или значение по умолчанию * @throws InvalidArgumentException если переменная обязательна и не найдена */ function env(string $key, mixed $default = null, bool $required = false): mixed { $value = $_ENV[$key] ?? null; if ($value === null && isset($_SERVER[$key])) { $value = $_SERVER[$key]; } if ($value !== null) { return $value; } if ($required) { throw new InvalidArgumentException( sprintf( 'Обязательная переменная окружения "%s" не найдена', $key, ), ); } return $default; } } if (!function_exists('here')) { /** * Возвращает координаты вызывавшего метода * * @param bool $asArray массив или строка в формате `:():` * @return string|array */ function here(bool $asArray = false): array|string { $trace = debug_backtrace(!DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 2); return $asArray ? [ 'from' => $trace[1]['class'] ?? $trace[0]['file'], 'function' => $trace[1]['function'], 'line' => $trace[0]['line'], ] : sprintf( '%s%s%s():%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[1]['type'] ?? '::', $trace[1]['function'], $trace[0]['line'], ); } } if (!function_exists('rslash')) { /** * Добавляет завершающий слеш к пути * * @param string $path Путь для обработки * @return string Путь с завершающим слешем */ function rslash(string $path): string { return rtrim($path, '/') . '/'; } } if (!function_exists('camel2snake')) { /** * Переводит строку из camelCase в snake_case * * @param string $input Строка в camelCase * @return string Строка в snake_case */ function camel2snake(string $input): string { if (!$input) { return $input; } $input = (string) preg_replace('/(? 1024 && $i < count($units) - 1; ++$i) { $bytes /= 1024; } return round($bytes, $precision) . ' ' . $units[$i]; } } if (!function_exists('time_ago')) { /** * Возвращает читаемое представление времени в прошедшем времени * * @param int|string $time Время в формате Unix timestamp или строка времени * @return string Относительное время */ function time_ago(int|string $time): string { $time = is_string($time) ? strtotime($time) : $time; if ($time === false || $time <= 0) { return 'неизвестно'; } $diff = time() - $time; if ($diff < 60) { return 'только что'; } if ($diff < 60 * 60) { // 1 час $minutes = (int) floor($diff / 60); return $minutes . ' ' . get_noun_form($minutes, 'минуту', 'минуты', 'минут') . ' назад'; } if ($diff < 24 * 60 * 60) { // 1 день $hours = (int) floor($diff / (60 * 60)); return $hours . ' ' . get_noun_form($hours, 'час', 'часа', 'часов') . ' назад'; } if ($diff < 7 * 24 * 60 * 60) { // 1 неделя $days = (int) floor($diff / (24 * 60 * 60)); return $days . ' ' . get_noun_form($days, 'день', 'дня', 'дней') . ' назад'; } if ($diff < 30 * 24 * 60 * 60) { // 1 месяц $weeks = (int) floor($diff / (7 * 24 * 60 * 60)); return $weeks . ' ' . get_noun_form($weeks, 'неделю', 'недели', 'недель') . ' назад'; } if ($diff < 365 * 24 * 60 * 60) { // 1 год $months = (int) floor($diff / (30 * 24 * 60 * 60)); return $months . ' ' . get_noun_form($months, 'месяц', 'месяца', 'месяцев') . ' назад'; } $years = (int) floor($diff / (365 * 24 * 60 * 60)); return $years . ' ' . get_noun_form($years, 'год', 'года', 'лет') . ' назад'; } } if (!function_exists('get_noun_form')) { /** * Возвращает правильную форму существительного для числительного * * @param int $number Числительное * @param string $form1 Форма для 1 * @param string $form2 Форма для 2-4 * @param string $form5 Форма для 5+ * @return string Правильная форма существительного */ function get_noun_form(int $number, string $form1, string $form2, string $form5): string { $number = abs($number); $lastDigit = $number % 10; $lastTwoDigits = $number % 100; if ($lastTwoDigits >= 11 && $lastTwoDigits <= 14) { return $form5; } if ($lastDigit === 1) { return $form1; } if ($lastDigit >= 2 && $lastDigit <= 4) { return $form2; } return $form5; } } if (!function_exists('random_string')) { /** * Генерирует случайную строку заданной длины * * @param int $length Длина строки * @param string $chars Набор символов для генерации * @return string Случайная строка * @throws RandomException */ function random_string( int $length = 32, string $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ): string { $result = ''; $max = strlen($chars) - 1; for ($i = 0; $i < $length; ++$i) { $result .= $chars[random_int(0, $max)]; } return $result; } } if (!function_exists('secure_token')) { /** * Генерирует безопасный случайный токен * * @param int $length Длина токена в байтах * @return string Шестнадцатеричное представление токена * @throws RandomException */ function secure_token(int $length = 32): string { return bin2hex(random_bytes($length)); } } if (!function_exists('decode_jwt')) { /** * Возвращает полезную нагрузку токена без проверки подписи * * @param string $token JWT токен * @return array Полезная нагрузка токена * @throws Exception */ function decode_jwt(string $token): array { $exploded = explode('.', $token); count($exploded) !== 3 && throw new InvalidArgumentException('JWT string expected'); $json = base64_decode($exploded[1]); return json_decode($json, true); } } if (!function_exists('nullable')) { /** * Возвращает значение, если оно непустое, иначе null * * @param mixed $value Значение, которое надо проверить на пустоту * @param string|callable|null $type Тип, в который следует привести непустое значение * @return mixed Значение в нужном $type либо null */ function nullable(mixed $value, callable|string|null $type = null): mixed { if (!is_bool($value) && empty($value)) { return null; } if (empty($type)) { return $value; } if (is_callable($type)) { return $type($value); } settype($value, $type); return $value; } } if (!function_exists('carbon')) { /** * Возвращает объект Carbon для значения переменной или null * * @param DateTimeInterface|string|null $value Строка/объект даты или null * @param string $format Формат * @return Carbon|null */ function carbon(DateTimeInterface|string|null $value = null, string $format = 'd.m.Y H:i:s'): ?Carbon { return Carbon::make($value)?->settings([ 'toStringFormat' => $format, 'toJsonFormat' => $format, ]); } } if (!function_exists('recast')) { /** * Преобразует stdClass объект в объект указанного типа * * @param string $className Имя класса для приведения * @param stdClass $object Объект для приведения * @return mixed Новый объект указанного типа * @throws InvalidArgumentException */ function recast(string $className, stdClass &$object): mixed { if (!class_exists($className)) { throw new InvalidArgumentException("Class not found: {$className}"); } $new = new $className(); foreach ($object as $property => &$value) { // @phpstan-ignore-line $new->{$property} = &$value; unset($object->{$property}); } unset($value, $object); return $new; } } if (!function_exists('truncate')) { /** * Обрезает текст до указанной длины и добавляет эллипсис * * @param string $text Исходный текст * @param int $length Максимальная длина * @param string $suffix Строка для добавления в конце * @return string Обрезанный текст */ function truncate(string $text, int $length = 100, string $suffix = '...'): string { if (mb_strlen($text) <= $length) { return $text; } $truncatedLength = $length - mb_strlen($suffix); return mb_substr($text, 0, $truncatedLength) . $suffix; } } if (!function_exists('tail')) { /** * Возвращает последние символы строки с префиксом * * @param string $string Исходная строка * @param int $count Количество символов для возврата * @param string $prefix Префикс для добавления * @return string Результат с префиксом */ function tail(string $string, int $count = 15, string $prefix = ''): string { return empty($string = trim($string)) ? '' : sprintf('%s%s', $prefix, mb_substr($string, -abs($count), mb_strlen($string))); } } if (!function_exists('mb_count_chars')) { /** * Подсчитывает количество вхождений каждого символа в строке * * @param string $string Строка для анализа * @return array Массив с количеством вхождений каждого символа */ function mb_count_chars(string $string): array { $length = mb_strlen($string, 'UTF-8'); $unique = []; for ($i = 0; $i < $length; ++$i) { $char = mb_substr($string, $i, 1, 'UTF-8'); !array_key_exists($char, $unique) && $unique[$char] = 0; ++$unique[$char]; } return $unique; } } if (!function_exists('query')) { /** * Возвращает ассоциативный массив GET-параметров из URL * * @param string $url URL для парсинга * @return array Ассоциативный массив GET-параметров */ function query(string $url): array { $result = []; $parsed = parse_url($url); if (empty($parsed['query'])) { return $result; } $params = explode('&', urldecode($parsed['query'])); foreach ($params as $url) { [$key, $value] = explode('=', $url); if (in_array($value, ['true', 'false'])) { $result[$key] = bool($value) === true; } else { $result[$key] = empty($value) ? null : $value; } } return $result; } } if (!function_exists('array_first')) { /** * Возвращает первый элемент массива без перемотки указателя * * @param array $array Входной массив * @param callable|null $callback Замыкание для предварительной фильтрации массива * @param mixed $default Значение по умолчанию, если массив пуст * @return mixed Первый элемент массива или значение по умолчанию */ function array_first(array $array, ?callable $callback = null, mixed $default = null): mixed { if (empty($array)) { return $default; } foreach ($array as $key => $item) { if ($callback === null || $callback($item, $key)) { return $item; } } $firstKey = array_key_first($array); return $array[$firstKey] ?? $default; } } if (!function_exists('array_last')) { /** * Возвращает последний элемент массива без перемотки указателя * * @param array $array Входной массив * @param callable|null $callback Замыкание для предварительной фильтрации массива * @param mixed $default Значение по умолчанию, если массив пуст * @return mixed Последний элемент массива или значение по умолчанию */ function array_last(array $array, ?callable $callback = null, mixed $default = null): mixed { $keys = array_keys($array); $lastKey = end($keys); if ($lastKey === false) { return $default; } if ($callback === null || $callback($array[$lastKey], $lastKey)) { return $array[$lastKey]; } for ($i = count($keys) - 1; $i >= 0; --$i) { $key = $keys[$i]; if ($callback($array[$key], $key)) { return $array[$key]; } } return $default; } } if (!function_exists('array_any')) { /** * Проверяет, удовлетворяет ли хотя бы один элемент массива условию * * @param array $array Массив для проверки * @param callable $callback Функция проверки для каждого элемента * @return bool True, если хотя бы один элемент удовлетворяет условию */ function array_any(array $array, callable $callback): bool { /** @noinspection PhpLoopCanBeConvertedToArrayAnyInspection */ foreach ($array as $item) { if ($callback($item)) { return true; } } return false; } } if (!function_exists('array_all')) { /** * Проверяет, удовлетворяют ли все элементы массива условию * * @param array $array Массив для проверки * @param callable $callback Функция проверки для каждого элемента * @return bool True, если все элементы удовлетворяют условию */ function array_all(array $array, callable $callback): bool { /** @noinspection PhpLoopCanBeConvertedToArrayAllInspection */ /** @noinspection PhpLoopCanBeConvertedToArrayAnyInspection */ foreach ($array as $item) { if (!$callback($item)) { return false; } } return true; } } if (!function_exists('array_get')) { /** * Безопасно получает значение из массива по ключу * * @param array $array Исходный массив * @param string|int $key Ключ для поиска * @param mixed $default Значение по умолчанию * @return mixed Значение из массива или значение по умолчанию */ function array_get(array $array, int|string $key, mixed $default = null): mixed { return $array[$key] ?? $default; } } if (!function_exists('array_is_assoc')) { /** * Проверяет, является ли массив ассоциативным * * @param array $array Проверяемый массив * @return bool True, если массив ассоциативный */ function array_is_assoc(array $array): bool { if ($array === []) { return false; } return array_keys($array) !== range(0, count($array) - 1); } } if (!function_exists('array_except')) { /** * Безопасно удаляет элементы из массива по ключам * * @param array $array Исходный массив * @param array|string $keys Ключи для удаления * @return array Массив с удаленными элементами */ function array_except(array $array, array|string $keys): array { $keys = is_array($keys) ? $keys : [$keys]; foreach ($keys as $key) { unset($array[$key]); } return $array; } } if (!function_exists('array_only')) { /** * Возвращает только указанные ключи из массива * * @param array $array Исходный массив * @param array $keys Ключи для сохранения * @return array Массив только с указанными ключами */ function array_only(array $array, array $keys): array { return array_intersect_key($array, array_flip($keys)); } } if (!function_exists('array_is_empty')) { /** * Проверяет, что все значения массива пусты * * @param array $array Проверяемый массив * @return bool True, если все значения массива пусты */ function array_is_empty(array $array): bool { return empty($array) || empty(array_filter(array_values($array), static fn ($value) => is_filled($value))); } } if (!function_exists('array_to_csv')) { /** * Преобразует массив в CSV строку * * @param array $array Массив для преобразования * @param string $delimiter Разделитель полей * @param string $enclosure Ограничитель строк * @return string CSV строка */ function array_to_csv(array $array, string $delimiter = ',', string $enclosure = '"'): string { $output = fopen('php://temp', 'r+'); try { foreach ($array as $row) { fputcsv($output, $row, $delimiter, $enclosure); } rewind($output); $csv = stream_get_contents($output); } finally { fclose($output); } return rtrim($csv, "\n"); } } if (!function_exists('array_is_multidim')) { /** * Проверяет, является ли массив многомерным * * @param array $array Проверяемый массив * @return bool True, если массив многомерный */ function array_is_multidim(array $array): bool { if ($array === []) { return false; } return count($array) !== count($array, COUNT_RECURSIVE); } } if (!function_exists('array_merge_recursive_distinct')) { /** * Рекурсивно объединяет массивы * * @param array ...$arrays Массивы для объединения * @return array Объединенный массив */ function array_merge_recursive_distinct(array ...$arrays): array { $result = []; foreach ($arrays as $array) { foreach ($array as $key => $value) { if (is_string($key)) { if (is_array($value) && isset($result[$key]) && is_array($result[$key])) { $result[$key] = array_merge_recursive_distinct($result[$key], $value); } else { $result[$key] = $value; } } else { $result[] = $value; } } } return $result; } } if (!function_exists('array_random')) { /** * Возвращает случайный элемент из массива * * @param array $array Массив для выбора * @param int $count Количество элементов для возврата * @return mixed|mixed[] Случайный элемент или массив элементов */ function array_random(array $array, int $count = 1): mixed { if ($count === 1) { return $array[array_rand($array)]; } $keys = array_rand($array, $count); $result = []; foreach ($keys as $key) { $result[] = $array[$key]; } return $result; } } if (!function_exists('array_clean')) { /** * Чистит пустые и повторяющиеся значения массива * * @param mixed[] $array Входной массив * @param callable|null $callback Функция для фильтрации * @return array Очищенный массив */ function array_clean(array $array, ?callable $callback = null): array { return array_filter(array_unique($array), $callback); } } if (!function_exists('array_recursive_diff')) { /** * Корректно вычисляет diff между массивами, включая мультиразмерные * * @param array $a Первый массив * @param array $b Второй массив * @return array Разность массивов */ function array_recursive_diff(array $a, array $b): array { $aReturn = []; foreach ($a as $key => $value) { if (array_key_exists($key, $b)) { if (is_array($value)) { $aRecursiveDiff = array_recursive_diff($value, $b[$key]); if (count($aRecursiveDiff)) { $aReturn[$key] = $aRecursiveDiff; } } else { if ($value !== $b[$key]) { $aReturn[$key] = $value; } } } else { $aReturn[$key] = $value; } } return $aReturn; } } if (!function_exists('array_group')) { /** * Группирует массив по ключу * * @param array $array Входной массив * @param callable|string $callback Функция для определения ключа группировки * @return array Сгруппированный массив * @throws Exception */ function array_group(array $array, callable|string $callback): array { if (is_string($callback)) { $callback = static fn ($item) => $item[$callback] ?? null; } $result = []; foreach ($array as $key => $value) { $callbackRes = $callback($value, $key); if (is_array($callbackRes)) { $keys = array_keys($callbackRes); if (count($keys) !== 1) { throw new Exception('Возращаемый массив может содержать только один ключ'); } $resultKey = $keys[0]; $resultValue = $callbackRes[$resultKey]; } else { $resultKey = $callbackRes; $resultValue = $value; } $value = $result[$resultKey] ?? []; $value[] = $resultValue; $result[$resultKey] = $value; } return $result; } } if (!function_exists('array_assoc')) { /** * Преобразует массив в ассоциативный массив по ключу * * @param array $array Входной массив * @param callable|string $callback Функция для определения ключа * @return array Ассоциативный массив * @throws Exception */ function array_assoc(array $array, callable|string $callback): array { if (is_string($callback)) { $callback = static fn ($item) => $item[$callback] ?? null; } $result = []; foreach ($array as $key => $value) { $callbackRes = $callback($value, $key); if (is_array($callbackRes)) { $keys = array_keys($callbackRes); if (count($keys) !== 1) { throw new Exception('Возращаемый массив может содержать только один ключ'); } $resultKey = $keys[0]; $result[$resultKey] = $callbackRes[$resultKey]; } else { $result[$callbackRes] = $value; } } return $result; } } if (!function_exists('array_sort_by')) { /** * Сортирует массив по ключу * * @param array &$array Массив для сортировки * @param callable|string $callback Функция для определения ключа сортировки * @param int $sortType Тип сортировки (SORT_ASC или SORT_DESC) * @return array Отсортированный массив */ function array_sort_by(array &$array, callable|string $callback, int $sortType = SORT_ASC): array { if (is_string($callback)) { $callback = static fn ($item) => $item[$callback] ?? null; } $keys = array_map($callback, $array); array_multisort($keys, $sortType, $array); return $array; } } if (!function_exists('array_avg')) { /** * Вычисляет среднее значение элементов массива * * @param array $array Массив для вычисления среднего * @param callable|string $callback Функция для получения значения * @return float Среднее значение */ function array_avg(array $array, callable|string $callback): float { if (is_string($callback)) { $callback = static fn ($item) => $item[$callback] ?? null; } $values = array_map($callback, $array); $values = array_filter($values, static fn ($item) => !is_null($item)); $count = count($values); if ($count === 0) { return 0; } return ((float) array_sum($values)) / $count; } } if (!function_exists('array_single')) { /** * Возвращает единственный элемент массива * * @param array $array Массив с одним элементом * @return mixed Единственный элемент массива * @throws Exception Если массив содержит не один элемент */ function array_single(array $array): mixed { if (count($array) !== 1) { throw new Exception('Expected 1 element, got ' . count($array)); } return array_values($array)[0]; } } if (!function_exists('array_partition')) { /** * Разделяет массив на два по условию * * @param array $array Входной массив * @param callable|string $callback Функция для проверки условия * @return array Массив с двумя элементами: успешные и неуспешные */ function array_partition(array $array, callable|string $callback): array { if (is_string($callback)) { $callback = static fn ($item) => $item[$callback] ?? null; } $successList = []; $failList = []; foreach ($array as $key => $value) { if ($callback($value, $key)) { $successList[$key] = $value; } else { $failList[$key] = $value; } } return [$successList, $failList]; } } if (!function_exists('array_multidim_unique')) { /** * Удаляет дубликаты из многомерного массива по ключу * * @param array $arr Многомерный массив * @param string $key Ключ для определения уникальности * @return array Массив без дубликатов */ function array_multidim_unique(array $arr, string $key): array { $tempArray = []; $keyArray = []; $i = 0; foreach ($arr as $idx => $item) { $subValue = $item[$key]; if (!in_array($subValue, $keyArray)) { $keyArray[$i] = $subValue; $tempArray[$idx] = $item; } ++$i; } return $tempArray; } } if (!function_exists('hashmap')) { /** * Возвращает хеш-таблицу, состоящую из ключей => значений указанных полей массива * * @param mixed[] $array Массив * @param int|string|null $keyField Имя поля, значения коего станут ключами * @param int|string|null $valueField Имя поля, значения коего станут значениями * @return array Хеш-таблица */ function hashmap(array $array, int|string|null $keyField, int|string|null $valueField): array { $keys = array_column($array, $keyField) ?: []; if (empty($keys)) { return []; } $values = array_column($array, $valueField) ?: array_fill(0, count($keys), null); return array_combine($keys, $values); } } if (!function_exists('key_by')) { /** * Возвращает исходный массив, индексированный по указанному полю * * @param mixed[] $array Массив * @param int|string|null $keyField Имя поля, значения коего станут ключами * @return array Индексированный массив */ function key_by(array $array, int|string|null $keyField): array { return array_combine(array_column($array, $keyField), $array); } } if (!function_exists('tree')) { /** * Строит дерево из плоского списка без рекурсии * * @param array $flatlist Список всех нод, из которых нужно построить дерево * @param string $fk Поле потомка, которое ссылается на ключевое родителя * @param string $pk Имя ключевого поля родителя * @return array Дерево */ function tree( array $flatlist, string $fk = 'hid', string $pk = 'id', ): array { $tree = []; foreach ($flatlist as &$node) { $references[$node[$pk]] = &$node; if (empty($node[$fk])) { $tree[$node[$pk]] = &$node; } else { $references[$node[$fk]]['children'][] = &$node; } } return array_values($tree); } } if (!function_exists('flatten')) { /** * Разворачивает дерево в плоский список без рекурсии * * @param array $tree Дерево (например, результат функции tree()) * @param string $branching Ключ ноды, под которым находится массив с дочерними нодами * @param string|null $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем * @return array Плоский список */ function flatten( array $tree, string $branching = 'children', ?string $leafProperty = 'is_leaf', ): array { $result = []; while (count($tree)) { $node = array_shift($tree); $needToSkip = !empty($leafProperty) && isset($node[$leafProperty]) && $node[$leafProperty] === false; if (!empty($node[$branching])) { $tree = array_merge($tree, $node[$branching]); unset($node[$branching]); } elseif ($needToSkip) { continue; } $result[] = $node; } return $result; } } if (!function_exists('hierarchy')) { /** * Восстанавливает иерархию ноды в плоском списке * * @param array $flatlist Список всех нод, среди коих есть нужная нода и её родители * @param array $node Нода, родителей коей необходимо найти в общем списке * @param string $fk Поле потомка, которое ссылается на ключевое родителя * @param string $pk Имя ключевого поля родителя * @return array Плоский ассоциированный список с потомком и его родителями */ function hierarchy( array $flatlist, array $node, string $fk = 'hid', string $pk = 'id', ): array { $stack[$node[$pk]] = $node; while (isset($flatlist[$node[$fk]])) { $node = $flatlist[$node[$fk]]; $stack[$node[$pk]] = $node; } return $stack; } } if (!function_exists('clear_tree')) { /** * Рекурсивно чистит дерево от пустых родителей * * @param array $node Нода, которая должна быть обработана * @param string $branching Ключ ноды, под которым находится массив с дочерними нодами * @param string|null $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем * @return array|null Обработанная нода с хотя бы одним потомком либо null */ function clear_tree( array $node, string $branching = 'children', ?string $leafProperty = 'is_leaf', ): ?array { if (empty($node[$leafProperty]) && empty($node[$branching])) { unset($node); return null; } if (isset($node[$branching])) { foreach ($node[$branching] as $key => $child) { $value = clear_tree($child); if (is_null($value)) { unset($node[$branching][$key]); /** @noinspection PhpConditionAlreadyCheckedInspection */ if (empty($node[$branching])) { unset($node); return null; } } else { $node[$branching][$key] = $value; } } } return $node; } } if (!function_exists('descendants')) { /** * Восстанавливает всех потомков ноды в плоском списке * * @param array $flatlist Список всех нод, среди коих есть нужная нода и её потомки * @param int $id Индекс в списке ноды, потомков которой необходимо найти * @param string $fk ссылка, которое ссылается на ключевое родителя * @param string $pk Имя ключевого поля родителя * @return array Плоский ассоциированный список с потомком и его родителями */ function descendants( array $flatlist, int $id, string $fk = 'hid', string $pk = 'id', ): array { $descendants = []; $stack = [$id]; while (!empty($stack)) { $currentId = array_pop($stack); foreach ($flatlist as $item) { if ($item[$fk] === $currentId) { $descendants[$item[$pk]] = $item; $stack[] = $item[$pk]; } } } $descendants[$id] = $flatlist[$id]; return $descendants; } }