# ConsoleContracts: Phred Console Interfaces ConsoleContracts provides a minimal, framework-agnostic set of interfaces for building CLI tools. It is designed to be the foundation for the Tasker ecosystem, allowing library authors to define CLI commands without depending on a heavy runner or a specific framework. ## Core Vision - **Zero Dependencies**: The package must have zero runtime dependencies (other than PHP 8.2+). - **Framework Agnostic**: Interfaces should not assume any specific runner (Tasker, Symfony, etc.). - **Portable Commands**: Commands defined using these interfaces should be runnable in any environment that supports the Phred Console standard. ## Architectural Decisions ### 1. Command Interface The `CommandInterface` is the primary contract for any CLI tool. It separates metadata (name, description, arguments, options) from execution logic. #### CommandInterface ```php namespace GetPhred\ConsoleContracts; interface CommandInterface { /** * The unique name of the command (e.g., 'db:migrate'). */ public function getName(): string; /** * A brief description of what the command does. */ public function getDescription(): string; /** * Returns an array of arguments expected by the command. * @return array Name => Description */ public function getArguments(): array; /** * Returns an array of options available for the command. * @return array Name => Description */ public function getOptions(): array; /** * Executes the command logic. * * @param InputInterface $input * @param OutputInterface $output * @return int Exit code (see ExitCode constants) */ public function execute(InputInterface $input, OutputInterface $output): int; } ``` ### 2. Input & Output Interfaces To ensure portability, commands interact with the environment only through these interfaces. #### InputInterface ```php namespace GetPhred\ConsoleContracts; interface InputInterface { /** * Retrieve the value of a specific argument. */ public function getArgument(string $name, mixed $default = null): mixed; /** * Retrieve the value of a specific option. */ public function getOption(string $name, mixed $default = null): mixed; /** * Check if a specific option was provided. */ public function hasOption(string $name): bool; } ``` #### OutputInterface A lean interface for writing messages to the console. ```php namespace GetPhred\ConsoleContracts; interface OutputInterface { public function write(string $message): void; public function writeln(string $message): void; // Semantic output methods for common use cases public function success(string $message): void; public function error(string $message): void; public function warning(string $message): void; public function info(string $message): void; /** * Set the verbosity level of the output. */ public function setVerbosity(int $level): void; /** * Get the current verbosity level. */ public function getVerbosity(): int; } ``` ### 3. Interactivity & Portability Interactive features (asking questions, confirmation prompts) are moved to an optional `InteractionInterface`. This ensures that a command remains runnable in non-interactive environments (CI/CD) by providing a mock or fallback implementation of this interface. #### InteractionInterface (Proposed) ```php namespace GetPhred\ConsoleContracts; interface InteractionInterface { /** * Ask a simple question and return the answer. */ public function ask(string $question, string $default = null): string; /** * Ask for confirmation (yes/no). */ public function confirm(string $question, bool $default = true): bool; /** * Ask for a sensitive value (input hidden). */ public function secret(string $question): string; /** * Provide a list of choices and return the selected one. */ public function choice(string $question, array $choices, mixed $default = null): mixed; } ``` ### 4. Console Middleware Middleware should be defined at the **contract level**. This allows for cross-cutting concerns (logging, locking, environment guards) to be implemented in a runner-agnostic way. #### ConsoleMiddlewareInterface ```php namespace GetPhred\ConsoleContracts; interface ConsoleMiddlewareInterface { /** * Handle the command execution lifecycle. * * @param CommandInterface $command * @param InputInterface $input * @param OutputInterface $output * @param callable $next The next middleware or the command execution itself. * @return int Exit code */ public function handle( CommandInterface $command, InputInterface $input, OutputInterface $output, callable $next ): int; } ``` ### 5. Constants & Standards #### Verbosity Levels Standardized verbosity levels to control output detail. - `Verbosity::QUIET` (0) - `Verbosity::NORMAL` (1) - `Verbosity::VERBOSE` (2) - `Verbosity::VERY_VERBOSE` (3) - `Verbosity::DEBUG` (4) #### Exit Codes Strict adherence to `sysexits.h` via `ExitCode` constants (defined in Tasker but standardized here). ### 6. Exception Handling A base `ConsoleExceptionInterface` should be defined to allow runners to catch and handle CLI-specific errors gracefully (e.g., CommandNotFound, InvalidArgument). ### 7. Lazy Loading Readiness The `CommandInterface` is specifically designed to support lazy loading. By separating metadata retrieval (`getName()`, `getDescription()`, `getArguments()`, `getOptions()`) from the execution logic (`execute()`), runners can display help information or command lists without instantiating dependencies required only for the command's execution. ### 8. Metadata via Attributes (Optional) To enhance the Developer Experience (DX), PHP 8 attributes are supported for defining command metadata. While the `CommandInterface` remains the primary contract, attributes provide a declarative way to satisfy the interface methods. #### Attributes (Proposed) - `#[Cmd(name: '...', description: '...')]` - `#[Arg(name: '...', description: '...')]` - `#[Opt(name: '...', description: '...')]` ```php use Phred\ConsoleContracts\Attributes\Cmd; use Phred\ConsoleContracts\Attributes\Arg; use Phred\ConsoleContracts\Attributes\Opt; use Phred\ConsoleContracts\CommandInterface; #[Cmd(name: 'db:migrate', description: 'Run database migrations')] #[Arg(name: 'step', description: 'Number of migrations to run')] #[Opt(name: 'force', description: 'Force the operation')] class MigrateCommand implements CommandInterface { use HasAttributes; // A trait that handles the Reflection logic public function execute(InputInterface $input, OutputInterface $output): int { // Logic here... return 0; } } ``` #### Implementation Strategy The `ConsoleContracts` will provide an optional `HasAttributes` trait (or similar utility) that uses Reflection to read these attributes and return the appropriate values through the `getName()`, `getDescription()`, `getArguments()`, and `getOptions()` methods. This approach is: - **Attribute-Aware**: Modern, declarative DX. - **Interface-Driven**: The runner only interacts with the `CommandInterface` methods. - **Optional**: Developers can still override the methods manually without any attributes. ### 9. Output Formatting (Minimal Markup) The `OutputInterface` should support a minimal, standardized set of markup tags to ensure consistent styling across different runners. #### Core Tag Set (Fixed) To ensure consistent behavior across all runners and bridges, the set of supported markup tags is **fixed**. Developers should NOT attempt to use custom tags, as bridges are only required to implement this standardized core set: - `...`: Green text. - `...`: Blue text. - `...`: White text on a red background. - `...`: Black text on a yellow background. - `...`: Yellow text. - `...`: Bold text. - `...`: Italicized text. - `...`: Underlined text. #### Bridge Responsibility Each bridge (e.g., `SymfonyBridge`, `LaminasBridge`) is responsible for translating this fixed set of Phred tags into the native formatting syntax of the underlying CLI tool. - **SymfonyBridge**: Maps `` to Symfony's `` tag in its `Formatter`. - **PhredBridge (Native)**: Translates tags into ANSI escape sequences (e.g., `\e[32m` for green). This ensures that a command's output remains visually consistent whether it's running via Tasker or inside a Symfony application. ### 10. Helper Contracts (Optional) Complex CLI components like **Progress Bars** and **Tables** are defined as separate, optional interfaces. This keeps the core `OutputInterface` lean and allows commands to only depend on the specific features they need. #### ProgressBarInterface (Proposed) Commands can inject this interface to display progress for long-running tasks. - `start(int $max)` - `advance(int $step = 1)` - `finish()` #### TableInterface (Proposed) Commands can inject this interface to render tabular data. - `setHeaders(array $headers)` - `addRow(array $row)` - `render()` #### MarkdownConverterInterface (Proposed) Commands can inject this interface to convert a string or a file containing Markdown into a string formatted with Phred's standardized markup tags. - `convert(string $markdown): string` - `convertFile(string $path): string` ### Implementation Strategy for Helpers Like the `InteractionInterface`, these helpers are satisfied by the runner or bridge. - **SymfonyBridge**: Maps these to Symfony's `ProgressBar` and `Table` helper classes. For Markdown, it could use an existing library or Symfony's native Markdown support. - **Native (Tasker)**: Provides basic ANSI-based implementations and a lightweight regex-based Markdown parser. ## Brainstorming / Open Questions (All current open questions have been resolved and moved to Architectural Decisions.)