feat: refactor command structure to use attribute-based command registration and enhance input/output handling
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 3m43s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 3m54s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 5m4s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 5m4s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 5m27s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m5s

This commit is contained in:
2026-02-05 21:24:23 -05:00
parent e971d32f9d
commit ed32bd3624
9 changed files with 1163 additions and 801 deletions

View File

@@ -20,7 +20,6 @@
"predis/predis": "^v3.2.0", "predis/predis": "^v3.2.0",
"siteworxpro/http-status": "0.0.2", "siteworxpro/http-status": "0.0.2",
"lcobucci/jwt": "^5.6", "lcobucci/jwt": "^5.6",
"adhocore/cli": "^1.9",
"robinvdvleuten/ulid": "^5.0", "robinvdvleuten/ulid": "^5.0",
"monolog/monolog": "^3.9", "monolog/monolog": "^3.9",
"react/promise": "^3", "react/promise": "^3",
@@ -28,7 +27,9 @@
"guzzlehttp/guzzle": "^7.10", "guzzlehttp/guzzle": "^7.10",
"zircote/swagger-php": "^5.7", "zircote/swagger-php": "^5.7",
"spiral/roadrunner-grpc": "^3.5", "spiral/roadrunner-grpc": "^3.5",
"league/tactician": "^1.1" "league/tactician": "^1.1",
"symfony/console": "^v7.4.3",
"league/climate": "^3.10"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^12.4", "phpunit/phpunit": "^12.4",
@@ -46,7 +47,7 @@
"composer run-script tests:phpstan" "composer run-script tests:phpstan"
], ],
"tests:unit": [ "tests:unit": [
"phpunit --colors=always --display-deprecations tests" "phpunit --testdox --colors=always --display-notices --display-deprecations tests"
], ],
"tests:unit:coverage": [ "tests:unit:coverage": [
"phpunit --coverage-text --colors=never --display-deprecations --log-junit tests/reports/junit.xml --coverage-html tests/reports/html tests " "phpunit --coverage-text --colors=never --display-deprecations --log-junit tests/reports/junit.xml --coverage-html tests/reports/html tests "

1599
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli; namespace Siteworxpro\App\Cli;
use Ahc\Cli\Application;
use Siteworxpro\App\Cli\Commands\DemoCommand; use Siteworxpro\App\Cli\Commands\DemoCommand;
use Siteworxpro\App\Cli\Commands\Queue\Start; use Siteworxpro\App\Cli\Commands\Queue\Start;
use Siteworxpro\App\Cli\Commands\Queue\TestJob; use Siteworxpro\App\Cli\Commands\Queue\TestJob;
use Siteworxpro\App\Helpers\Version; use Siteworxpro\App\Helpers\Version;
use Siteworxpro\App\Kernel; use Siteworxpro\App\Kernel;
use Siteworxpro\App\Services\Facades\Config; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
class App class App
{ {
@@ -22,25 +22,28 @@ class App
public function __construct() public function __construct()
{ {
Kernel::boot(); Kernel::boot();
$this->app = new Application('Php-Template', Version::VERSION);
$this->app->add(new DemoCommand()); $this->app = new Application('Siteworxpro Auth', Version::VERSION);
$this->app->add(new Start());
$this->app->add(new TestJob()); $this->app->setCatchErrors();
$this->app->addCommand(new DemoCommand());
$this->app->addCommand(new Start());
$this->app->addCommand(new TestJob());
} }
public function run(): int public function run(): int
{ {
$this->app->logo(
<<<EOF $output = new ClimateOutput();
▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▀▀█ ▄
█ ▀█ █ █ █ ▀█ █ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄ █ ▄▄▄ ▄▄█▄▄ ▄▄▄ try {
█▄▄▄█▀ █▄▄▄▄█ █▄▄▄█▀ █ █▀ █ █ █ █ █▀ ▀█ █ ▀ █ █ █▀ █ return $this->app->run(new ArgvInput(), $output);
█ █ █ ▀▀▀ █ █▀▀▀▀ █ █ █ █ █ █ ▄▀▀▀█ █ █▀▀▀▀ } catch (\Exception $e) {
█ █ █ █ █ ▀█▄▄▀ █ █ █ ██▄█▀ ▀▄▄ ▀▄▄▀█ ▀▄▄ ▀█▄▄▀ $output->error($e->getMessage());
$output->error($e->getTraceAsString());
EOF
); return $e->getCode() ?: 1;
return $this->app->handle($_SERVER['argv']); }
} }
} }

210
src/Cli/ClimateOutput.php Normal file
View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli;
use League\CLImate\CLImate;
use League\CLImate\TerminalObject\Dynamic\Spinner;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @method CLImate black(string $str = null)
* @method CLImate red(string $str = null)
* @method CLImate green(string $str = null)
* @method CLImate yellow(string $str = null)
* @method CLImate blue(string $str = null)
* @method CLImate magenta(string $str = null)
* @method CLImate cyan(string $str = null)
* @method CLImate lightGray(string $str = null)
* @method CLImate darkGray(string $str = null)
* @method CLImate lightRed(string $str = null)
* @method CLImate lightGreen(string $str = null)
* @method CLImate lightYellow(string $str = null)
* @method CLImate lightBlue(string $str = null)
* @method CLImate lightMagenta(string $str = null)
* @method CLImate lightCyan(string $str = null)
* @method CLImate white(string $str = null)
*
* @method CLImate backgroundBlack(string $str = null)
* @method CLImate backgroundRed(string $str = null)
* @method CLImate backgroundGreen(string $str = null)
* @method CLImate backgroundYellow(string $str = null)
* @method CLImate backgroundBlue(string $str = null)
* @method CLImate backgroundMagenta(string $str = null)
* @method CLImate backgroundCyan(string $str = null)
* @method CLImate backgroundLightGray(string $str = null)
* @method CLImate backgroundDarkGray(string $str = null)
* @method CLImate backgroundLightRed(string $str = null)
* @method CLImate backgroundLightGreen(string $str = null)
* @method CLImate backgroundLightYellow(string $str = null)
* @method CLImate backgroundLightBlue(string $str = null)
* @method CLImate backgroundLightMagenta(string $str = null)
* @method CLImate backgroundLightCyan(string $str = null)
* @method CLImate backgroundWhite(string $str = null)
*
* @method CLImate bold(string $str = null)
* @method CLImate dim(string $str = null)
* @method CLImate underline(string $str = null)
* @method CLImate blink(string $str = null)
* @method CLImate invert(string $str = null)
* @method CLImate hidden(string $str = null)
*
* @method CLImate info(string $str = null)
* @method CLImate comment(string $str = null)
* @method CLImate whisper(string $str = null)
* @method CLImate shout(string $str = null)
* @method CLImate error(string $str = null)
*
* @method mixed out(string $str)
* @method mixed inline(string $str)
* @method mixed table(array $data)
* @method mixed json(mixed $var)
* @method mixed br($count = 1)
* @method mixed tab($count = 1)
* @method mixed draw(string $art)
* @method mixed border(string $char = null, integer $length = null)
* @method mixed dump(mixed $var)
* @method mixed flank(string $output, string $char = null, integer $length = null)
* @method mixed progress(integer $total = null)
* @method Spinner spinner(string $label = null, string ...$characters = null)
* @method mixed padding(integer $length = 0, string $char = '.')
* @method mixed columns(array $data, $column_count = null)
* @method mixed clear()
* @method CLImate clearLine()
*
* @method CLImate addArt(string $dir)
*/
class ClimateOutput extends ConsoleSectionOutput implements ConsoleOutputInterface
{
private CLImate $CLImate;
private OutputFormatterInterface|OutputFormatter $formatter;
private int $verbosity = self::VERBOSITY_NORMAL;
private bool $decorated = true;
public function __construct()
{
$this->CLImate = new CLImate();
$this->formatter = new OutputFormatter($this->isDecorated());
// create stream output
$output = fopen('php://stdout', 'w');
stream_set_blocking($output, true);
$sections = [];
parent::__construct(
$output,
$sections,
$this->getVerbosity(),
$this->isDecorated(),
$this->getFormatter()
);
}
public function write(iterable|string $messages, bool $newline = false, int $options = 0): void
{
if (is_string($messages)) {
$messages = [$messages];
}
foreach ($messages as $message) {
$message = $this->formatter->format($message);
if (!$newline) {
$this->CLImate->inline($message);
continue;
}
$this->CLImate->out($message);
}
}
protected function doWrite(string $message, bool $newline): void
{
if ($newline) {
$this->CLImate->out($message);
} else {
$this->CLImate->inline($message);
}
}
public function setVerbosity(int $level): void
{
$this->verbosity = $level;
}
/**
* @return int
*/
public function getVerbosity(): int
{
return $this->verbosity;
}
public function isQuiet(): bool
{
return $this->verbosity === self::VERBOSITY_QUIET;
}
public function isVerbose(): bool
{
return $this->verbosity >= self::VERBOSITY_VERBOSE;
}
public function isVeryVerbose(): bool
{
return $this->verbosity >= self::VERBOSITY_VERY_VERBOSE;
}
public function isDebug(): bool
{
return $this->verbosity >= self::VERBOSITY_DEBUG;
}
public function setDecorated(bool $decorated): void
{
$this->decorated = $decorated;
}
public function isDecorated(): bool
{
return $this->decorated;
}
public function setFormatter(OutputFormatterInterface $formatter): void
{
$this->formatter = $formatter;
}
public function getFormatter(): OutputFormatterInterface
{
return $this->formatter;
}
public function __call(string $name, array $arguments)
{
return $this->CLImate->$name(...$arguments);
}
public function getErrorOutput(): OutputInterface
{
return $this;
}
public function setErrorOutput(OutputInterface $error): void
{
// no-op
}
public function section(): ConsoleSectionOutput
{
return $this;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
abstract class Command extends \Symfony\Component\Console\Command\Command implements CommandInterface
{
protected QuestionHelper $helper;
public function __construct(?string $name = null, ?callable $code = null)
{
parent::__construct($name, $code);
$this->helper = new QuestionHelper();
$this->setHelperSet(new HelperSet([$this->helper]));
}
}

View File

@@ -4,12 +4,20 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands; namespace Siteworxpro\App\Cli\Commands;
use Siteworxpro\App\Cli\ClimateOutput;
use Symfony\Component\Console\Input\ArgvInput;
interface CommandInterface interface CommandInterface
{ {
/** /**
* Execute the command. * Execute the command.
* *
* @param ArgvInput|ClimateOutput $input
* @param ClimateOutput $output
* @return int * @return int
*/ */
public function execute(): int; public function __invoke(
ClimateOutput|ArgvInput $input,
ClimateOutput $output
): int;
} }

View File

@@ -4,40 +4,43 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands; namespace Siteworxpro\App\Cli\Commands;
use Ahc\Cli\Input\Command; use League\CLImate\TerminalObject\Dynamic\Progress;
use Siteworxpro\App\Cli\ClimateOutput;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand; use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus; use Siteworxpro\App\Services\Facades\CommandBus;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\OutputInterface;
class DemoCommand extends Command implements CommandInterface #[AsCommand('demo', 'A demo command to showcase the CLI functionality.')]
class DemoCommand extends Command
{ {
public function __construct() protected function configure(): void
{ {
parent::__construct('api:demo', 'A demo command to showcase the CLI functionality.'); $this->addArgument('name', null, 'Your name');
$this->addOption('greet', 'g', null, 'Include a greeting message');
$this->argument('[name]', 'Your name')
->option('-g, --greet', 'Include a greeting message');
} }
public function execute(): int public function __invoke(ClimateOutput|ArgvInput $input, ClimateOutput|OutputInterface $output): int
{ {
$pb = $this->progress(100); /** @var Progress $pb */
$pb = $output->progress(100);
for ($i = 0; $i < 100; $i += 10) { for ($i = 0; $i < 100; $i += 10) {
usleep(100000); // Simulate work usleep(100000); // Simulate work
$pb->advance(10); $pb->advance(10);
} }
$pb->finish(); $output->bold()->blue("Demo Command Executed!\n");
$name = $input->getArgument('name');
$this->writer()->boldBlue("Demo Command Executed!\n"); $greet = $input->getOption('greet') !== null;
$name = $this->values()['name'];
$greet = $this->values()['greet'] ?? false;
if ($greet) { if ($greet) {
$this->writer()->green("Hello, $name! Welcome to the CLI demo.\n"); $output->green("Hello, $name! Welcome to the CLI demo.\n");
} else { } else {
$exampleCommand = new ExampleCommand($name); $exampleCommand = new ExampleCommand($name);
$this->writer()->yellow(CommandBus::handle($exampleCommand)); $output->yellow(CommandBus::handle($exampleCommand));
} }
return 0; return 0;

View File

@@ -4,24 +4,29 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\Queue; namespace Siteworxpro\App\Cli\Commands\Queue;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\Async\Consumer; use Siteworxpro\App\Async\Consumer;
use Siteworxpro\App\Async\Messages\SayHelloMessage; use Siteworxpro\App\Cli\ClimateOutput;
use Siteworxpro\App\Cli\Commands\CommandInterface; use Siteworxpro\App\Cli\Commands\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\ArgvInput;
class Start extends Command implements CommandInterface #[AsCommand('queue:start', 'Start the queue consumer to process messages.')]
class Start extends Command
{ {
public function __construct() protected function configure(): void
{ {
parent::__construct('queue:start', 'Start the queue consumer to process messages.'); $this->addArgument(
$this->argument('[queues]', 'The name of the queue to consume from. ex. "first_queue,second_queue"'); 'queues',
null,
'The name of the queue to consume from. ex. "first_queue,second_queue"'
);
} }
public function execute(): int public function __invoke(ClimateOutput|ArgvInput $input, ClimateOutput $output): int
{ {
$queues = []; $queues = [];
if ($this->values()['queues'] !== null) { if ($input->getArgument('queues') !== null) {
$queues = explode(',', $this->values()['queues']); $queues = explode(',', $input->getArgument('queues'));
} }
$consumer = new Consumer($queues); $consumer = new Consumer($queues);

View File

@@ -4,28 +4,22 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\Queue; namespace Siteworxpro\App\Cli\Commands\Queue;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\Async\Messages\SayHelloMessage; use Siteworxpro\App\Async\Messages\SayHelloMessage;
use Siteworxpro\App\Cli\Commands\CommandInterface; use Siteworxpro\App\Cli\ClimateOutput;
use Siteworxpro\App\Cli\Commands\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\OutputInterface;
/** /**
* Class TestJob * Class TestJob
* *
* A CLI command to schedule a demo job that dispatches a SayHelloMessage. * A CLI command to schedule a demo job that dispatches a SayHelloMessage.
*/ */
class TestJob extends Command implements CommandInterface #[AsCommand('queue:test-job', 'Dispatches a SayHelloMessage to demonstrate queue functionality')]
class TestJob extends Command
{ {
public function __construct() public function __invoke(ClimateOutput|ArgvInput $input, ClimateOutput|OutputInterface $output): int
{
parent::__construct('queue:demo', 'Schedule a demo job.');
}
/**
* Execute the command to dispatch a SayHelloMessage.
*
* @return int Exit code
*/
public function execute(): int
{ {
SayHelloMessage::dispatch('World from TestJob Command!'); SayHelloMessage::dispatch('World from TestJob Command!');