diff --git a/routes/web.php b/routes/web.php index 93e316a9..042d7c29 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,6 +20,7 @@ use Dotenv\Dotenv; use Lion\Bundle\Enums\LogTypeEnum; use Lion\Bundle\Helpers\Commands\Queue\TaskQueue; +use Lion\Bundle\Support\Task; use Lion\Database\Driver; use Lion\Files\Store; use Lion\Route\Route; @@ -79,23 +80,24 @@ Route::addMiddleware(Routes::getMiddleware()); // ----------------------------------------------------------------------------- Route::get('/', function (): stdClass { - /** @phpstan-ignore-next-line */ - $taskQueue = new TaskQueue([ + $data = [ 'scheme' => env('REDIS_SCHEME'), 'host' => env('REDIS_HOST'), 'port' => env('REDIS_PORT'), + 'database' => TaskQueue::LION_DATABASE, 'parameters' => [ 'password' => env('REDIS_PASSWORD'), - 'database' => TaskQueue::LION_DATABASE, ], - ]); + ]; + + /** @phpstan-ignore-next-line */ + $taskQueue = new TaskQueue($data); $taskQueue ->push( - new \Lion\Bundle\Support\Task(ExampleProvider::class, 'getArrExample', [ - 'name' => 'root', - ]), - new \Lion\Bundle\Support\Task(ExampleProvider::class, 'getResult') + new Task(ExampleProvider::class, 'getArrExample', ['name' => 'root']), + new Task(ExampleProvider::class, 'getResult'), + new Task(ExampleProvider::class, 'generateError') ); return info('[index]'); diff --git a/src/LionBundle/Commands/Lion/Queue/RunQueuedTasksCommand.php b/src/LionBundle/Commands/Lion/Queue/RunQueuedTasksCommand.php index 6297fe70..526cba12 100644 --- a/src/LionBundle/Commands/Lion/Queue/RunQueuedTasksCommand.php +++ b/src/LionBundle/Commands/Lion/Queue/RunQueuedTasksCommand.php @@ -5,14 +5,17 @@ namespace Lion\Bundle\Commands\Lion\Queue; use DI\Attribute\Inject; +use JsonException; use Lion\Bundle\Enums\LogTypeEnum; use Lion\Bundle\Helpers\Commands\Queue\TaskQueue; use Lion\Bundle\Helpers\Commands\Selection\MenuCommand; +use Lion\Bundle\Support\Task; use Lion\Dependency\Injection\Container; use LogicException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; /** * Allows queued tasks to run in the background. @@ -33,6 +36,13 @@ class RunQueuedTasksCommand extends MenuCommand */ private TaskQueue $taskQueue; + /** + * Database in use. + * + * @var int $database + */ + private int $database; + #[Inject] public function setContainer(Container $container): RunQueuedTasksCommand { @@ -50,34 +60,59 @@ protected function configure(): void { $this ->setName('queue:run') - ->setDescription('Run queued tasks') - ->addOption( - 'pause', - 'p', - InputOption::VALUE_OPTIONAL, - 'Defines the time to wait before retrieving tasks if all have been executed.', - 60 - ); + ->setDescription('Run queued tasks.') + ->addOption('database', 'd', InputOption::VALUE_OPTIONAL, 'Redis database, default value 0 for internal operations.', TaskQueue::LION_DATABASE) // phpcs:ignore + ->addOption('pause', 'p', InputOption::VALUE_OPTIONAL, 'Defines the time to wait before retrieving tasks if all have been executed.', 60); // phpcs:ignore } /** * Initializes the command after the input has been bound and before the - * input is validated + * input is validated. * * This is mainly useful when a lot of commands extends one main command * where some things need to be initialized based on the input arguments and - * options + * options. * - * @param InputInterface $input [InputInterface is the interface implemented - * by all input classes] - * @param OutputInterface $output [OutputInterface is the interface - * implemented by all Output classes] + * @param InputInterface $input InputInterface is the interface implemented + * by all input classes. + * @param OutputInterface $output OutputInterface is the interface + * implemented by all Output classes. * * @return void */ protected function initialize(InputInterface $input, OutputInterface $output): void { parent::initialize($input, $output); + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class as a concrete + * class. In this case, instead of defining the execute() method, you set + * the code to execute by passing a Closure to the setCode() method. + * + * @param InputInterface $input InputInterface is the interface implemented + * by all input classes. + * @param OutputInterface $output OutputInterface is the interface + * implemented by all Output classes. + * + * @return int + * + * @throws JsonException If encoding to JSON fails. + * @throws LogicException When this abstract method is not implemented. + * + * @codeCoverageIgnore + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $pause */ + $pause = $input->getOption('pause'); + + /** @var string $database */ + $database = $input->getOption('database'); + + $this->database = (int) $database; /** @var string $redisScheme */ $redisScheme = env('REDIS_SCHEME'); @@ -91,47 +126,22 @@ protected function initialize(InputInterface $input, OutputInterface $output): v /** @var string $password */ $password = env('REDIS_PASSWORD'); - $this->taskQueue = new TaskQueue([ - 'scheme' => $redisScheme, - 'host' => $host, - 'port' => $port, - 'parameters' => [ - 'password' => $password, - 'database' => TaskQueue::LION_DATABASE, + $this->taskQueue = new TaskQueue(parameters: [ + TaskQueue::SCHEME => $redisScheme, + TaskQueue::HOST => $host, + TaskQueue::PORT => $port, + TaskQueue::DATABASE => $this->database, + TaskQueue::PARAMETERS => [ + TaskQueue::PASSWORD => $password, ], ]); - } - - /** - * Executes the current command - * - * This method is not abstract because you can use this class - * as a concrete class. In this case, instead of defining the - * execute() method, you set the code to execute by passing - * a Closure to the setCode() method - * - * @param InputInterface $input [InputInterface is the interface implemented - * by all input classes] - * @param OutputInterface $output [OutputInterface is the interface - * implemented by all Output classes] - * - * @return int - * - * @throws LogicException [When this abstract method is not implemented] - * - * @codeCoverageIgnore - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var int|string $pause */ - $pause = $input->getOption('pause'); /** @phpstan-ignore-next-line */ while (true) { $json = $this->taskQueue->get(); if (null === $json) { - $output->writeln($this->infoOutput("\t>> SCHEDULE: no queued tasks available")); + $output->writeln($this->infoOutput("\t>> TASK [DATABASE: {$this->database}]: There are no queued tasks available. [OMITTED]")); // phpcs:ignore $this->taskQueue->pause((int) $pause); @@ -146,38 +156,88 @@ protected function execute(InputInterface $input, OutputInterface $output): int * } $queue */ $queue = json_decode($json, true); - $output->writeln( - $this->warningOutput( - "\t>> SCHEDULE: {$queue['id']} / {$queue['namespace']}::{$queue['method']} [PROCESSING]" - ) - ); - - $return = $this->container->callMethod( - $this->container->resolve($queue['namespace']), - $queue['method'], - [ - 'queue' => $queue, - ...$queue['data'], - ], - ); - - if (is_object($return)) { - $return = (array) $return; - } + $output->writeln($this->warningOutput($this->getOutput('PROCESSING', $queue))); + + try { + /** @var string $id */ + $id = $queue[Task::ID]; + + /** @var string $namespace */ + $namespace = $queue[Task::NAMESPACE]; + + /** @var string $method */ + $method = $queue['method']; + + /** + * @var array $data + * + * @phpstan-ignore-next-line + */ + $data = $queue[Task::DATA]; + + $instance = $this->container->resolve($namespace); + + $instanceParams = ['queue' => $queue, ...$data]; + + /** @phpstan-ignore-next-line */ + $return = $this->container->callMethod($instance, $method, $instanceParams); + + if (is_object($return)) { + $return = (array) $return; + } - $json = [ - 'class' => "{$queue['namespace']}::{$queue['method']}", - 'params' => $queue['data'], - 'return' => $return, - ]; + $log = [ + 'class' => "{$namespace}::{$method}", + 'params' => $data, + 'return' => $return, + ]; - logger("TASK: {$queue['id']}", LogTypeEnum::INFO, $json); + logger("TASK: {$id}", LogTypeEnum::INFO, $log); - $output->writeln( - $this->successOutput( - "\t>> SCHEDULE: {$queue['id']} / {$queue['namespace']}::{$queue['method']} [COMPLETED]" - ) - ); + $output->writeln($this->successOutput($this->getOutput('COMPLETED', $queue))); + } catch (Throwable $exception) { + $loggerData = [ + 'class' => "{$namespace}::{$method}", + 'params' => $data, + 'error' => [ + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTraceAsString(), + ], + ]; + + logger("TASK [DATABASE: {$this->database}]: {$id}", LogTypeEnum::ERROR, $loggerData); + + $output->writeln($this->errorOutput($this->getOutput('ERROR', $queue))); + } } } + + /** + * @param string $type + * @param array{ + * id: string, + * namespace: string, + * method: string, + * data: array + * } $queue + * + * @return string + * + * @codeCoverageIgnore + */ + private function getOutput(string $type, array $queue): string + { + /** @var string $id */ + $id = $queue['id']; + + /** @var string $namespace */ + $namespace = $queue['namespace']; + + /** @var string $method */ + $method = $queue['method']; + + return "\t>> TASK [DATABASE: {$this->database}]: {$id} / {$namespace}::{$method} [{$type}]"; + } } diff --git a/src/LionBundle/Helpers/Commands/Queue/TaskQueue.php b/src/LionBundle/Helpers/Commands/Queue/TaskQueue.php index 3779c137..768036c9 100644 --- a/src/LionBundle/Helpers/Commands/Queue/TaskQueue.php +++ b/src/LionBundle/Helpers/Commands/Queue/TaskQueue.php @@ -13,17 +13,28 @@ */ class TaskQueue { + /** + * Reference constants for Redis initialization. + */ + public const string SCHEME = 'scheme'; + + public const string HOST = 'host'; + + public const string PORT = 'port'; + + public const string DATABASE = 'database'; + + public const string PARAMETERS = 'parameters'; + + public const string PASSWORD = 'password'; + /** * Defines the property that contains the queued task data. - * - * @const LION_TASKS */ public const string LION_TASKS = 'lion-tasks'; /** * Defines the database to connect to and manipulate tasks. - * - * @const LION_DATABASE */ public const int LION_DATABASE = 0; @@ -41,9 +52,9 @@ class TaskQueue * scheme: string, * host: string, * port: int, + * database: int, * parameters: array{ - * password: string, - * database: int + * password: string * } * } $parameters */ diff --git a/src/LionBundle/Support/Task.php b/src/LionBundle/Support/Task.php index e75c65ac..3042c0eb 100644 --- a/src/LionBundle/Support/Task.php +++ b/src/LionBundle/Support/Task.php @@ -8,11 +8,20 @@ /** * Tasks class to encapsulate tasks in queue. - * - * @package Lion\Bundle\Helpers\Commands\Schedule */ class Task { + /** + * Reference constants for obtaining task data. + */ + public const string ID = 'id'; + + public const string NAMESPACE = 'namespace'; + + public const string METHOD = 'method'; + + public const string DATA = 'data'; + /** * Property for namespace. * @@ -60,10 +69,10 @@ public function __construct(string $namespace, string $method, array $data = []) public function getTask(): string { return json([ - 'id' => uniqid('task-'), - 'namespace' => $this->namespace, - 'method' => $this->method, - 'data' => $this->data, + self::ID => uniqid('task-'), + self::NAMESPACE => $this->namespace, + self::METHOD => $this->method, + self::DATA => $this->data, ]); } } diff --git a/tests/Commands/Lion/Queue/RunQueuedTasksCommandTest.php b/tests/Commands/Lion/Queue/RunQueuedTasksCommandTest.php index 5e11cfa8..b7e49c9e 100644 --- a/tests/Commands/Lion/Queue/RunQueuedTasksCommandTest.php +++ b/tests/Commands/Lion/Queue/RunQueuedTasksCommandTest.php @@ -7,7 +7,6 @@ use DI\DependencyException; use DI\NotFoundException; use Lion\Bundle\Commands\Lion\Queue\RunQueuedTasksCommand; -use Lion\Bundle\Helpers\Commands\Queue\TaskQueue; use Lion\Dependency\Injection\Container; use Lion\Test\Test; use ReflectionException; @@ -68,8 +67,8 @@ public function initialize(): void 'output' => $output ]); - $taskQueue = $this->getPrivateProperty('taskQueue'); + $outputInstance = $this->getPrivateProperty('output'); - $this->assertInstanceOf(TaskQueue::class, $taskQueue); + $this->assertInstanceOf(BufferedOutput::class, $outputInstance); } } diff --git a/tests/Providers/ExampleProvider.php b/tests/Providers/ExampleProvider.php index 803b22fa..7496ef5f 100644 --- a/tests/Providers/ExampleProvider.php +++ b/tests/Providers/ExampleProvider.php @@ -4,7 +4,9 @@ namespace Tests\Providers; +use Exception; use Lion\Helpers\Arr; +use Lion\Request\Http; use Lion\Route\Attributes\Rules; use stdClass; @@ -24,4 +26,16 @@ public function getResult(): stdClass { return success('Name: ' . request('name')); } + + /** + * Execute a sample exception. + * + * @return void + * + * @throws Exception + */ + public function generateError(): void + { + throw new Exception('ERR', Http::INTERNAL_SERVER_ERROR); + } }