Files
web/app/helpers.php
T
2025-12-09 11:28:56 +08:00

1530 lines
50 KiB
PHP

<?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): string|array
{
$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(
int|float $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;
} elseif ($lastDigit >= 2 && $lastDigit <= 4) {
return $form2;
} else {
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, string|int $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 null|string $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 null|string $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем
* @return null|array Обработанная нода с хотя бы одним потомком либо 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;
}
}