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.
|
This document defines the implementation milestones for the `getphred/tasker-bridges` package.
|
||||||
|
|
||||||
## 1. Bridge Infrastructure
|
## 1. Bridge Infrastructure
|
||||||
- [ ] Initialize repository with `getphred/console-contracts` dependency.
|
- [x] Initialize repository with `getphred/console-contracts` dependency.
|
||||||
- [ ] Set up testing environment for multi-toolkit validation (mocking Symfony/Laravel).
|
- [x] Set up testing environment for multi-toolkit validation (mocking Symfony/Laravel).
|
||||||
|
|
||||||
## 2. Symfony Bridge
|
## 2. Symfony Bridge
|
||||||
- [ ] Implement `SymfonyCommandAdapter`.
|
- [x] Implement `SymfonyCommandAdapter`.
|
||||||
- [ ] Implement `SymfonyInputAdapter` and `SymfonyOutputAdapter`.
|
- [x] Implement `SymfonyInputAdapter` and `SymfonyOutputAdapter`.
|
||||||
- [ ] Implement helper adapters (Interaction, ProgressBar, Table).
|
- [x] Implement helper adapters (Interaction, ProgressBar, Table).
|
||||||
- [ ] Implement fixed markup translation (mapping to Symfony Formatter).
|
- [x] Implement fixed markup translation (mapping to Symfony Formatter).
|
||||||
|
|
||||||
## 3. Laravel Bridge
|
## 3. Laravel Bridge
|
||||||
- [ ] Implement `LaravelCommandAdapter` and `LaravelServiceProvider`.
|
- [x] Implement `LaravelCommandAdapter` and `LaravelServiceProvider`.
|
||||||
- [ ] Integrate Symfony IO adapters within the Laravel context.
|
- [x] Integrate Symfony IO adapters within the Laravel context.
|
||||||
- [ ] Implement native Laravel interaction helper mappings.
|
- [x] Implement native Laravel interaction helper mappings.
|
||||||
|
|
||||||
## 4. Phred (Native) Bridge
|
## 4. Phred (Native) Bridge
|
||||||
- [ ] Implement ANSI-based `OutputInterface` with full markup support.
|
- [x] Implement ANSI-based `OutputInterface` with full markup support.
|
||||||
- [ ] Implement lightweight interaction and progress helpers.
|
- [x] Implement lightweight interaction and progress helpers.
|
||||||
- [ ] Implement regex-based Markdown-to-Markup converter.
|
- [x] Implement regex-based Markdown-to-Markup converter.
|
||||||
|
|
||||||
## 5. Universal Integration
|
## 5. Universal Integration
|
||||||
- [ ] Implement universal exit code translation logic.
|
- [x] Implement universal exit code translation logic.
|
||||||
- [ ] Implement global flag mapping (Verbosity, Decoration, Interactivity).
|
- [x] Implement global flag mapping (Verbosity, Decoration, Interactivity).
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,19 @@
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"getphred/console-contracts": "dev-master"
|
"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": {
|
"suggest": {
|
||||||
"getphred/tasker": "To use the PhredBridge with the Tasker runner",
|
"getphred/tasker": "To use the PhredBridge with the Tasker runner",
|
||||||
"symfony/console": "To use the SymfonyBridge",
|
"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