Files
web/app/helpers.php
2026-01-03 01:12:18 +08:00

1532 lines
50 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of m3u.su project
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
*/
declare(strict_types=1);
use App\Core\IniFile;
use App\Core\Kernel;
use Carbon\Carbon;
use Random\RandomException;
use Slim\App;
if (!function_exists('root_path')) {
/**
* Возвращает абсолютный путь к корневой директории приложения.
*
* @param string $path относительный путь для добавления к корневому
* @return string абсолютный путь
*/
function root_path(string $path = ''): string
{
$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? dirname(__DIR__);
return rtrim(sprintf('%s/%s', dirname($documentRoot), $path), '/');
}
}
if (!function_exists('config_path')) {
/**
* Возвращает абсолютный путь к директории конфигурации приложения.
*
* @param string $path относительный путь для добавления к директории конфигурации
* @return string абсолютный путь
*/
function config_path(string $path = ''): string
{
return root_path("config/{$path}");
}
}
if (!function_exists('cache_path')) {
/**
* Возвращает абсолютный путь к директории кэша приложения.
*
* @param string $path относительный путь для добавления к директории кэша
* @return string абсолютный путь
*/
function cache_path(string $path = ''): string
{
return root_path("cache/{$path}");
}
}
if (!function_exists('base_url')) {
/**
* Возвращает базовый URL приложения.
*
* @param string $route дополнительный маршрут, который будет добавлен к базовому URL
* @return string полный URL
*/
function base_url(string $route = ''): string
{
return rtrim(sprintf('%s/%s', config('app.base_url'), $route), '/');
}
}
if (!function_exists('kernel')) {
/**
* Возвращает синглтон-экземпляр ядра приложения.
*
* @return Kernel экземпляр ядра приложения
*/
function kernel(): Kernel
{
return Kernel::instance();
}
}
if (!function_exists('app')) {
/**
* Возвращает синглтон-экземпляр Slim-приложения.
*
* @return App экземпляр Slim-приложения
*/
function app(): App
{
return Kernel::instance()->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 массив или строка в формате `<file|class>:<func>():<line>`
* @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('/(?<!^)[A-Z]/', '_$0', $input);
return strtolower($input);
}
}
if (!function_exists('snake2camel')) {
/**
* Переводит строку из snake_case в camelCase
*
* @param string $input Строка в snake_case
* @return string Строка в camelCase
*/
function snake2camel(string $input): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
}
}
if (!function_exists('as_data_url')) {
/**
* Создает data URL для данных
*
* @param string $data Данные для создания URL
* @param string $mimeType MIME-тип данных
* @return string Data URL
*/
function as_data_url(string $data, string $mimeType = 'text/plain'): string
{
return "data://{$mimeType},{$data}";
}
}
if (!function_exists('is_soft_null')) {
/**
* Проверяет, является ли значение мягким null
*
* @param mixed $value Проверяемое значение
* @return bool True, если значение является мягким null
*/
function is_soft_null(mixed $value): bool
{
return is_null($value) || is_string($value) && strtolower(trim($value)) === 'null';
}
}
if (!function_exists('is_blank')) {
/**
* Проверяет значение на пустоту
*
* @param mixed $value Проверяемое значение
* @return bool True, если значение пустое
*/
function is_blank(mixed $value): bool
{
if ($value === null) {
return true;
}
if (is_string($value)) {
return trim($value) === '';
}
if (is_numeric($value) || is_bool($value)) {
return false;
}
if (is_array($value) || ($value instanceof Countable)) {
return count($value) === 0;
}
if (is_object($value)) {
return false;
}
return empty($value);
}
}
if (!function_exists('is_filled')) {
/**
* Проверяет, что значение заполнено
*
* @param mixed $value Проверяемое значение
* @return bool True, если значение заполнено
*/
function is_filled(mixed $value): bool
{
return !is_blank($value);
}
}
if (!function_exists('is_valid_email')) {
/**
* Проверяет, является ли строка корректным email адресом
*
* @param string $email Email для проверки
* @return bool True, если email корректный
*/
function is_valid_email(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
}
if (!function_exists('number_format_local')) {
/**
* Форматирует число с разделителями тысяч
*
* @param int|float $number Число для форматирования
* @param int $decimals Количество знаков после запятой
* @param string $decPoint Символ для десятичной точки
* @param string $thousandsSep Символ-разделитель тысяч
* @return string Отформатированное число
*/
function number_format_local(
float|int $number,
int $decimals = 0,
string $decPoint = '.',
string $thousandsSep = ' ',
): string {
return number_format($number, $decimals, $decPoint, $thousandsSep);
}
}
if (!function_exists('format_bytes')) {
/**
* Форматирует размер файла в удобочитаемый вид
*
* @param int $bytes Размер в байтах
* @param int $precision Точность (количество знаков после запятой)
* @return string Отформатированный размер файла
*/
function format_bytes(int $bytes, int $precision = 2): string
{
$units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ'];
for ($i = 0; $bytes > 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;
}
}