ConsoleContracts/NOTES.md
Funky Waddle c06e557620
Some checks are pending
CI / console-contracts (8.2) (push) Waiting to run
CI / console-contracts (8.3) (push) Waiting to run
docs: add project milestones, specs, and notes
2026-02-21 19:13:45 -06:00

9.9 KiB

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

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<string, string> Name => Description
     */
    public function getArguments(): array;
    
    /**
     * Returns an array of options available for the command.
     * @return array<string, string> 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

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.

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)

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

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: '...')]
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:

  • <success>...</success>: Green text.
  • <info>...</info>: Blue text.
  • <error>...</error>: White text on a red background.
  • <warning>...</warning>: Black text on a yellow background.
  • <comment>...</comment>: Yellow text.
  • <b>...</b>: Bold text.
  • <i>...</i>: Italicized text.
  • <u>...</u>: 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 <info> to Symfony's <info> 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.)