feat: complete TaskerBridges with Symfony, Laravel, and Native adapters
This commit is contained in:
parent
eead487714
commit
da1efaba55
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tasker-bridges:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: TaskerBridges
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.2', '8.3']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: none
|
||||
tools: phpstan, php-cs-fixer, phpunit
|
||||
- name: Validate composer.json
|
||||
run: php -v && composer validate --strict
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
- name: PHPUnit
|
||||
run: vendor/bin/phpunit --configuration phpunit.xml --display-deprecations
|
||||
- name: PHPStan
|
||||
run: vendor/bin/phpstan analyse --no-progress --memory-limit=512M
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.idea/
|
||||
/vendor/
|
||||
/composer.lock
|
||||
/.phpunit.cache/
|
||||
/phpstan-baseline.neon
|
||||
|
|
@ -3,25 +3,25 @@
|
|||
This document defines the implementation milestones for the `getphred/tasker-bridges` package.
|
||||
|
||||
## 1. Bridge Infrastructure
|
||||
- [ ] Initialize repository with `getphred/console-contracts` dependency.
|
||||
- [ ] Set up testing environment for multi-toolkit validation (mocking Symfony/Laravel).
|
||||
- [x] Initialize repository with `getphred/console-contracts` dependency.
|
||||
- [x] Set up testing environment for multi-toolkit validation (mocking Symfony/Laravel).
|
||||
|
||||
## 2. Symfony Bridge
|
||||
- [ ] Implement `SymfonyCommandAdapter`.
|
||||
- [ ] Implement `SymfonyInputAdapter` and `SymfonyOutputAdapter`.
|
||||
- [ ] Implement helper adapters (Interaction, ProgressBar, Table).
|
||||
- [ ] Implement fixed markup translation (mapping to Symfony Formatter).
|
||||
- [x] Implement `SymfonyCommandAdapter`.
|
||||
- [x] Implement `SymfonyInputAdapter` and `SymfonyOutputAdapter`.
|
||||
- [x] Implement helper adapters (Interaction, ProgressBar, Table).
|
||||
- [x] Implement fixed markup translation (mapping to Symfony Formatter).
|
||||
|
||||
## 3. Laravel Bridge
|
||||
- [ ] Implement `LaravelCommandAdapter` and `LaravelServiceProvider`.
|
||||
- [ ] Integrate Symfony IO adapters within the Laravel context.
|
||||
- [ ] Implement native Laravel interaction helper mappings.
|
||||
- [x] Implement `LaravelCommandAdapter` and `LaravelServiceProvider`.
|
||||
- [x] Integrate Symfony IO adapters within the Laravel context.
|
||||
- [x] Implement native Laravel interaction helper mappings.
|
||||
|
||||
## 4. Phred (Native) Bridge
|
||||
- [ ] Implement ANSI-based `OutputInterface` with full markup support.
|
||||
- [ ] Implement lightweight interaction and progress helpers.
|
||||
- [ ] Implement regex-based Markdown-to-Markup converter.
|
||||
- [x] Implement ANSI-based `OutputInterface` with full markup support.
|
||||
- [x] Implement lightweight interaction and progress helpers.
|
||||
- [x] Implement regex-based Markdown-to-Markup converter.
|
||||
|
||||
## 5. Universal Integration
|
||||
- [ ] Implement universal exit code translation logic.
|
||||
- [ ] Implement global flag mapping (Verbosity, Decoration, Interactivity).
|
||||
- [x] Implement universal exit code translation logic.
|
||||
- [x] Implement global flag mapping (Verbosity, Decoration, Interactivity).
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@
|
|||
"php": "^8.2",
|
||||
"getphred/console-contracts": "dev-master"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"symfony/console": "^6.0 || ^7.0",
|
||||
"illuminate/console": "^10.0 || ^11.0",
|
||||
"illuminate/support": "^10.0 || ^11.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../ConsoleContracts"
|
||||
}
|
||||
],
|
||||
"suggest": {
|
||||
"getphred/tasker": "To use the PhredBridge with the Tasker runner",
|
||||
"symfony/console": "To use the SymfonyBridge",
|
||||
|
|
|
|||
5
phpstan.neon
Normal file
5
phpstan.neon
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
parameters:
|
||||
level: 8
|
||||
paths:
|
||||
- src
|
||||
treatPhpDocTypesAsCertain: false
|
||||
24
phpunit.xml
Normal file
24
phpunit.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
<testsuite name="Phred TaskerBridges Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage/>
|
||||
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
37
src/ExitCodeTranslator.php
Normal file
37
src/ExitCodeTranslator.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges;
|
||||
|
||||
use Phred\ConsoleContracts\ExitCode;
|
||||
use Throwable;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Translates exceptions and codes into Phred standardized exit codes.
|
||||
*/
|
||||
final class ExitCodeTranslator
|
||||
{
|
||||
/**
|
||||
* Translates an exception to an exit code.
|
||||
*
|
||||
* @param Throwable $throwable
|
||||
* @return int
|
||||
*/
|
||||
public static function translate(Throwable $throwable): int
|
||||
{
|
||||
if ($throwable instanceof InvalidArgumentException) {
|
||||
return ExitCode::USAGE;
|
||||
}
|
||||
|
||||
if ($throwable instanceof RuntimeException || $throwable instanceof LogicException) {
|
||||
return ExitCode::SOFTWARE;
|
||||
}
|
||||
|
||||
// Default to generic failure
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
45
src/Laravel/LaravelCommandAdapter.php
Normal file
45
src/Laravel/LaravelCommandAdapter.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Laravel;
|
||||
|
||||
use Illuminate\Console\Command as LaravelCommand;
|
||||
use Phred\ConsoleContracts\CommandInterface;
|
||||
use Phred\TaskerBridges\Symfony\SymfonyInputAdapter;
|
||||
use Phred\TaskerBridges\Symfony\SymfonyOutputAdapter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Adapter to run Phred commands within a Laravel Artisan application.
|
||||
*/
|
||||
class LaravelCommandAdapter extends LaravelCommand
|
||||
{
|
||||
public function __construct(private readonly CommandInterface $phredCommand)
|
||||
{
|
||||
$this->name = $phredCommand->getName();
|
||||
$this->description = $phredCommand->getDescription();
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
foreach ($this->phredCommand->getArguments() as $name => $description) {
|
||||
$this->addArgument($name, InputArgument::OPTIONAL, $description);
|
||||
}
|
||||
|
||||
foreach ($this->phredCommand->getOptions() as $name => $description) {
|
||||
$this->addOption($name, null, InputOption::VALUE_OPTIONAL, $description);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$phredInput = new SymfonyInputAdapter($this->input);
|
||||
$phredOutput = new SymfonyOutputAdapter($this->output);
|
||||
|
||||
return $this->phredCommand->execute($phredInput, $phredOutput);
|
||||
}
|
||||
}
|
||||
126
src/Laravel/LaravelServiceProvider.php
Normal file
126
src/Laravel/LaravelServiceProvider.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Laravel;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Phred\ConsoleContracts\CommandInterface;
|
||||
|
||||
/**
|
||||
* Service provider to automatically discover and register Phred commands in Laravel.
|
||||
*/
|
||||
class LaravelServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if (!$this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->discoverCommands();
|
||||
}
|
||||
|
||||
private function discoverCommands(): void
|
||||
{
|
||||
$basePath = function_exists('base_path') ? base_path() : getcwd();
|
||||
|
||||
// 1. Discover via composer.json (package-based/explicitly registered)
|
||||
$this->discoverFromComposer($basePath);
|
||||
|
||||
// 2. Discover via app/Console/Commands (Laravel standard)
|
||||
$this->discoverFromDirectory($basePath . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'Console' . DIRECTORY_SEPARATOR . 'Commands');
|
||||
}
|
||||
|
||||
private function discoverFromComposer(string $basePath): void
|
||||
{
|
||||
$rootComposer = $basePath . DIRECTORY_SEPARATOR . 'composer.json';
|
||||
if (!file_exists($rootComposer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = file_get_contents($rootComposer);
|
||||
if ($json === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$composerData = json_decode($json, true);
|
||||
if (!is_array($composerData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commands = $composerData['extra']['phred-tasker']['commands'] ?? [];
|
||||
$adapters = [];
|
||||
|
||||
foreach ((array)$commands as $commandClass) {
|
||||
if (is_string($commandClass) && is_subclass_of($commandClass, CommandInterface::class)) {
|
||||
$adapters[] = new LaravelCommandAdapter($this->app->make($commandClass));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($adapters)) {
|
||||
$this->commands($adapters);
|
||||
}
|
||||
}
|
||||
|
||||
private function discoverFromDirectory(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adapters = [];
|
||||
$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $this->resolveClassName($file->getPathname());
|
||||
|
||||
if ($className && is_subclass_of($className, CommandInterface::class)) {
|
||||
$adapters[] = new LaravelCommandAdapter($this->app->make($className));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($adapters)) {
|
||||
$this->commands($adapters);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveClassName(string $path): ?string
|
||||
{
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokens = token_get_all($content);
|
||||
$namespace = '';
|
||||
$class = '';
|
||||
$gettingNamespace = false;
|
||||
$gettingClass = false;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (is_array($token)) {
|
||||
if ($token[0] === T_NAMESPACE) {
|
||||
$gettingNamespace = true;
|
||||
} elseif ($token[0] === T_CLASS) {
|
||||
$gettingClass = true;
|
||||
} elseif ($gettingNamespace) {
|
||||
if ($token[0] === T_STRING || $token[0] === T_NAME_QUALIFIED) {
|
||||
$namespace .= $token[1];
|
||||
} elseif ($token[1] === ';') {
|
||||
$gettingNamespace = false;
|
||||
}
|
||||
} elseif ($gettingClass && $token[0] === T_STRING) {
|
||||
$class = $token[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $namespace ? $namespace . '\\' . $class : $class;
|
||||
}
|
||||
}
|
||||
100
src/Phred/InteractionAdapter.php
Normal file
100
src/Phred/InteractionAdapter.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Phred;
|
||||
|
||||
use Phred\ConsoleContracts\InteractionInterface;
|
||||
|
||||
/**
|
||||
* Native Phred adapter for InteractionInterface.
|
||||
*/
|
||||
class InteractionAdapter implements InteractionInterface
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $inputStream;
|
||||
|
||||
/**
|
||||
* @param resource|null $inputStream
|
||||
*/
|
||||
public function __construct($inputStream = null)
|
||||
{
|
||||
$this->inputStream = $inputStream ?? STDIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function ask(string $question, ?string $default = null): string
|
||||
{
|
||||
$prompt = $question . ($default ? " [$default]" : '') . ': ';
|
||||
echo $prompt;
|
||||
|
||||
$input = trim(fgets($this->inputStream) ?: '');
|
||||
|
||||
if ($input === '' && $default !== null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function confirm(string $question, bool $default = true): bool
|
||||
{
|
||||
$prompt = $question . ($default ? ' [Y/n]' : ' [y/N]') . ': ';
|
||||
echo $prompt;
|
||||
|
||||
$input = strtolower(trim(fgets($this->inputStream) ?: ''));
|
||||
|
||||
if ($input === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $input === 'y' || $input === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function secret(string $question): string
|
||||
{
|
||||
echo $question . ': ';
|
||||
|
||||
// Hide input using stty if available
|
||||
$sttyAvailable = shell_exec('stty 2>&1') !== null;
|
||||
|
||||
if ($sttyAvailable) {
|
||||
shell_exec('stty -echo');
|
||||
}
|
||||
|
||||
$input = trim(fgets($this->inputStream) ?: '');
|
||||
|
||||
if ($sttyAvailable) {
|
||||
shell_exec('stty echo');
|
||||
echo PHP_EOL;
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function choice(string $question, array $choices, mixed $default = null): mixed
|
||||
{
|
||||
echo $question . ':' . PHP_EOL;
|
||||
|
||||
foreach ($choices as $key => $choice) {
|
||||
echo " [$key] $choice" . PHP_EOL;
|
||||
}
|
||||
|
||||
$input = $this->ask('Choice', (string)$default);
|
||||
|
||||
return $choices[$input] ?? $default;
|
||||
}
|
||||
}
|
||||
150
src/Phred/MarkdownConverter.php
Normal file
150
src/Phred/MarkdownConverter.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Phred;
|
||||
|
||||
use Phred\ConsoleContracts\Helpers\MarkdownConverterInterface;
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
|
||||
/**
|
||||
* Native Phred adapter for MarkdownConverterInterface.
|
||||
*/
|
||||
class MarkdownConverter implements MarkdownConverterInterface
|
||||
{
|
||||
public function __construct(private readonly OutputInterface $output)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convert(string $markdown): string
|
||||
{
|
||||
$result = $markdown;
|
||||
|
||||
// Headers
|
||||
$result = (string)preg_replace('/^# (.*)$/m', '<info><b>$1</b></info>', $result);
|
||||
$result = (string)preg_replace('/^## (.*)$/m', '<info>$1</info>', $result);
|
||||
$result = (string)preg_replace('/^### (.*)$/m', '<b>$1</b>', $result);
|
||||
|
||||
// Bold
|
||||
$result = (string)preg_replace('/\*\*(.*?)\*\*/', '<b>$1</b>', $result);
|
||||
$result = (string)preg_replace('/__(.*?)__/', '<b>$1</b>', $result);
|
||||
|
||||
// Italic
|
||||
$result = (string)preg_replace('/\*(.*?)\*/', '<i>$1</i>', $result);
|
||||
$result = (string)preg_replace('/_(.*?)_/', '<i>$1</i>', $result);
|
||||
|
||||
// Lists
|
||||
$result = (string)preg_replace('/^[-*] (.*)$/m', ' • $1', $result);
|
||||
|
||||
// Code blocks
|
||||
$result = (string)preg_replace('/```(.*?)```/s', '<comment>$1</comment>', $result);
|
||||
$result = (string)preg_replace('/`(.*?)`/', '<comment>$1</comment>', $result);
|
||||
|
||||
// Tables
|
||||
$result = $this->parseTables($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseTables(string $markdown): string
|
||||
{
|
||||
$lines = explode(PHP_EOL, $markdown);
|
||||
$result = [];
|
||||
$tableLines = [];
|
||||
$inTable = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$isSeparator = (bool)preg_match('/^\|?[:\s-]*\|[:\s-]*\|?/', trim($line));
|
||||
$isRow = (bool)preg_match('/^\|.*\|$/', trim($line));
|
||||
|
||||
if ($isRow || ($inTable && $isSeparator)) {
|
||||
$inTable = true;
|
||||
$tableLines[] = $line;
|
||||
} else {
|
||||
if ($inTable) {
|
||||
$result[] = $this->renderTable($tableLines);
|
||||
$tableLines = [];
|
||||
$inTable = false;
|
||||
}
|
||||
$result[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if ($inTable) {
|
||||
$result[] = $this->renderTable($tableLines);
|
||||
}
|
||||
|
||||
return implode(PHP_EOL, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $lines
|
||||
*/
|
||||
private function renderTable(array $lines): string
|
||||
{
|
||||
if (count($lines) < 2) {
|
||||
return implode(PHP_EOL, $lines);
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
$rows = [];
|
||||
$alignments = [];
|
||||
$headerLine = array_shift($lines);
|
||||
$separatorLine = array_shift($lines);
|
||||
|
||||
// Parse Headers
|
||||
$headers = array_map('trim', explode('|', trim($headerLine, '|')));
|
||||
|
||||
// Parse Alignments
|
||||
if ($separatorLine !== null) {
|
||||
$parts = explode('|', trim($separatorLine, '|'));
|
||||
foreach ($parts as $index => $part) {
|
||||
$part = trim($part);
|
||||
if (str_starts_with($part, ':') && str_ends_with($part, ':')) {
|
||||
$alignments[$index] = 'center';
|
||||
} elseif (str_ends_with($part, ':')) {
|
||||
$alignments[$index] = 'right';
|
||||
} else {
|
||||
$alignments[$index] = 'left';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Rows
|
||||
foreach ($lines as $line) {
|
||||
$rows[] = array_map('trim', explode('|', trim($line, '|')));
|
||||
}
|
||||
|
||||
// Use TableAdapter to render
|
||||
$table = new TableAdapter($this->output);
|
||||
$table->setHeaders($headers);
|
||||
$table->setColumnAlignments($alignments);
|
||||
foreach ($rows as $row) {
|
||||
$table->addRow($row);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$table->render();
|
||||
return trim((string)ob_get_clean());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertFile(string $path): string
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->convert($content);
|
||||
}
|
||||
}
|
||||
75
src/Phred/ProgressBarAdapter.php
Normal file
75
src/Phred/ProgressBarAdapter.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Phred;
|
||||
|
||||
use Phred\ConsoleContracts\Helpers\ProgressBarInterface;
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
|
||||
/**
|
||||
* Native Phred adapter for ProgressBarInterface.
|
||||
*/
|
||||
class ProgressBarAdapter implements ProgressBarInterface
|
||||
{
|
||||
private int $max = 0;
|
||||
|
||||
private int $current = 0;
|
||||
|
||||
public function __construct(private readonly OutputInterface $output)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(int $max = 0): void
|
||||
{
|
||||
$this->max = $max;
|
||||
$this->current = 0;
|
||||
$this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function advance(int $step = 1): void
|
||||
{
|
||||
$this->current += $step;
|
||||
if ($this->max > 0 && $this->current > $this->max) {
|
||||
$this->current = $this->max;
|
||||
}
|
||||
$this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function finish(): void
|
||||
{
|
||||
if ($this->max > 0) {
|
||||
$this->current = $this->max;
|
||||
}
|
||||
$this->render();
|
||||
$this->output->writeln('');
|
||||
}
|
||||
|
||||
private function render(): void
|
||||
{
|
||||
$percent = $this->max > 0 ? ($this->current / $this->max) * 100 : 0;
|
||||
$barWidth = 40;
|
||||
$filledWidth = $this->max > 0 ? (int)round(($this->current / $this->max) * $barWidth) : 0;
|
||||
|
||||
$bar = str_repeat('█', $filledWidth) . str_repeat('_', $barWidth - $filledWidth);
|
||||
|
||||
$message = sprintf(
|
||||
"\r [%s] %d%% (%d/%d)",
|
||||
$bar,
|
||||
$percent,
|
||||
$this->current,
|
||||
$this->max
|
||||
);
|
||||
|
||||
$this->output->write($message);
|
||||
}
|
||||
}
|
||||
137
src/Phred/TableAdapter.php
Normal file
137
src/Phred/TableAdapter.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Phred;
|
||||
|
||||
use Phred\ConsoleContracts\Helpers\TableInterface;
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
|
||||
/**
|
||||
* Native Phred adapter for TableInterface.
|
||||
*/
|
||||
class TableAdapter implements TableInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $headers = [];
|
||||
|
||||
/**
|
||||
* @var array<array<mixed>>
|
||||
*/
|
||||
private array $rows = [];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $alignments = [];
|
||||
|
||||
public function __construct(private readonly OutputInterface $output)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setHeaders(array $headers): void
|
||||
{
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addRow(array $row): void
|
||||
{
|
||||
$this->rows[] = $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setColumnAlignments(array $alignments): void
|
||||
{
|
||||
$this->alignments = $alignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
if (empty($this->headers) && empty($this->rows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columnWidths = $this->calculateColumnWidths();
|
||||
|
||||
// Render headers
|
||||
if (!empty($this->headers)) {
|
||||
$this->renderRow($this->headers, $columnWidths);
|
||||
$this->renderSeparator($columnWidths);
|
||||
}
|
||||
|
||||
// Render rows
|
||||
foreach ($this->rows as $row) {
|
||||
$this->renderRow($row, $columnWidths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function calculateColumnWidths(): array
|
||||
{
|
||||
$widths = [];
|
||||
|
||||
foreach ($this->headers as $index => $header) {
|
||||
$widths[$index] = mb_strlen((string)$header);
|
||||
}
|
||||
|
||||
foreach ($this->rows as $row) {
|
||||
foreach ($row as $index => $value) {
|
||||
$width = mb_strlen((string)$value);
|
||||
if (!isset($widths[$index]) || $width > $widths[$index]) {
|
||||
$widths[$index] = $width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $widths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $row
|
||||
* @param array<int, int> $widths
|
||||
*/
|
||||
private function renderRow(array $row, array $widths): void
|
||||
{
|
||||
$formatted = '|';
|
||||
foreach ($widths as $index => $width) {
|
||||
$value = (string)($row[$index] ?? '');
|
||||
$alignment = $this->alignments[$index] ?? 'left';
|
||||
|
||||
$padType = match ($alignment) {
|
||||
'right' => STR_PAD_LEFT,
|
||||
'center' => STR_PAD_BOTH,
|
||||
default => STR_PAD_RIGHT,
|
||||
};
|
||||
|
||||
$formatted .= ' ' . str_pad($value, $width, ' ', $padType) . ' |';
|
||||
}
|
||||
$this->output->writeln($formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $widths
|
||||
*/
|
||||
private function renderSeparator(array $widths): void
|
||||
{
|
||||
$separator = '+';
|
||||
foreach ($widths as $width) {
|
||||
$separator .= str_repeat('-', $width + 2) . '+';
|
||||
}
|
||||
$this->output->writeln($separator);
|
||||
}
|
||||
}
|
||||
44
src/Symfony/SymfonyCommandAdapter.php
Normal file
44
src/Symfony/SymfonyCommandAdapter.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\CommandInterface;
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface as SymfonyInputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface as SymfonyOutputInterface;
|
||||
|
||||
/**
|
||||
* Adapter to run Phred commands within a Symfony Console application.
|
||||
*/
|
||||
class SymfonyCommandAdapter extends SymfonyCommand
|
||||
{
|
||||
public function __construct(private readonly CommandInterface $phredCommand)
|
||||
{
|
||||
parent::__construct($phredCommand->getName());
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription($this->phredCommand->getDescription());
|
||||
|
||||
foreach ($this->phredCommand->getArguments() as $name => $description) {
|
||||
$this->addArgument($name, InputArgument::OPTIONAL, $description);
|
||||
}
|
||||
|
||||
foreach ($this->phredCommand->getOptions() as $name => $description) {
|
||||
$this->addOption($name, null, InputOption::VALUE_OPTIONAL, $description);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(SymfonyInputInterface $input, SymfonyOutputInterface $output): int
|
||||
{
|
||||
$phredInput = new SymfonyInputAdapter($input);
|
||||
$phredOutput = new SymfonyOutputAdapter($output);
|
||||
|
||||
return $this->phredCommand->execute($phredInput, $phredOutput);
|
||||
}
|
||||
}
|
||||
39
src/Symfony/SymfonyFormatter.php
Normal file
39
src/Symfony/SymfonyFormatter.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Helper to register Phred tags in Symfony OutputFormatter.
|
||||
*/
|
||||
final class SymfonyFormatter
|
||||
{
|
||||
/**
|
||||
* Registers Phred tags in the given output formatter.
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* @return void
|
||||
*/
|
||||
public static function register(OutputInterface $output): void
|
||||
{
|
||||
$formatter = $output->getFormatter();
|
||||
|
||||
if (!$formatter->hasStyle('success')) {
|
||||
$formatter->setStyle('success', new OutputFormatterStyle('green'));
|
||||
}
|
||||
|
||||
if (!$formatter->hasStyle('info')) {
|
||||
$formatter->setStyle('info', new OutputFormatterStyle('blue'));
|
||||
}
|
||||
|
||||
if (!$formatter->hasStyle('warning')) {
|
||||
$formatter->setStyle('warning', new OutputFormatterStyle('black', 'yellow'));
|
||||
}
|
||||
|
||||
// Symfony already has <error> and <comment> by default
|
||||
}
|
||||
}
|
||||
42
src/Symfony/SymfonyInputAdapter.php
Normal file
42
src/Symfony/SymfonyInputAdapter.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface as SymfonyInputInterface;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony InputInterface.
|
||||
*/
|
||||
class SymfonyInputAdapter implements InputInterface
|
||||
{
|
||||
public function __construct(private readonly SymfonyInputInterface $symfonyInput)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getArgument(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->symfonyInput->getArgument($name) ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOption(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->symfonyInput->getOption($name) ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasOption(string $name): bool
|
||||
{
|
||||
return $this->symfonyInput->hasOption($name);
|
||||
}
|
||||
}
|
||||
64
src/Symfony/SymfonyInteractionAdapter.php
Normal file
64
src/Symfony/SymfonyInteractionAdapter.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\InteractionInterface;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface as SymfonyInputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface as SymfonyOutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony InteractionInterface using QuestionHelper.
|
||||
*/
|
||||
class SymfonyInteractionAdapter implements InteractionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SymfonyInputInterface $input,
|
||||
private readonly SymfonyOutputInterface $output,
|
||||
private readonly QuestionHelper $helper
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function ask(string $question, ?string $default = null): string
|
||||
{
|
||||
$q = new Question($question, $default);
|
||||
return (string)$this->helper->ask($this->input, $this->output, $q);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function confirm(string $question, bool $default = true): bool
|
||||
{
|
||||
$q = new ConfirmationQuestion($question, $default);
|
||||
return (bool)$this->helper->ask($this->input, $this->output, $q);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function secret(string $question): string
|
||||
{
|
||||
$q = new Question($question);
|
||||
$q->setHidden(true);
|
||||
$q->setHiddenFallback(true);
|
||||
return (string)$this->helper->ask($this->input, $this->output, $q);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function choice(string $question, array $choices, mixed $default = null): mixed
|
||||
{
|
||||
$q = new ChoiceQuestion($question, $choices, $default);
|
||||
return $this->helper->ask($this->input, $this->output, $q);
|
||||
}
|
||||
}
|
||||
108
src/Symfony/SymfonyOutputAdapter.php
Normal file
108
src/Symfony/SymfonyOutputAdapter.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface as SymfonyOutputInterface;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony OutputInterface.
|
||||
*/
|
||||
class SymfonyOutputAdapter implements OutputInterface
|
||||
{
|
||||
public function __construct(private readonly SymfonyOutputInterface $symfonyOutput)
|
||||
{
|
||||
SymfonyFormatter::register($this->symfonyOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write(string $message): void
|
||||
{
|
||||
$this->symfonyOutput->write($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function writeln(string $message): void
|
||||
{
|
||||
$this->symfonyOutput->writeln($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function success(string $message): void
|
||||
{
|
||||
$this->writeln("<success>$message</success>");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function info(string $message): void
|
||||
{
|
||||
$this->writeln("<info>$message</info>");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function error(string $message): void
|
||||
{
|
||||
$this->writeln("<error>$message</error>");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function warning(string $message): void
|
||||
{
|
||||
$this->writeln("<warning>$message</warning>");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function comment(string $message): void
|
||||
{
|
||||
$this->writeln("<comment>$message</comment>");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setVerbosity(int $level): void
|
||||
{
|
||||
// Map Phred verbosity to Symfony verbosity
|
||||
$symfonyLevel = match ($level) {
|
||||
0 => SymfonyOutputInterface::VERBOSITY_QUIET,
|
||||
1 => SymfonyOutputInterface::VERBOSITY_NORMAL,
|
||||
2 => SymfonyOutputInterface::VERBOSITY_VERBOSE,
|
||||
3 => SymfonyOutputInterface::VERBOSITY_VERY_VERBOSE,
|
||||
4 => SymfonyOutputInterface::VERBOSITY_DEBUG,
|
||||
default => SymfonyOutputInterface::VERBOSITY_NORMAL,
|
||||
};
|
||||
$this->symfonyOutput->setVerbosity($symfonyLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getVerbosity(): int
|
||||
{
|
||||
$symfonyLevel = $this->symfonyOutput->getVerbosity();
|
||||
return match ($symfonyLevel) {
|
||||
SymfonyOutputInterface::VERBOSITY_QUIET => 0,
|
||||
SymfonyOutputInterface::VERBOSITY_NORMAL => 1,
|
||||
SymfonyOutputInterface::VERBOSITY_VERBOSE => 2,
|
||||
SymfonyOutputInterface::VERBOSITY_VERY_VERBOSE => 3,
|
||||
SymfonyOutputInterface::VERBOSITY_DEBUG => 4,
|
||||
default => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
42
src/Symfony/SymfonyProgressBarAdapter.php
Normal file
42
src/Symfony/SymfonyProgressBarAdapter.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\Helpers\ProgressBarInterface;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony ProgressBar.
|
||||
*/
|
||||
class SymfonyProgressBarAdapter implements ProgressBarInterface
|
||||
{
|
||||
public function __construct(private readonly ProgressBar $progressBar)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(int $max = 0): void
|
||||
{
|
||||
$this->progressBar->start($max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function advance(int $step = 1): void
|
||||
{
|
||||
$this->progressBar->advance($step);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function finish(): void
|
||||
{
|
||||
$this->progressBar->finish();
|
||||
}
|
||||
}
|
||||
54
src/Symfony/SymfonyTableAdapter.php
Normal file
54
src/Symfony/SymfonyTableAdapter.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Symfony;
|
||||
|
||||
use Phred\ConsoleContracts\Helpers\TableInterface;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony Table.
|
||||
*/
|
||||
class SymfonyTableAdapter implements TableInterface
|
||||
{
|
||||
public function __construct(private readonly Table $table)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setHeaders(array $headers): void
|
||||
{
|
||||
$this->table->setHeaders($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addRow(array $row): void
|
||||
{
|
||||
$this->table->addRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setColumnAlignments(array $alignments): void
|
||||
{
|
||||
// Symfony doesn't have a direct Index => Alignment method on the Table helper
|
||||
// that works exactly like this, but we can set them individually
|
||||
foreach ($alignments as $index => $alignment) {
|
||||
$this->table->setColumnStyle($index, $alignment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
$this->table->render();
|
||||
}
|
||||
}
|
||||
51
tests/LaravelBridgeTest.php
Normal file
51
tests/LaravelBridgeTest.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Phred\ConsoleContracts\CommandInterface;
|
||||
use Phred\ConsoleContracts\InputInterface;
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
use Phred\TaskerBridges\Laravel\LaravelCommandAdapter;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class LaravelBridgeTest extends TestCase
|
||||
{
|
||||
public function testLaravelCommandAdapter(): void
|
||||
{
|
||||
$phredCommand = new class implements CommandInterface {
|
||||
public function getName(): string { return 'test:phred'; }
|
||||
public function getDescription(): string { return 'Test description'; }
|
||||
public function getArguments(): array { return ['name' => 'Name']; }
|
||||
public function getOptions(): array { return []; }
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->info('Hello ' . $input->getArgument('name'));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
$adapter = new LaravelCommandAdapter($phredCommand);
|
||||
|
||||
// Mock Laravel components
|
||||
$symfonyInput = new ArrayInput(['name' => 'John'], $adapter->getDefinition());
|
||||
$symfonyOutput = new BufferedOutput();
|
||||
|
||||
// Use reflection to set protected properties as Laravel's constructor and run logic is complex
|
||||
$refl = new \ReflectionClass($adapter);
|
||||
|
||||
$inputProp = $refl->getProperty('input');
|
||||
$inputProp->setValue($adapter, $symfonyInput);
|
||||
|
||||
$outputProp = $refl->getProperty('output');
|
||||
$outputProp->setValue($adapter, $symfonyOutput);
|
||||
|
||||
$exitCode = $adapter->handle();
|
||||
|
||||
$this->assertEquals(0, $exitCode);
|
||||
$this->assertStringContainsString('Hello John', $symfonyOutput->fetch());
|
||||
}
|
||||
}
|
||||
136
tests/PhredBridgeTest.php
Normal file
136
tests/PhredBridgeTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Phred\TaskerBridges\Phred\InputAdapter;
|
||||
use Phred\TaskerBridges\Phred\OutputAdapter;
|
||||
use Phred\TaskerBridges\Phred\TableAdapter;
|
||||
use Phred\TaskerBridges\Phred\MarkdownConverter;
|
||||
use Phred\TaskerBridges\Phred\InteractionAdapter;
|
||||
use Phred\TaskerBridges\Phred\ProgressBarAdapter;
|
||||
|
||||
class PhredBridgeTest extends TestCase
|
||||
{
|
||||
public function testInputAdapter(): void
|
||||
{
|
||||
$adapter = new InputAdapter(
|
||||
['arg1' => 'val1'],
|
||||
['opt1' => 'val2']
|
||||
);
|
||||
|
||||
$this->assertEquals('val1', $adapter->getArgument('arg1'));
|
||||
$this->assertEquals('default', $adapter->getArgument('arg2', 'default'));
|
||||
$this->assertEquals('val2', $adapter->getOption('opt1'));
|
||||
$this->assertTrue($adapter->hasOption('opt1'));
|
||||
$this->assertFalse($adapter->hasOption('opt2'));
|
||||
}
|
||||
|
||||
public function testOutputAdapterFormatting(): void
|
||||
{
|
||||
$adapter = new OutputAdapter(true);
|
||||
|
||||
ob_start();
|
||||
$adapter->info('test');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString("\033[34mtest\033[0m", $output);
|
||||
|
||||
$adapter = new OutputAdapter(false);
|
||||
ob_start();
|
||||
$adapter->info('test');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertEquals("test" . PHP_EOL, $output);
|
||||
}
|
||||
|
||||
public function testTableAdapter(): void
|
||||
{
|
||||
$output = new OutputAdapter(false);
|
||||
$table = new TableAdapter($output);
|
||||
|
||||
$table->setHeaders(['ID', 'Name', 'Status']);
|
||||
$table->setColumnAlignments([0 => 'right', 1 => 'left', 2 => 'center']);
|
||||
$table->addRow([1, 'John', 'Active']);
|
||||
$table->addRow([100, 'Jane', 'Inactive']);
|
||||
|
||||
ob_start();
|
||||
$table->render();
|
||||
$content = ob_get_clean();
|
||||
|
||||
// ID column (right aligned)
|
||||
$this->assertStringContainsString('| 1 |', $content);
|
||||
$this->assertStringContainsString('| 100 |', $content);
|
||||
|
||||
// Name column (left aligned)
|
||||
$this->assertStringContainsString('| John |', $content);
|
||||
|
||||
// Status column (center aligned)
|
||||
$this->assertStringContainsString('| Active |', $content);
|
||||
}
|
||||
|
||||
public function testMarkdownConverter(): void
|
||||
{
|
||||
$output = new OutputAdapter(false);
|
||||
$converter = new MarkdownConverter($output);
|
||||
|
||||
$this->assertEquals('<info><b>Title</b></info>', $converter->convert('# Title'));
|
||||
$this->assertEquals('<b>Bold</b>', $converter->convert('**Bold**'));
|
||||
$this->assertEquals('<i>Italic</i>', $converter->convert('*Italic*'));
|
||||
$this->assertEquals('<comment>code</comment>', $converter->convert('`code`'));
|
||||
}
|
||||
|
||||
public function testMarkdownTableConverter(): void
|
||||
{
|
||||
$output = new OutputAdapter(false);
|
||||
$converter = new MarkdownConverter($output);
|
||||
|
||||
$markdown = <<<MD
|
||||
| ID | Name |
|
||||
|---|---|
|
||||
| 1 | John |
|
||||
MD;
|
||||
|
||||
$result = $converter->convert($markdown);
|
||||
$this->assertStringContainsString('| ID | Name |', $result);
|
||||
$this->assertStringContainsString('| 1 | John |', $result);
|
||||
}
|
||||
|
||||
public function testInteractionAdapter(): void
|
||||
{
|
||||
$input = fopen('php://memory', 'r+');
|
||||
if ($input === false) {
|
||||
$this->fail('Could not open memory stream');
|
||||
}
|
||||
fwrite($input, "John\ny\nsecret\n1\n");
|
||||
rewind($input);
|
||||
|
||||
$adapter = new InteractionAdapter($input);
|
||||
|
||||
ob_start();
|
||||
$this->assertEquals('John', $adapter->ask('Name'));
|
||||
$this->assertTrue($adapter->confirm('Continue?'));
|
||||
$this->assertEquals('secret', $adapter->secret('Password'));
|
||||
$this->assertEquals('Choice 2', $adapter->choice('Select', ['0' => 'Choice 1', '1' => 'Choice 2']));
|
||||
ob_end_clean();
|
||||
|
||||
fclose($input);
|
||||
}
|
||||
|
||||
public function testProgressBarAdapter(): void
|
||||
{
|
||||
$output = new OutputAdapter(false);
|
||||
$bar = new ProgressBarAdapter($output);
|
||||
|
||||
ob_start();
|
||||
$bar->start(100);
|
||||
$bar->advance(50);
|
||||
$bar->finish();
|
||||
$content = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString('50%', $content);
|
||||
$this->assertStringContainsString('100%', $content);
|
||||
}
|
||||
}
|
||||
78
tests/SymfonyBridgeTest.php
Normal file
78
tests/SymfonyBridgeTest.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Phred\TaskerBridges\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Phred\ConsoleContracts\CommandInterface;
|
||||
use Phred\ConsoleContracts\InputInterface;
|
||||
use Phred\ConsoleContracts\OutputInterface;
|
||||
use Phred\TaskerBridges\Symfony\SymfonyCommandAdapter;
|
||||
use Phred\TaskerBridges\Symfony\SymfonyInputAdapter;
|
||||
use Phred\TaskerBridges\Symfony\SymfonyOutputAdapter;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface as SymfonyOutputInterface;
|
||||
|
||||
class SymfonyBridgeTest extends TestCase
|
||||
{
|
||||
public function testSymfonyCommandAdapter(): void
|
||||
{
|
||||
$phredCommand = new class implements CommandInterface {
|
||||
public function getName(): string { return 'test:phred'; }
|
||||
public function getDescription(): string { return 'Test description'; }
|
||||
public function getArguments(): array { return ['name' => 'Name']; }
|
||||
public function getOptions(): array { return []; }
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->info('Hello ' . $input->getArgument('name'));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
$adapter = new SymfonyCommandAdapter($phredCommand);
|
||||
$app = new Application();
|
||||
$app->add($adapter);
|
||||
$app->setAutoExit(false);
|
||||
|
||||
$input = new ArrayInput(['test:phred', 'name' => 'John']);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$exitCode = $app->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $exitCode);
|
||||
$this->assertStringContainsString('Hello John', $output->fetch());
|
||||
}
|
||||
|
||||
public function testVerbosityMapping(): void
|
||||
{
|
||||
$symfonyOutput = new BufferedOutput();
|
||||
$adapter = new SymfonyOutputAdapter($symfonyOutput);
|
||||
|
||||
$adapter->setVerbosity(0);
|
||||
$this->assertEquals(SymfonyOutputInterface::VERBOSITY_QUIET, $symfonyOutput->getVerbosity());
|
||||
|
||||
$adapter->setVerbosity(4);
|
||||
$this->assertEquals(SymfonyOutputInterface::VERBOSITY_DEBUG, $symfonyOutput->getVerbosity());
|
||||
|
||||
$symfonyOutput->setVerbosity(SymfonyOutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->assertEquals(2, $adapter->getVerbosity());
|
||||
}
|
||||
|
||||
public function testSymfonyHelpers(): void
|
||||
{
|
||||
$symfonyOutput = new BufferedOutput();
|
||||
$table = new \Symfony\Component\Console\Helper\Table($symfonyOutput);
|
||||
$adapter = new \Phred\TaskerBridges\Symfony\SymfonyTableAdapter($table);
|
||||
|
||||
$adapter->setHeaders(['ID', 'Name']);
|
||||
$adapter->addRow([1, 'John']);
|
||||
$adapter->render();
|
||||
|
||||
$output = $symfonyOutput->fetch();
|
||||
$this->assertStringContainsString('ID', $output);
|
||||
$this->assertStringContainsString('John', $output);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue