From aabad9d74407ed3f0f26bf25a286498b3dfb689e Mon Sep 17 00:00:00 2001 From: Anthony Axenov Date: Sat, 3 Jan 2026 01:12:18 +0800 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=B8=D0=BD=D1=82=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .php-cs-fixer.php | 2 +- app/Controllers/ApiController.php | 10 +- app/Controllers/BasicController.php | 3 + app/Controllers/BotController.php | 1 + app/Controllers/WebController.php | 5 +- app/Core/IniFile.php | 8 +- app/Core/Kernel.php | 141 +++++++++--------- app/Core/StatisticsService.php | 63 ++++---- app/Core/TwigExtention.php | 3 +- app/Errors/ErrorHandler.php | 62 ++++---- app/Errors/InvalidTelegramSecretException.php | 3 +- app/Errors/PlaylistNotFoundException.php | 3 +- app/helpers.php | 80 +++++----- config/app.php | 1 + config/bot.php | 1 + config/cache.php | 1 + config/routes.php | 4 +- config/twig.php | 1 + linter | 21 ++- public/index.php | 1 + 20 files changed, 224 insertions(+), 190 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 09732ce..7a4066e 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -725,7 +725,7 @@ $finder = (new Finder()) ->notPath($excludeNames); // исключаем файлы return (new Config()) - // спорная фигня, пока не + // спорная фигня, пока не активирую // ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()); ->setFinder($finder) ->setRules($rules) // ставим правила diff --git a/app/Controllers/ApiController.php b/app/Controllers/ApiController.php index 5728cd0..d606bbf 100644 --- a/app/Controllers/ApiController.php +++ b/app/Controllers/ApiController.php @@ -1,4 +1,5 @@ responseJson($response, 200, $playlist); } catch (PlaylistNotFoundException $e) { return $this->responseJsonError($response, 404, $e); @@ -65,7 +67,7 @@ class ApiController extends BasicController return $response->withStatus(404); } - $filePath = cache_path("qr-codes/$code.jpg"); + $filePath = cache_path("qr-codes/{$code}.jpg"); if (file_exists($filePath)) { $raw = file_get_contents($filePath); } else { @@ -74,13 +76,14 @@ class ApiController extends BasicController 'outputType' => QRCode::OUTPUT_IMAGE_JPG, 'eccLevel' => QRCode::ECC_L, ]); - $data = config('app.mirror_url') ? mirror_url("$code.m3u") : base_url("$code.m3u"); + $data = config('app.mirror_url') ? mirror_url("{$code}.m3u") : base_url("{$code}.m3u"); $raw = new QRCode($options)->render($data, $filePath); $raw = base64_decode(str_replace('data:image/jpg;base64,', '', $raw)); } $mime = mime_content_type($filePath); $response->getBody()->write($raw); + return $response->withStatus(200) ->withHeader('Content-Type', $mime); } @@ -116,10 +119,11 @@ class ApiController extends BasicController function getSize(string $directory): int { $size = 0; - foreach (glob($directory . '/*') as $path){ + foreach (glob($directory . '/*') as $path) { is_file($path) && $size += filesize($path); is_dir($path) && $size += getSize($path); } + return $size; } diff --git a/app/Controllers/BasicController.php b/app/Controllers/BasicController.php index c16fc51..e049c03 100644 --- a/app/Controllers/BasicController.php +++ b/app/Controllers/BasicController.php @@ -1,4 +1,5 @@ getBody()->write($json); + return $response->withStatus($status) ->withHeader('Content-Type', 'application/json'); } @@ -99,6 +101,7 @@ class BasicController array $data = [], ): ResponseInterface { $view = Twig::fromRequest($request); + return $view->render($response, $template, $data); } } diff --git a/app/Controllers/BotController.php b/app/Controllers/BotController.php index b45c3fe..e9e1f07 100644 --- a/app/Controllers/BotController.php +++ b/app/Controllers/BotController.php @@ -1,4 +1,5 @@ 0) { - $pageCurrent = (int)($request->getAttributes()['page'] ?? $request->getQueryParams()['page'] ?? 1); + $pageCurrent = (int) ($request->getAttributes()['page'] ?? $request->getQueryParams()['page'] ?? 1); $pageCount = ceil($count / $pageSize); $offset = max(0, ($pageCurrent - 1) * $pageSize); $ini = array_slice($ini, $offset, $pageSize, true); @@ -76,6 +77,7 @@ class WebController extends BasicController try { $playlist = ini()->getPlaylist($code); + return $response->withHeader('Location', $playlist['url']); } catch (Throwable) { return $this->notFound($request, $response); @@ -99,6 +101,7 @@ class WebController extends BasicController try { $playlist = ini()->getPlaylist($code); + return $this->view($request, $response, 'details.twig', ['playlist' => $playlist]); } catch (PlaylistNotFoundException) { return $this->notFound($request, $response); diff --git a/app/Core/IniFile.php b/app/Core/IniFile.php index f218dc9..40c8d59 100644 --- a/app/Core/IniFile.php +++ b/app/Core/IniFile.php @@ -1,4 +1,5 @@ playlists) && $this->load(); $data = redis()->get($code); + return $this->initPlaylist($code, $data); } @@ -96,11 +98,11 @@ class IniFile protected function initPlaylist(string $code, array|false $data): array { if ($data === false) { - $raw = $this->playlists[$code] + $raw = $this->playlists[$code] ?? throw new PlaylistNotFoundException($code); $data = [ 'code' => $code, - 'name' => $raw['name'] ?? "Плейлист #$code", + 'name' => $raw['name'] ?? "Плейлист #{$code}", 'description' => $raw['desc'] ?? null, 'url' => $raw['pls'], 'source' => $raw['src'] ?? null, @@ -182,7 +184,7 @@ class IniFile return array_any( $badAttributes, - static fn (string $badAttribute) => preg_match_all("/$badAttribute/", $string) >= 1, + static fn (string $badAttribute) => preg_match_all("/{$badAttribute}/", $string) >= 1, ); } } diff --git a/app/Core/Kernel.php b/app/Core/Kernel.php index 5268368..92d935e 100644 --- a/app/Core/Kernel.php +++ b/app/Core/Kernel.php @@ -1,4 +1,5 @@ cache)) { + return $this->cache; + } + + $options = [ + 'host' => $this->config['cache']['host'], + 'port' => (int) $this->config['cache']['port'], + ]; + + if (!empty($this->config['cache']['password'])) { + $options['auth'] = $this->config['cache']['password']; + } + + $this->cache = new Redis($options); + $this->cache->select((int) $this->config['cache']['db']); + $this->cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); + + return $this->cache; + } + + /** + * Возвращает значение из конфига + * + * @param string $key Ключ в формате "config.key" + * @param mixed|null $default Значение по умолчанию + * @return mixed + */ + public function config(string $key, mixed $default = null): mixed + { + $parts = explode('.', $key); + + return $this->config[$parts[0]][$parts[1]] ?? $default; + } + + /** + * Возвращает объект приложения + * + * @return App + */ + public function app(): App + { + return $this->app; + } + + /** + * Возвращает объект ini-файла + * + * @return IniFile + */ + public function ini(): IniFile + { + return $this->iniFile ??= new IniFile(); + } + /** * Загружает файл .env или .env.$env * @@ -88,12 +151,13 @@ final class Kernel */ protected function loadDotEnvFile(string $env = ''): array { - $filename = empty($env) ? '.env' : ".env.$env"; + $filename = empty($env) ? '.env' : ".env.{$env}"; if (!file_exists(root_path($filename))) { return []; } $dotenv = Dotenv::createMutable(root_path(), $filename); + return $dotenv->safeLoad(); } @@ -138,7 +202,7 @@ final class Kernel default => throw new InvalidArgumentException(sprintf('Неверный HTTP метод %s', $method)) }; - $definition = $this->app->$func($route['path'], $route['handler']); + $definition = $this->app->{$func}($route['path'], $route['handler']); } if (!empty($route['name'])) { @@ -163,65 +227,4 @@ final class Kernel $twig->addExtension(new DebugExtension()); } } - - /** - * Возвращает объект подключения к Redis - * - * @return Redis - * @see https://github.com/phpredis/phpredis/?tab=readme-ov-file - */ - public function redis(): Redis - { - if (!empty($this->cache)) { - return $this->cache; - } - - $options = [ - 'host' => $this->config['cache']['host'], - 'port' => (int)$this->config['cache']['port'], - ]; - - if (!empty($this->config['cache']['password'])) { - $options['auth'] = $this->config['cache']['password']; - } - - $this->cache = new Redis($options); - $this->cache->select((int)$this->config['cache']['db']); - $this->cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); - - return $this->cache; - } - - /** - * Возвращает значение из конфига - * - * @param string $key Ключ в формате "config.key" - * @param mixed|null $default Значение по умолчанию - * @return mixed - */ - public function config(string $key, mixed $default = null): mixed - { - $parts = explode('.', $key); - return $this->config[$parts[0]][$parts[1]] ?? $default; - } - - /** - * Возвращает объект приложения - * - * @return App - */ - public function app(): App - { - return $this->app; - } - - /** - * Возвращает объект ini-файла - * - * @return IniFile - */ - public function ini(): IniFile - { - return $this->iniFile ??= new IniFile(); - } } diff --git a/app/Core/StatisticsService.php b/app/Core/StatisticsService.php index 2140c22..3b10668 100644 --- a/app/Core/StatisticsService.php +++ b/app/Core/StatisticsService.php @@ -1,4 +1,5 @@ channels = $this->getAllChannels(); } - protected function getPlaylistsByField(string $field, int|string|bool|null $value): array + /** + * Обрабатывает команду /stats + * + * @return bool + * @throws Exception + */ + public function get(): array + { + return [ + 'playlists' => [ + 'all' => count($this->playlists), + 'online' => count($this->getPlaylistsByField('isOnline', true)), + 'offline' => count($this->getPlaylistsByField('isOnline', false)), + 'unknown' => count($this->getPlaylistsByField('isOnline', null)), + 'adult' => count($this->getPlaylistsByTag('adult')), + 'hasCatchup' => count($this->getPlaylistsByField('hasCatchup', true)), + 'hasTvg' => count($this->getPlaylistsByField('hasTvg', true)), + 'groupped' => count($this->getPlaylistsWithGroups()), + 'latest' => $this->getLatestPlaylist(), + ], + 'channels' => [ + 'all' => $this->getAllChannelsCount(), + 'online' => count($this->getChannelsByField('isOnline', true)), + 'offline' => count($this->getChannelsByField('isOnline', false)), + 'adult' => count($this->getChannelsByTag('adult')), + ], + ]; + } + + protected function getPlaylistsByField(string $field, bool|int|string|null $value): array { return array_filter( $this->playlists, @@ -85,7 +115,7 @@ class StatisticsService return count($this->channels); } - protected function getChannelsByField(string $field, int|string|bool|null $value): array + protected function getChannelsByField(string $field, bool|int|string|null $value): array { return array_filter( $this->channels, @@ -100,33 +130,4 @@ class StatisticsService static fn (array $channel) => in_array($tag, $channel['tags']), ); } - - /** - * Обрабатывает команду /stats - * - * @return bool - * @throws Exception - */ - public function get(): array - { - return [ - 'playlists' => [ - 'all' => count($this->playlists), - 'online' => count($this->getPlaylistsByField('isOnline', true)), - 'offline' => count($this->getPlaylistsByField('isOnline', false)), - 'unknown' => count($this->getPlaylistsByField('isOnline', null)), - 'adult' => count($this->getPlaylistsByTag('adult')), - 'hasCatchup' => count($this->getPlaylistsByField('hasCatchup', true)), - 'hasTvg' => count($this->getPlaylistsByField('hasTvg', true)), - 'groupped' => count($this->getPlaylistsWithGroups()), - 'latest' => $this->getLatestPlaylist(), - ], - 'channels' => [ - 'all' => $this->getAllChannelsCount(), - 'online' => count($this->getChannelsByField('isOnline', true)), - 'offline' => count($this->getChannelsByField('isOnline', false)), - 'adult' => count($this->getChannelsByTag('adult')), - ], - ]; - } } diff --git a/app/Core/TwigExtention.php b/app/Core/TwigExtention.php index bd4d254..959cd1f 100644 --- a/app/Core/TwigExtention.php +++ b/app/Core/TwigExtention.php @@ -1,4 +1,5 @@ payload($exception, $displayErrorDetails); - - $response = app()->getResponseFactory()->createResponse(); - $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); - - return $response; - } - /** * Возвращает структуру исключения для контекста * @@ -63,7 +36,7 @@ class ErrorHandler extends SlimErrorHandler 'class' => $e::class, 'file' => $e->getFile(), 'line' => $e->getLine(), - 'trace' => $e->getTrace() + 'trace' => $e->getTrace(), ]; return $result; @@ -94,4 +67,31 @@ class ErrorHandler extends SlimErrorHandler return $result; } + + /** + * Логирует ошибку и отдаёт JSON-ответ с необходимым содержимым + * + * @param ServerRequestInterface $request + * @param Throwable $exception + * @param bool $displayErrorDetails + * @param bool $logErrors + * @param bool $logErrorDetails + * @param LoggerInterface|null $logger + * @return ResponseInterface + */ + public function __invoke( + ServerRequestInterface $request, + Throwable $exception, + bool $displayErrorDetails, + bool $logErrors, + bool $logErrorDetails, + ?LoggerInterface $logger = null + ): ResponseInterface { + $payload = $this->payload($exception, $displayErrorDetails); + + $response = app()->getResponseFactory()->createResponse(); + $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); + + return $response; + } } diff --git a/app/Errors/InvalidTelegramSecretException.php b/app/Errors/InvalidTelegramSecretException.php index 984d5e7..d6ba77a 100644 --- a/app/Errors/InvalidTelegramSecretException.php +++ b/app/Errors/InvalidTelegramSecretException.php @@ -1,4 +1,5 @@ :():` * @return string|array */ - function here(bool $asArray = false): string|array + function here(bool $asArray = false): array|string { $trace = debug_backtrace(!DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 2); @@ -386,7 +387,7 @@ if (!function_exists('as_data_url')) { */ function as_data_url(string $data, string $mimeType = 'text/plain'): string { - return "data://$mimeType,$data"; + return "data://{$mimeType},{$data}"; } } @@ -473,7 +474,7 @@ if (!function_exists('number_format_local')) { * @return string Отформатированное число */ function number_format_local( - int|float $number, + float|int $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ' ', @@ -494,7 +495,7 @@ if (!function_exists('format_bytes')) { { $units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ']; - for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { + for ($i = 0; $bytes > 1024 && $i < count($units) - 1; ++$i) { $bytes /= 1024; } @@ -581,11 +582,12 @@ if (!function_exists('get_noun_form')) { if ($lastDigit === 1) { return $form1; - } elseif ($lastDigit >= 2 && $lastDigit <= 4) { - return $form2; - } else { - return $form5; } + if ($lastDigit >= 2 && $lastDigit <= 4) { + return $form2; + } + + return $form5; } } @@ -605,7 +607,7 @@ if (!function_exists('random_string')) { $result = ''; $max = strlen($chars) - 1; - for ($i = 0; $i < $length; $i++) { + for ($i = 0; $i < $length; ++$i) { $result .= $chars[random_int(0, $max)]; } @@ -702,7 +704,7 @@ if (!function_exists('recast')) { function recast(string $className, stdClass &$object): mixed { if (!class_exists($className)) { - throw new InvalidArgumentException("Class not found: $className"); + throw new InvalidArgumentException("Class not found: {$className}"); } $new = new $className(); @@ -769,7 +771,7 @@ if (!function_exists('mb_count_chars')) { for ($i = 0; $i < $length; ++$i) { $char = mb_substr($string, $i, 1, 'UTF-8'); !array_key_exists($char, $unique) && $unique[$char] = 0; - $unique[$char]++; + ++$unique[$char]; } return $unique; @@ -853,7 +855,7 @@ if (!function_exists('array_last')) { return $array[$lastKey]; } - for ($i = count($keys) - 1; $i >= 0; $i--) { + for ($i = count($keys) - 1; $i >= 0; --$i) { $key = $keys[$i]; if ($callback($array[$key], $key)) { return $array[$key]; @@ -916,7 +918,7 @@ if (!function_exists('array_get')) { * @param mixed $default Значение по умолчанию * @return mixed Значение из массива или значение по умолчанию */ - function array_get(array $array, string|int $key, mixed $default = null): mixed + function array_get(array $array, int|string $key, mixed $default = null): mixed { return $array[$key] ?? $default; } @@ -1118,7 +1120,7 @@ if (!function_exists('array_recursive_diff')) { $aReturn[$key] = $aRecursiveDiff; } } else { - if ($value != $b[$key]) { + if ($value !== $b[$key]) { $aReturn[$key] = $value; } } @@ -1400,7 +1402,7 @@ if (!function_exists('flatten')) { * * @param array $tree Дерево (например, результат функции tree()) * @param string $branching Ключ ноды, под которым находится массив с дочерними нодами - * @param null|string $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем + * @param string|null $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем * @return array Плоский список */ function flatten( @@ -1459,8 +1461,8 @@ if (!function_exists('clear_tree')) { * * @param array $node Нода, которая должна быть обработана * @param string $branching Ключ ноды, под которым находится массив с дочерними нодами - * @param null|string $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем - * @return null|array Обработанная нода с хотя бы одним потомком либо null + * @param string|null $leafProperty Ключ ноды, значение коего определяет является ли каждая нода родителем + * @return array|null Обработанная нода с хотя бы одним потомком либо null */ function clear_tree( array $node, diff --git a/config/app.php b/config/app.php index cba193c..dfbff5a 100644 --- a/config/app.php +++ b/config/app.php @@ -1,4 +1,5 @@ 'not-found', ], ]; - diff --git a/config/twig.php b/config/twig.php index 922c4e6..55a8d23 100644 --- a/config/twig.php +++ b/config/twig.php @@ -1,4 +1,5 @@