diff --git a/FAQ.md b/FAQ.md index 8e893e6e..193871c0 100644 --- a/FAQ.md +++ b/FAQ.md @@ -275,22 +275,21 @@ $ docker exec -ti watchstate console system:env These environment variables relates to the tool itself, you can load them via the recommended methods. -| Key | Type | Description | Default | -|--------------------------|---------|-------------------------------------------------------------------------|--------------------| -| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` | -| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` | -| WS_TZ | string | Set timezone. | `UTC` | -| WS_CRON_{TASK} | bool | Enable {task} task. Value casted to bool. | `false` | -| WS_CRON_{TASK}_AT | string | When to run {task} task. Valid Cron Expression Expected. | `*/1 * * * *` | -| WS_CRON_{TASK}_ARGS | string | Flags to pass to the {task} command. | `-v` | -| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` | -| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` | -| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` | -| WS_WEBHOOK_DUMP_REQUEST | bool | If enabled, will dump all received requests. | `false` | -| WS_EPISODES_DISABLE_GUID | bool | Disable external id parsing for episodes and rely on relative ids. | `true` | -| WS_TRUST_PROXY | bool | Trust `WS_TRUST_HEADER` ip. Value casted to bool. | `false` | -| WS_TRUST_HEADER | string | Which header contain user true IP. | `X-Forwarded-For` | -| WS_LIBRARY_SEGMENT | integer | Paginate backend library items request. Per request get total X number. | `1000` | +| Key | Type | Description | Default | +|-------------------------|---------|-------------------------------------------------------------------------|--------------------| +| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` | +| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` | +| WS_TZ | string | Set timezone. | `UTC` | +| WS_CRON_{TASK} | bool | Enable {task} task. Value casted to bool. | `false` | +| WS_CRON_{TASK}_AT | string | When to run {task} task. Valid Cron Expression Expected. | `*/1 * * * *` | +| WS_CRON_{TASK}_ARGS | string | Flags to pass to the {task} command. | `-v` | +| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` | +| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` | +| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` | +| WS_WEBHOOK_DUMP_REQUEST | bool | If enabled, will dump all received requests. | `false` | +| WS_TRUST_PROXY | bool | Trust `WS_TRUST_HEADER` ip. Value casted to bool. | `false` | +| WS_TRUST_HEADER | string | Which header contain user true IP. | `X-Forwarded-For` | +| WS_LIBRARY_SEGMENT | integer | Paginate backend library items request. Per request get total X number. | `1000` | > [!IMPORTANT] > for environment variables that has `{TASK}` tag, you **MUST** replace it with one diff --git a/src/Commands/State/ExportCommand.php b/src/Commands/State/ExportCommand.php index 7a2f3893..5f146bf2 100644 --- a/src/Commands/State/ExportCommand.php +++ b/src/Commands/State/ExportCommand.php @@ -17,13 +17,19 @@ use App\Libs\Routable; use App\Libs\Stream; use Monolog\Logger; use Psr\Log\LoggerInterface as iLogger; -use RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; use Throwable; +/** + * Class ExportCommand + * + * Command for exporting play state to backends. + * + * @package App\Console\Commands\State + */ #[Routable(command: self::ROUTE)] class ExportCommand extends Command { @@ -31,6 +37,14 @@ class ExportCommand extends Command public const TASK_NAME = 'export'; + /** + * Class Constructor. + * + * @param iDB $db The instance of the iDB class. + * @param DirectMapper $mapper The instance of the DirectMapper class. + * @param QueueRequests $queue The instance of the QueueRequests class. + * @param iLogger $logger The instance of the iLogger class. + */ public function __construct( private iDB $db, private DirectMapper $mapper, @@ -43,6 +57,9 @@ class ExportCommand extends Command parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -100,15 +117,30 @@ class ExportCommand extends Command ); } + /** + * Make sure the command is not running in parallel. + * + * @param InputInterface $input The input object containing the command data. + * @param OutputInterface $output The output object for displaying command output. + * + * @return int The exit code of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { return $this->single(fn(): int => $this->process($input, $output), $output); } + /** + * Process the command by pulling and comparing status and then pushing. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ protected function process(InputInterface $input, OutputInterface $output): int { if (null !== ($logfile = $input->getOption('logfile')) && true === ($this->logger instanceof Logger)) { - $this->logger->pushHandler(new StreamLogHandler(new Stream(fopen($logfile, 'a')), $output)); + $this->logger->pushHandler(new StreamLogHandler(new Stream($logfile, 'a'), $output)); } // -- Use Custom servers.yaml file. @@ -116,7 +148,7 @@ class ExportCommand extends Command try { $custom = true; Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config))); - } catch (RuntimeException $e) { + } catch (\App\Libs\Exceptions\RuntimeException $e) { $output->writeln(sprintf('%s', $e->getMessage())); return self::FAILURE; } @@ -448,13 +480,22 @@ class ExportCommand extends Command copy($config, $config . '.bak'); } - file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2)); + $stream = new Stream($config, 'w'); + $stream->write(Yaml::dump(Config::get('servers', []), 8, 2)); + $stream->close(); } - return self::SUCCESS; } + /** + * Push entities to backends if applicable. + * + * @param array $backends An array of backends. + * @param array $entities An array of entities to be pushed. + * + * @return int The success status code. + */ protected function push(array $backends, array $entities): int { $this->logger->notice('Push mode start.', [ @@ -477,10 +518,10 @@ class ExportCommand extends Command } /** - * Pull and compare status and then push. + * Fallback to export mode if push mode is not supported for the backend. * - * @param array $backends - * @param InputInterface $input + * @param array $backends An array of backends to export data to. + * @param InputInterface $input The input containing export options. */ protected function export(array $backends, InputInterface $input): void { diff --git a/src/Commands/State/ImportCommand.php b/src/Commands/State/ImportCommand.php index e1d1eb2f..d7dd1d7c 100644 --- a/src/Commands/State/ImportCommand.php +++ b/src/Commands/State/ImportCommand.php @@ -20,7 +20,6 @@ use App\Libs\Routable; use App\Libs\Stream; use Monolog\Logger; use Psr\Log\LoggerInterface as iLogger; -use RuntimeException; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; @@ -30,6 +29,11 @@ use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\HttpClient\ResponseInterface; use Throwable; +/** + * Class ImportCommand + * + * This command imports metadata and play state of items from backends and updates the local database. + */ #[Routable(command: self::ROUTE)] class ImportCommand extends Command { @@ -37,6 +41,13 @@ class ImportCommand extends Command public const TASK_NAME = 'import'; + /** + * Class Constructor. + * + * @param iDB $db The database interface object. + * @param iImport $mapper The import interface object. + * @param iLogger $logger The logger interface object. + */ public function __construct(private iDB $db, private iImport $mapper, private iLogger $logger) { set_time_limit(0); @@ -45,6 +56,9 @@ class ImportCommand extends Command parent::__construct(); } + /** + * Configure the method. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -201,15 +215,31 @@ class ImportCommand extends Command ); } + /** + * Make sure the command is not running in parallel. + * + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * + * @return int The status code of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { return $this->single(fn(): int => $this->process($input, $output), $output); } + /** + * Import the state from the backends. + * + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * + * @return int The return status code. + */ protected function process(InputInterface $input, OutputInterface $output): int { if (null !== ($logfile = $input->getOption('logfile')) && true === ($this->logger instanceof Logger)) { - $this->logger->pushHandler(new StreamLogHandler(new Stream(fopen($logfile, 'a')), $output)); + $this->logger->pushHandler(new StreamLogHandler(new Stream($logfile, 'a'), $output)); } // -- Use Custom servers.yaml file. @@ -217,7 +247,7 @@ class ImportCommand extends Command try { $custom = true; Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config))); - } catch (RuntimeException $e) { + } catch (\App\Libs\Exceptions\RuntimeException $e) { $output->writeln(sprintf('%s', $e->getMessage())); return self::FAILURE; } @@ -482,7 +512,9 @@ class ImportCommand extends Command copy($config, $config . '.bak'); } - file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2)); + $stream = new Stream($config, 'w'); + $stream->write(Yaml::dump(Config::get('servers', []), 8, 2)); + $stream->close(); } if ($input->getOption('show-messages')) { diff --git a/src/Commands/State/ProgressCommand.php b/src/Commands/State/ProgressCommand.php index bebf4903..e536873d 100644 --- a/src/Commands/State/ProgressCommand.php +++ b/src/Commands/State/ProgressCommand.php @@ -13,11 +13,21 @@ use App\Libs\QueueRequests; use App\Libs\Routable; use Psr\Log\LoggerInterface as iLogger; use Psr\SimpleCache\CacheInterface as iCache; -use Psr\SimpleCache\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; +/** + * Class ProgressCommand + * + * This command is used to push user watch progress to export enabled backends. + * It should not be run manually and should be scheduled to run as a task. + * + * This command requires the watch progress metadata to be already saved in the database. + * If no metadata is available for a backend, + * the watch progress update won't be sent to that backend + */ #[Routable(command: self::ROUTE)] class ProgressCommand extends Command { @@ -25,6 +35,14 @@ class ProgressCommand extends Command public const TASK_NAME = 'progress'; + /** + * Class Constructor. + * + * @param iLogger $logger The logger instance. + * @param iCache $cache The cache instance. + * @param iDB $db The database instance. + * @param QueueRequests $queue The queue requests instance. + */ public function __construct( private iLogger $logger, private iCache $cache, @@ -37,6 +55,9 @@ class ProgressCommand extends Command parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -69,10 +90,12 @@ class ProgressCommand extends Command } /** + * Make sure the command is not running in parallel. + * * @param InputInterface $input * @param OutputInterface $output * @return int - * @throws InvalidArgumentException + * @throws \Psr\Cache\InvalidArgumentException if the cache key is not a legal value */ protected function runCommand(InputInterface $input, OutputInterface $output): int { @@ -80,7 +103,12 @@ class ProgressCommand extends Command } /** - * @throws InvalidArgumentException + * Run the command. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * @return int Returns the status code. + * @throws \Psr\Cache\InvalidArgumentException if the cache key is not a legal value */ protected function process(InputInterface $input, OutputInterface $output): int { @@ -219,7 +247,7 @@ class ProgressCommand extends Command ...$context, 'status_code' => $response->getStatusCode(), ]); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->logger->error( message: 'SYSTEM: Exception [{error.kind}] was thrown unhandled during [{backend}] request to change watch progress of {item.type} [{item.title}]. Error [{error.message} @ {error.file}:{error.line}].', context: [ @@ -266,12 +294,13 @@ class ProgressCommand extends Command } /** - * List Items. + * Renders and displays a list of items based on the specified output mode. * - * @param InputInterface $input - * @param OutputInterface $output - * @param array $items - * @return int + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * @param array $items An array of items to be listed. + * + * @return int The status code indicating the success of the method execution. */ private function listItems(InputInterface $input, OutputInterface $output, array $items): int { diff --git a/src/Commands/State/PushCommand.php b/src/Commands/State/PushCommand.php index 25a78200..5a315eff 100644 --- a/src/Commands/State/PushCommand.php +++ b/src/Commands/State/PushCommand.php @@ -14,11 +14,17 @@ use App\Libs\QueueRequests; use App\Libs\Routable; use Psr\Log\LoggerInterface as iLogger; use Psr\SimpleCache\CacheInterface as iCache; -use Psr\SimpleCache\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; +/** + * Class PushCommand + * + * This class represents a command that pushes webhook queued events. + * It sends change play state requests to the supported backends. + */ #[Routable(command: self::ROUTE)] class PushCommand extends Command { @@ -26,6 +32,16 @@ class PushCommand extends Command public const TASK_NAME = 'push'; + /** + * Constructor for the given class. + * + * @param iLogger $logger The logger instance. + * @param iCache $cache The cache instance. + * @param iDB $db The database instance. + * @param QueueRequests $queue The queue instance. + * + * @return void + */ public function __construct( private iLogger $logger, private iCache $cache, @@ -38,6 +54,9 @@ class PushCommand extends Command parent::__construct(); } + /** + * Configure command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -68,10 +87,13 @@ class PushCommand extends Command } /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws InvalidArgumentException + * Make sure the command is not running in parallel. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int Returns the process result status code. + * @throws \Psr\SimpleCache\InvalidArgumentException if the cache key is not a legal value. */ protected function runCommand(InputInterface $input, OutputInterface $output): int { @@ -79,7 +101,12 @@ class PushCommand extends Command } /** - * @throws InvalidArgumentException + * Process the queue items and send change play state requests to the supported backends. + * + * @param InputInterface $input The input interface. + * + * @return int Returns the process result status code. + * @throws \Psr\SimpleCache\InvalidArgumentException if the cache key is not a legal value. */ protected function process(InputInterface $input): int { @@ -197,7 +224,7 @@ class PushCommand extends Command } $this->logger->notice('SYSTEM: Marked [{backend}] [{item.title}] as [{play_state}].', $context); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->logger->error( message: 'SYSTEM: Exception [{error.kind}] was thrown unhandled during [{backend}] request to change play state of {item.type} [{item.title}]. Error [{error.message} @ {error.file}:{error.line}].', context: [ diff --git a/src/Commands/State/RequestsCommand.php b/src/Commands/State/RequestsCommand.php index fcb40443..b88e746a 100644 --- a/src/Commands/State/RequestsCommand.php +++ b/src/Commands/State/RequestsCommand.php @@ -12,13 +12,19 @@ use App\Libs\Options; use App\Libs\Routable; use Psr\Log\LoggerInterface as iLogger; use Psr\SimpleCache\CacheInterface as iCache; -use Psr\SimpleCache\InvalidArgumentException; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class RequestsCommand + * + * This class is responsible for processing queued HTTP requests. + * + * @package Your\Namespace + */ #[Routable(command: self::ROUTE)] class RequestsCommand extends Command { @@ -26,6 +32,13 @@ class RequestsCommand extends Command public const TASK_NAME = 'requests'; + /** + * Class constructor. + * + * @param iLogger $logger The logger object. + * @param iCache $cache The cache object. + * @param DirectMapper $mapper The DirectMapper object. + */ public function __construct(private iLogger $logger, private iCache $cache, private DirectMapper $mapper) { set_time_limit(0); @@ -34,6 +47,9 @@ class RequestsCommand extends Command parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -46,10 +62,13 @@ class RequestsCommand extends Command } /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws InvalidArgumentException + * Make sure the command is not running in parallel. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code of the command. + * @throws \Psr\Cache\InvalidArgumentException if the $key string is not a legal value */ protected function runCommand(InputInterface $input, OutputInterface $output): int { @@ -57,7 +76,13 @@ class RequestsCommand extends Command } /** - * @throws InvalidArgumentException + * Run the command. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code of the command. + * @throws \Psr\Cache\InvalidArgumentException if the $key string is not a legal value */ protected function process(InputInterface $input, OutputInterface $output): int { @@ -155,12 +180,13 @@ class RequestsCommand extends Command } /** - * List Items. + * Lists items based on the provided input and output. * - * @param InputInterface $input - * @param OutputInterface $output - * @param array $requests - * @return int + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * @param array $requests The array of requests. + * + * @return int Returns the success status code. */ private function listItems(InputInterface $input, OutputInterface $output, array $requests): int { diff --git a/src/Commands/System/EnvCommand.php b/src/Commands/System/EnvCommand.php index 3fae953b..6b688bc4 100644 --- a/src/Commands/System/EnvCommand.php +++ b/src/Commands/System/EnvCommand.php @@ -10,11 +10,19 @@ use App\Libs\Routable; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class EnvCommand + * + * This command displays the environment variables that were loaded during the execution of the tool. + */ #[Routable(command: self::ROUTE)] final class EnvCommand extends Command { public const ROUTE = 'system:env'; + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -78,13 +86,21 @@ final class EnvCommand extends Command ); } + /** + * Run the command. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $mode = $input->getOption('output'); $keys = []; foreach (getenv() as $key => $val) { - if (false === str_starts_with($key, 'WS_')) { + if (false === str_starts_with($key, 'WS_') && $key !== 'HTTP_PORT') { continue; } diff --git a/src/Commands/System/IndexCommand.php b/src/Commands/System/IndexCommand.php index 1588e0d6..db965d56 100644 --- a/src/Commands/System/IndexCommand.php +++ b/src/Commands/System/IndexCommand.php @@ -12,6 +12,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class IndexCommand + * + * This command ensures that the database has correct indexes. + */ #[Routable(command: self::ROUTE)] final class IndexCommand extends Command { @@ -19,11 +24,21 @@ final class IndexCommand extends Command public const TASK_NAME = 'indexes'; + /** + * Class constructor. + * + * @param iDB $db An instance of the iDB class. + */ public function __construct(private iDB $db) { parent::__construct(); } + /** + * Configure the command. + * + * @return void + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -56,6 +71,14 @@ final class IndexCommand extends Command ); } + /** + * Run a command. + * + * @param InputInterface $input An instance of the InputInterface interface. + * @param OutputInterface $output An instance of the OutputInterface interface. + * + * @return int The status code indicating the success or failure of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $this->db->ensureIndex([ diff --git a/src/Commands/System/LogsCommand.php b/src/Commands/System/LogsCommand.php index 2a986421..367ee466 100644 --- a/src/Commands/System/LogsCommand.php +++ b/src/Commands/System/LogsCommand.php @@ -6,8 +6,8 @@ namespace App\Commands\System; use App\Command; use App\Libs\Config; +use App\Libs\Exceptions\InvalidArgumentException; use App\Libs\Routable; -use Exception; use LimitIterator; use SplFileObject; use Symfony\Component\Console\Completion\CompletionInput; @@ -17,19 +17,33 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; +/** + * Class LogsCommand. + * + * This class is used to view and clear log files. + */ #[Routable(command: self::ROUTE)] final class LogsCommand extends Command { public const ROUTE = 'system:logs'; + /** + * @var array Constant array containing names of supported log files. + */ private const LOG_FILES = [ 'app', 'access', 'task' ]; + /** + * @var int The default limit of how many lines to show. + */ public const DEFAULT_LIMIT = 50; + /** + * Configure the command. + */ protected function configure(): void { $defaultDate = makeDate()->format('Ymd'); @@ -121,7 +135,15 @@ final class LogsCommand extends Command } /** - * @throws Exception + * Run the command. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code of the command. + * + * @throws InvalidArgumentException If the log type is not one of the supported log files. + * @throws InvalidArgumentException If the log date is not in the correct format. */ protected function runCommand(InputInterface $input, OutputInterface $output): int { @@ -132,7 +154,7 @@ final class LogsCommand extends Command $type = $input->getOption('type'); if (false === in_array($type, self::LOG_FILES)) { - throw new \RuntimeException( + throw new InvalidArgumentException( sprintf('Log type has to be one of the supported log files [%s].', implode(', ', self::LOG_FILES)) ); } @@ -140,12 +162,15 @@ final class LogsCommand extends Command $date = $input->getOption('date'); if (1 !== preg_match('/^\d{8}$/', $date)) { - throw new \RuntimeException('Log date must be in [YYYYMMDD] format. For example [20220622].'); + throw new InvalidArgumentException('Log date must be in [YYYYMMDD] format. For example [20220622].'); } $limit = (int)$input->getOption('limit'); - $file = sprintf(Config::get('tmpDir') . '/logs/%s.%s.log', $type, $date); + $file = r(text: Config::get('tmpDir') . '/logs/{type}.{date}.log', context: [ + 'type' => $type, + 'date' => $date + ]); if (false === file_exists($file) || filesize($file) < 1) { $output->writeln( @@ -226,6 +251,14 @@ final class LogsCommand extends Command return self::SUCCESS; } + /** + * Lists the logs. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code. + */ private function listLogs(InputInterface $input, OutputInterface $output): int { $path = fixPath(Config::get('tmpDir') . '/logs'); @@ -259,6 +292,15 @@ final class LogsCommand extends Command return self::SUCCESS; } + /** + * Clears the contents of a log file. + * + * @param SplFileObject $file The log file object. + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit code. + */ private function handleClearLog(SplFileObject $file, InputInterface $input, OutputInterface $output): int { $logfile = after($file->getRealPath(), Config::get('tmpDir') . '/'); @@ -310,6 +352,12 @@ final class LogsCommand extends Command return self::SUCCESS; } + /** + * Complete the suggestions for the given input. + * + * @param CompletionInput $input The completion input. + * @param CompletionSuggestions $suggestions The completion suggestions. + */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions); @@ -329,6 +377,11 @@ final class LogsCommand extends Command } } + /** + * Retrieve the types of log files. + * + * @return array The array of available log file types. + */ public static function getTypes(): array { return self::LOG_FILES; diff --git a/src/Commands/System/MaintenanceCommand.php b/src/Commands/System/MaintenanceCommand.php index 66d8346b..54ab21ac 100644 --- a/src/Commands/System/MaintenanceCommand.php +++ b/src/Commands/System/MaintenanceCommand.php @@ -10,16 +10,29 @@ use App\Libs\Routable; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class MaintenanceCommand + * + * Runs maintenance tasks on the database. + */ #[Routable(command: self::ROUTE)] final class MaintenanceCommand extends Command { public const ROUTE = 'system:db:maintenance'; + /** + * Class constructor. + * + * @param iDB $db The database connection object. + */ public function __construct(private iDB $db) { parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -35,6 +48,14 @@ final class MaintenanceCommand extends Command ); } + /** + * Runs the command. + * + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * + * @return int Returns the exit code. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $this->db->maintenance(); diff --git a/src/Commands/System/MakeCommand.php b/src/Commands/System/MakeCommand.php index cec88d9d..295000c5 100644 --- a/src/Commands/System/MakeCommand.php +++ b/src/Commands/System/MakeCommand.php @@ -11,16 +11,29 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class MakeCommand + * + * This class represents a command to create a database schema migration file. + */ #[Routable(command: self::ROUTE)] final class MakeCommand extends Command { public const ROUTE = 'system:db:make'; + /** + * Class Constructor. + * + * @param iDB $db The iDB object used for database operations. + */ public function __construct(private iDB $db) { parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -46,16 +59,21 @@ final class MakeCommand extends Command ); } + /** + * Executes a command. + * + * @param InputInterface $input The input object containing command arguments and options. + * @param OutputInterface $output The output object used for displaying messages. + * + * @return int The exit code of the command execution. Returns "SUCCESS" constant value. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $file = $this->db->makeMigration($input->getArgument('filename')); - $output->writeln( - sprintf( - 'Created new migration at \'%s\'.', - after(realpath($file), ROOT_PATH), - ) - ); + $output->writeln(r(text: "Created new migration file at '{file}'.", context: [ + 'file' => after(realpath($file), ROOT_PATH), + ])); return self::SUCCESS; } diff --git a/src/Commands/System/MigrationsCommand.php b/src/Commands/System/MigrationsCommand.php index 1da18fc4..b5b12b77 100644 --- a/src/Commands/System/MigrationsCommand.php +++ b/src/Commands/System/MigrationsCommand.php @@ -11,16 +11,30 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class MigrationsCommand + * + * Database migrations runner. + */ #[Routable(command: self::ROUTE)] final class MigrationsCommand extends Command { public const ROUTE = 'system:db:migrations'; + /** + * Class Constructor. + * + * @param iDB $db The database connection object. + * + */ public function __construct(private iDB $db) { parent::__construct(); } + /** + * Configures the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -37,7 +51,27 @@ final class MigrationsCommand extends Command ); } + /** + * Make sure the command is not running in parallel. + * + * @param InputInterface $input The input object containing the command data. + * @param OutputInterface $output The output object for displaying command output. + * + * @return int The exit code of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int + { + return $this->single(fn(): int => $this->process($input), $output); + } + + /** + * Run the command to migrate the database. + * + * @param InputInterface $input The input object representing the command inputs. + * + * @return int The exit code of the command execution. + */ + protected function process(InputInterface $input): int { $opts = []; diff --git a/src/Commands/System/PHPCommand.php b/src/Commands/System/PHPCommand.php index 99765e9a..401eadaf 100644 --- a/src/Commands/System/PHPCommand.php +++ b/src/Commands/System/PHPCommand.php @@ -11,11 +11,22 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class PHPCommand + * + * This command is used to generate expected values for php.ini and fpm pool worker. + * To generate fpm values, use the "--fpm" option. + */ #[Routable(command: self::ROUTE)] final class PHPCommand extends Command { public const ROUTE = 'system:php'; + /** + * Configures the command. + * + * @return void + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -40,11 +51,25 @@ final class PHPCommand extends Command ); } + /** + * Runs the command based on the input options. + * + * @param InputInterface $input The input options. + * @param OutputInterface $output The output interface for displaying messages. + * @return int The exit code of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { return $input->getOption('fpm') ? $this->makeFPM($output) : $this->makeConfig($output); } + /** + * Print the php.ini configuration. + * + * @param OutputInterface $output The OutputInterface object to write the configuration to. + * + * @return int The status code indicating the success of the method. + */ protected function makeConfig(OutputInterface $output): int { $config = Config::get('php.ini', []); @@ -56,6 +81,13 @@ final class PHPCommand extends Command return self::SUCCESS; } + /** + * Print the PHP-FPM configuration. + * + * @param OutputInterface $output The OutputInterface object to write the configuration to. + * + * @return int The status code indicating the success of the method. + */ protected function makeFPM(OutputInterface $output): int { $config = Config::get('php.fpm', []); @@ -70,6 +102,13 @@ final class PHPCommand extends Command return self::SUCCESS; } + /** + * Escape the given value. + * + * @param mixed $val The value to escape. + * + * @return mixed The escaped value. + */ private function escapeValue(mixed $val): mixed { if (is_bool($val) || is_int($val)) { diff --git a/src/Commands/System/PruneCommand.php b/src/Commands/System/PruneCommand.php index f73b5498..2843b7ae 100644 --- a/src/Commands/System/PruneCommand.php +++ b/src/Commands/System/PruneCommand.php @@ -13,6 +13,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class PruneCommand + * + * This command removes automatically generated files like logs and backups. + * It provides an option to run in dry-run mode to see what files will be removed without actually removing them. + */ #[Routable(command: self::ROUTE)] final class PruneCommand extends Command { @@ -20,11 +26,19 @@ final class PruneCommand extends Command public const TASK_NAME = 'prune'; + /** + * Class Constructor. + * + * @param LoggerInterface $logger The logger implementation used for logging. + */ public function __construct(private LoggerInterface $logger) { parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -49,6 +63,14 @@ final class PruneCommand extends Command ); } + /** + * Executes the command. + * + * @param InputInterface $input The input interface. + * @param OutputInterface $output The output interface. + * + * @return int The exit status code. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $time = time(); @@ -139,5 +161,4 @@ final class PruneCommand extends Command return self::SUCCESS; } - } diff --git a/src/Commands/System/ReportCommand.php b/src/Commands/System/ReportCommand.php index 3b25366c..dfd381fd 100644 --- a/src/Commands/System/ReportCommand.php +++ b/src/Commands/System/ReportCommand.php @@ -11,8 +11,8 @@ use App\Libs\Entity\StateEntity; use App\Libs\Extends\Date; use App\Libs\Options; use App\Libs\Routable; +use App\Libs\Stream; use Cron\CronExpression; -use Exception; use LimitIterator; use SplFileObject; use Symfony\Component\Console\Input\InputInterface as iInput; @@ -20,16 +20,31 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface as iOutput; use Throwable; +/** + * Class ReportCommand + * + * Show basic information for diagnostics. + */ #[Routable(command: self::ROUTE)] final class ReportCommand extends Command { public const ROUTE = 'system:report'; + /** + * Class Constructor. + * + * @param iDB $db An instance of the iDB class used for database operations. + * + * @return void + */ public function __construct(private iDB $db) { parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -54,6 +69,14 @@ final class ReportCommand extends Command ); } + /** + * Display basic information for diagnostics. + * + * @param iInput $input An instance of the iInput class used for command input. + * @param iOutput $output An instance of the iOutput class used for command output. + * + * @return int Returns the command execution status code. + */ protected function runCommand(iInput $input, iOutput $output): int { $output->writeln('[ Basic Report ]' . PHP_EOL); @@ -82,7 +105,7 @@ final class ReportCommand extends Command } try { - $pid = trim(file_get_contents($pidFile)); + $pid = trim((string)(new Stream($pidFile))); } catch (Throwable $e) { return $e->getMessage(); } @@ -111,6 +134,14 @@ final class ReportCommand extends Command return self::SUCCESS; } + /** + * Get backends and display information about each backend. + * + * @param iInput $input An instance of the iInput class used for input operations. + * @param iOutput $output An instance of the iOutput class used for output operations. + * + * @return void + */ private function getBackends(iInput $input, iOutput $output): void { $includeSample = (bool)$input->getOption('include-db-sample'); @@ -239,6 +270,13 @@ final class ReportCommand extends Command } } + /** + * Retrieves the tasks and displays information about each task. + * + * @param iOutput $output An instance of the iOutput class used for displaying output. + * + * @return void + */ private function getTasks(iOutput $output): void { foreach (Config::get('tasks.list', []) as $task) { @@ -274,7 +312,7 @@ final class ReportCommand extends Command 'answer' => gmdate(Date::ATOM, $timer->getNextRunDate()->getTimestamp()), ]) ); - } catch (Exception $e) { + } catch (Throwable $e) { $output->writeln( r('Next Run scheduled failed. {answer}', [ 'answer' => $e->getMessage(), @@ -288,6 +326,12 @@ final class ReportCommand extends Command } } + /** + * Get logs. + * + * @param iInput $input An instance of the iInput class used for input operations. + * @param iOutput $output An instance of the iOutput class used for output operations. + */ private function getLogs(iInput $input, iOutput $output): void { $todayAffix = makeDate()->format('Ymd'); @@ -307,6 +351,16 @@ final class ReportCommand extends Command } } + /** + * Get last X lines from log file. + * + * @param iOutput $output An instance of the iOutput class used for displaying output. + * @param string $type The type of the log. + * @param string|int $date The date of the log file. + * @param int|string $limit The maximum number of lines to display. + * + * @return void + */ private function handleLog(iOutput $output, string $type, string|int $date, int|string $limit): void { $logFile = Config::get('tmpDir') . '/logs/' . r( diff --git a/src/Commands/System/RoutesCommand.php b/src/Commands/System/RoutesCommand.php index 41b353df..0aa5e9ca 100644 --- a/src/Commands/System/RoutesCommand.php +++ b/src/Commands/System/RoutesCommand.php @@ -9,11 +9,19 @@ use App\Libs\Routable; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class RoutesCommand + * + * This command is used to generate routes for commands. It is automatically run on container startup. + */ #[Routable(command: self::ROUTE)] final class RoutesCommand extends Command { public const ROUTE = 'system:routes'; + /** + * Configures the command. + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -28,6 +36,14 @@ final class RoutesCommand extends Command ); } + /** + * Executes the command to generate routes. + * + * @param InputInterface $input The input interface object. + * @param OutputInterface $output The output interface object. + * + * @return int The exit code of the command execution. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { generateRoutes(); diff --git a/src/Commands/System/ServerCommand.php b/src/Commands/System/ServerCommand.php index d5678bec..5745282f 100644 --- a/src/Commands/System/ServerCommand.php +++ b/src/Commands/System/ServerCommand.php @@ -11,16 +11,35 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class ServerCommand + * + * This class represents a command that starts a minimal HTTP server. + * + * @package YourPackage + */ #[Routable(command: self::ROUTE)] final class ServerCommand extends Command { public const ROUTE = 'system:server'; + /** + * Class Constructor. + * + * @param Server $server The server object to be injected. + * + * @return void + */ public function __construct(private Server $server) { parent::__construct(); } + /** + * Configure the command options and description. + * + * @return void + */ protected function configure(): void { $this->setName(self::ROUTE) @@ -37,6 +56,14 @@ final class ServerCommand extends Command ); } + /** + * Runs Server. + * + * @param InputInterface $input The input interface object containing command input options. + * @param OutputInterface $output The output interface object used to display command output. + * + * @return int Returns the exit code of the command. + */ protected function runCommand(InputInterface $input, OutputInterface $output): int { $host = $input->getOption('interface'); diff --git a/src/Commands/System/TasksCommand.php b/src/Commands/System/TasksCommand.php index d4de4894..149d983b 100644 --- a/src/Commands/System/TasksCommand.php +++ b/src/Commands/System/TasksCommand.php @@ -8,6 +8,7 @@ use App\Command; use App\Libs\Config; use App\Libs\Extends\ConsoleOutput; use App\Libs\Routable; +use App\Libs\Stream; use Cron\CronExpression; use Exception; use Symfony\Component\Console\Completion\CompletionInput; @@ -17,7 +18,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface as iOutput; use Symfony\Component\Process\Process; +use Throwable; +/** + * Class TasksCommand + * + * Automates the runs of scheduled tasks. + */ #[Routable(command: self::ROUTE)] final class TasksCommand extends Command { @@ -26,6 +33,9 @@ final class TasksCommand extends Command private array $logs = []; private array $taskOutput = []; + /** + * Class Constructor. + */ public function __construct() { set_time_limit(0); @@ -34,6 +44,9 @@ final class TasksCommand extends Command parent::__construct(); } + /** + * Configure the command. + */ protected function configure(): void { $tasksName = implode( @@ -109,7 +122,12 @@ final class TasksCommand extends Command } /** - * @throws Exception + * If the run option is set, run the tasks, otherwise list available tasks. + * + * @param iInput $input The input instance. + * @param iOutput $output The output instance. + * + * @return int Returns the exit code of the command. */ protected function runCommand(iInput $input, iOutput $output): int { @@ -117,10 +135,34 @@ final class TasksCommand extends Command return $this->runTasks($input, $output); } - $this->listTasks($input, $output); + $list = []; + + $mode = $input->getOption('output'); + + foreach ($this->getTasks() as $task) { + $list[] = [ + 'name' => $task['name'], + 'command' => $task['command'], + 'options' => $task['args'] ?? '', + 'timer' => $task['timer']->getExpression(), + 'description' => $task['description'] ?? '', + 'NextRun' => $task['next'], + ]; + } + + $this->displayContent($list, $output, $mode); + return self::SUCCESS; } + /** + * Runs the tasks. + * + * @param iInput $input The input object. + * @param iOutput $output The output object. + * + * @return int The exit code of the command. + */ private function runTasks(iInput $input, iOutput $output): int { $run = []; @@ -232,15 +274,35 @@ final class TasksCommand extends Command } if ($input->getOption('save-log') && count($this->logs) >= 1) { - if (false !== ($fp = @fopen(Config::get('tasks.logfile'), 'a'))) { - fwrite($fp, preg_replace('#\R+#', PHP_EOL, implode(PHP_EOL, $this->logs)) . PHP_EOL . PHP_EOL); - fclose($fp); + try { + $stream = new Stream(Config::get('tasks.logfile'), 'a'); + $stream->write(preg_replace('#\R+#', PHP_EOL, implode(PHP_EOL, $this->logs)) . PHP_EOL . PHP_EOL); + $stream->close(); + } catch (Throwable $e) { + $this->write(r('Failed to open log file [{file}]. Error [{message}].', [ + 'file' => Config::get('tasks.logfile'), + 'message' => $e->getMessage(), + ]), $input, $output); + + return self::INVALID; } } return self::SUCCESS; } + /** + * Write method. + * + * Writes a given text to the output with the specified level. + * Optionally if the 'save-log' option is set to true, the output will be saved to the logs array. + * The logs array will be saved to the log file at the end of the command execution. + * + * @param string $text The text to write to output. + * @param iInput $input The input object. + * @param iOutput $output The output object. + * @param int $level The level of the output (default: iOutput::OUTPUT_NORMAL). + */ private function write(string $text, iInput $input, iOutput $output, int $level = iOutput::OUTPUT_NORMAL): void { assert($output instanceof ConsoleOutput); @@ -252,28 +314,10 @@ final class TasksCommand extends Command } /** - * @throws Exception + * Get the list of tasks. + * + * @return array The list of tasks. */ - private function listTasks(iInput $input, iOutput $output): void - { - $list = []; - - $mode = $input->getOption('output'); - - foreach ($this->getTasks() as $task) { - $list[] = [ - 'name' => $task['name'], - 'command' => $task['command'], - 'options' => $task['args'] ?? '', - 'timer' => $task['timer']->getExpression(), - 'description' => $task['description'] ?? '', - 'NextRun' => $task['next'], - ]; - } - - $this->displayContent($list, $output, $mode); - } - private function getTasks(): array { $list = []; @@ -302,6 +346,12 @@ final class TasksCommand extends Command return $list; } + /** + * Complete the input with suggestions if necessary. + * + * @param CompletionInput $input The completion input object. + * @param CompletionSuggestions $suggestions The completion suggestions object. + */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions);