1532 lines
50 KiB
PHP
1532 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): 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;
|
||
}
|
||
}
|