Search by

tereta / console

tereta

Building console (CLI) applications on PHP 8.2+ with PSR-3 output and a PSR-11 dependency container.

0.0.11 2026-10-10 15:30 UTC

This package is not auto-updated.

Last update: 2026-10-10 12:31:31 UTC


README

🌐 English | Русский | Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°

Building console (CLI) applications on PHP 8.2+ with PSR-3 output and a PSR-11 dependency container.

Requirements

PHP 8.2+, psr/container ^2.0, psr/log ^3.0.

Installation

composer require tereta/console

Quick start

The entry point is the Tereta\Console\Runtime class - it registers commands and passes $argv for execution:

#!/usr/bin/env php
<?php

declare(strict_types=1);

require_once __DIR__ . '/../vendor/autoload.php';

use Tereta\Console\Runtime;

$runtime = new Runtime();

exit($runtime->execute($argv));

The first argument is the command identifier, the rest is passed to the command. With no arguments the built-in commands:listing runs - a table of the registered commands:

php bin/console
php bin/console commands:listing

Runtime::register - registers a command, Runtime::execute - runs the command named in $argv. The built-in commands:listing is registered automatically, there is no need to register it.

Command

Commands are built on the Tereta\Console\Interfaces\Command interface: with the built-in factory the constructor takes the context (see "Context and dependencies"), execute() takes the arguments and the PSR-3 output. The identifier and the description are declared with class attributes:

use Psr\Log\LoggerInterface;
use Tereta\Console\Attributes\Description as DescriptionAttribute;
use Tereta\Console\Attributes\Identifier as IdentifierAttribute;
use Tereta\Console\Contexts\Runtime as RuntimeContext;
use Tereta\Console\Interfaces\Command;

#[IdentifierAttribute('cache:clean')]
#[DescriptionAttribute('Clears the application cache')]
class Clean implements Command
{
    public function __construct(private RuntimeContext $context)
    {
    }

    public function execute(array $argv, LoggerInterface $output): int
    {
        $output->info('Cache is cleaned.');

        return Command::SUCCESS;
    }
}

execute() returns the exit code of the process: Command::SUCCESS (0), Command::FAILURE (1) or Command::INVALID (2) for wrong arguments; any other number is passed as is. Runtime::execute returns this code, so pass it to exit() - then shell scripts, cron and CI see that the command failed. If the command throws an exception, the code is Command::FAILURE; if the command is not found, the code is Command::INVALID.

Without the Tereta\Console\Attributes\Identifier attribute the identifier is built from the last two namespace segments of the class implementing the Tereta\Console\Interfaces\Command interface: The Tereta\Console\Attributes\Description attribute declares the command description, when it is omitted the command listing shows No description available. Two different classes cannot share one identifier - the second registration throws Tereta\Console\Exceptions\Duplicate. Registering the same class again is allowed.

Context and dependencies

Tereta\Console\Contexts\Runtime - the context holds:

  • $commands - the command container Tereta\Console\Containers\Command,
  • $factory - the command factory, Tereta\Console\Interfaces\Factory,
  • $output - the output, any PSR-3 logger Psr\Log\LoggerInterface,
  • $format - the formatting service, Tereta\Console\Interfaces\Format,
  • $pool - the configuration pool Tereta\Console\Containers\Pool.

All parts are passed as constructor arguments, so any of them can be replaced with your own implementation:

$context = new RuntimeContext(output: $logger);
$runtime = new Runtime($context);

Dependencies of a command

The built-in factory Tereta\Console\Factories\Command creates a command as new Command($context). This is enough for simple commands, but then the command has to take everything from the context itself.

The recommended way is to declare dependencies in the command constructor:

class Clean implements Command
{
    public function __construct(
        private FormatInterface $format,
        private CacheInterface $cache
    ) {
    }
}

Such a command is created by a factory that works with a dependency container. Any PSR-11 container fits, for example tereta/di:

use Psr\Container\ContainerInterface;
use Tereta\Console\Contexts\Runtime as RuntimeContext;
use Tereta\Console\Interfaces\Command;
use Tereta\Console\Interfaces\Factory;

class ContainerFactory implements Factory
{
    public function __construct(private ContainerInterface $container)
    {
    }

    public function create(string $identifier, RuntimeContext $context): Command
    {
        return $this->container->get($context->commands->get($identifier));
    }
}

$runtime = new Runtime(new RuntimeContext(factory: new ContainerFactory($container)));

Type the dependencies with interfaces, not with classes: FormatInterface, LoggerInterface and so on. Then a command is easy to test and does not depend on a specific implementation.

Configuration pool

Tereta\Console\Containers\Pool - a PSR-11 container for configuration values, it is filled during bootstrap:

$runtime->context->pool->set('development', true);

Runtime reads the development flag: when it is on, an exception in a command is shown with its trace. Keep only configuration in the pool. Services such as ORM or PDO should come through the command constructor: the pool returns mixed, so the type and the presence of a value are checked only when the command runs.

Output and formatting

Tereta\Console\Services\Output - a PSR-3 logger implementation: the emergency, alert, critical, error, warning levels are logged to stderr, while notice, info, debug go to stdout. Errors are red, warning is yellow, info is green, debug is gray, notice has no colour. An unknown level throws Psr\Log\InvalidArgumentException, as PSR-3 requires.

Tereta\Console\Services\Format - the formatting service for tables, boxes and the banner. Runtime::execute sets the width from the terminal before running a command, inside a command it can be changed manually:

use Tereta\Console\Enums\Style;

$this->format->setWidth(120);

$output->info($this->format->table($rows));
$output->info($this->format->box('Done', Style::Green));

The Tereta\Console\Interfaces\Format functions:

$format->setWidth(int $width): static
$format->getWidth(): int
$format->decoration(string $message, ?Style $style = null): string
$format->table(array $rows, array $titles = [], ?Style $style = null): string
$format->box(string $text, ?Style $style = null): string
  • setWidth - sets the output width in characters, getWidth - returns the current one, Format::DEFAULT_WIDTH (80) by default.
  • decoration - a banner with the logo, the message is split into at most three lines of 52 characters; below a width of 95 characters it returns just the styled message; the style is green by default.
  • table - a table in a pseudographic frame, $titles overrides the column headers by the array keys, $style styles the whole table; when the table does not fit the width, it is drawn without a frame, columns are separated by two spaces; the frame is drawn with unicode only when LANG/LC_ALL contains utf, otherwise +, -, |.
  • box - text in a frame, multiline text is aligned to the longest line; when the text does not fit the width, it is returned without a frame.

Format is built from small parts, each can be replaced through the constructor: Tereta\Console\Services\Formats\Table, Tereta\Console\Services\Formats\Box, Tereta\Console\Services\Formats\Banner. Tereta\Console\Services\Formats\Text measures the visible length of a text and splits it into lines. Tereta\Console\Services\Terminal returns the terminal width via tput cols, 0 when there is no TERM or no tput utility; Runtime takes it as the second constructor argument.

Styles

Tereta\Console\Enums\Style - ANSI styles:

  • text: Bold, Underline, Reverse, Strikethrough;
  • colour: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Gray and the bright ones BrightRed ... BrightWhite;
  • background: BackgroundBlack ... BackgroundWhite, BackgroundGray and BackgroundBrightRed ... BackgroundBrightWhite.

apply() wraps a text in a style and resets only this style, so styles can be nested:

$output->info('Status: ' . Style::Bold->apply('ready'));

Style::RESET - resets all styles at once.

License and author

Tereta Alexander tereta.alexander@gmail.com Web: https://tereta.dev Copyright Β©2008-2026 Tereta Alexander License https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0

 www.β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
     β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
        β•šβ•β•   β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•   β•šβ•β•   β•šβ•β•  β•šβ•β•
                                                    .dev