diff --git a/.docs/README.md b/.docs/README.md
deleted file mode 100644
index 66dca73..0000000
--- a/.docs/README.md
+++ /dev/null
@@ -1,866 +0,0 @@
-# Contributte Console
-
-Integration of [Symfony Console](https://symfony.com/doc/current/console.html) into Nette Framework.
-
-## Content
-
-- [Getting started](#getting-started)
- - [Setup](#setup)
- - [Configuration](#configuration)
- - [Entrypoint](#entrypoint)
-- [Commands](#commands)
- - [Example command](#example-command)
- - [Invokable commands](#invokable-commands)
-- [UI](#ui)
- - [Styled output](#styled-output)
- - [Cursor control](#cursor-control)
- - [Tree display](#tree-display)
-- [Advanced](#advanced)
- - [Shell completion](#shell-completion)
- - [Signal handling](#signal-handling)
- - [Console events](#console-events)
-- [Testing](#testing)
-
----
-
-# Getting started
-
-## Setup
-
-```bash
-composer require contributte/console
-```
-
-```neon
-extensions:
- console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)
-```
-
-The extension will look for all commands extending from [`Symfony\Component\Console\Command\Command`](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Command/Command.php) and automatically add them to the console application.
-That's all. You don't have to worry about anything else.
-
-## Configuration
-
-```neon
-console:
- name: Acme Project
- version: '1.0'
- catchExceptions: true / false
- autoExit: true / false
- url: https://example.com
-```
-
-In SAPI (CLI) mode, there is no HTTP request and thus no URL address.
-You have to set base URL on your own so that link generator works. Use `console.url` option:
-
-```neon
-console:
- url: https://example.com
-```
-
-### Helpers
-
-You have the option to define your own helperSet if needed. There are two methods to do this. One way is to register your `App\Model\MyCustomHelperSet` as a service in the services section.
-Alternatively, you can directly provide it to the extension configuration helperSet.
-
-```neon
-console:
- # directly
- helperSet: App\Model\MyCustomHelperSet
-
- # or reference service
- helperSet: @customHelperSet
-
-services:
- customHelperSet: App\Model\MyCustomHelperSet
-```
-
-By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add your own helpers to the helperSet.
-
-```neon
-console:
- helpers:
- - App\Model\MyReallyGreatHelper
-```
-
-> See [Console Helpers](https://symfony.com/doc/current/components/console/helpers/index.html) in Symfony docs.
-
-### Lazy-loading
-
-By default, all commands are registered in the console application during the extension registration. This means that all commands are instantiated and their dependencies are injected.
-This can be a problem if you have a lot of commands and you don't need all of them at once. In this case, this extension setup lazy-loading of commands.
-This means that commands are instantiated only when they are needed.
-
-```php
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Attribute\AsCommand;
-
-#[AsCommand(name: 'app:foo')]
-class FooCommand extends Command
-{
-}
-```
-
-Or via a service tag.
-
-```neon
-services:
- commands.foo:
- class: App\FooCommand
- tags: [console.command: app:foo]
- # or
- tags: [console.command: {name: app:foo}]
-```
-
-## Entrypoint
-
-The very last piece of the puzzle is the console entrypoint. It is a simple script that loads the DI container and fires `Contributte\Console\Application::run`.
-
-You can copy & paste it to your project, for example to `/bin/console`.
-
-Make sure to set it as executable. `chmod +x /bin/console`.
-
-```php
-#!/usr/bin/env php
-createContainer()
- ->getByType(Symfony\Component\Console\Application::class)
- ->run());
-```
-
----
-
-# Commands
-
-## Example command
-
-In case of having `console.php` as entrypoint (see above), this would add a user with username `john.doe`:
-
-> `php console.php user:add john.doe`
-
-```php
-namespace App\Console;
-
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Attribute\AsCommand;
-
-#[AsCommand(
- name: 'app:foo',
- description: 'Adds user with given username to database',
-)]
-final class AddUserCommand extends Command
-{
-
- private UserFacade $userFacade;
-
- public function __construct(UserFacade $userFacade)
- {
- parent::__construct();
- $this->userFacade = $usersFacade;
- }
-
- protected function configure(): void
- {
- $this->addArgument('username', InputArgument::REQUIRED, "User's username");
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- // retrieve passed arguments/options
- $username = $input->getArgument('username');
-
- // you can use symfony/console output
- $output->writeln(\sprintf('Adding user %s…', $username));
-
- try {
- // do your logic
- $this->usersModel->add($username);
- // styled output is supported as well
- $output->writeln('✔ Successfully added');
- return 0;
-
- } catch (\Exception $e) {
- // handle error
- $output->writeln(\sprintf(
- '❌ Error occurred: ',
- $e->getMessage(),
- ));
- return 1;
- }
- }
-
-}
-```
-
-Register your command as a service in NEON file.
-
-```neon
-services:
- - App\Console\AddUserCommand
-```
-
-> [!IMPORTANT]
-> Remember! Flush `temp/cache` directory before running the command.
-
-> See [Console Commands](https://symfony.com/doc/current/console.html) in Symfony docs.
-
-## Invokable commands
-
-Since Symfony 6.4, you can use `#[Argument]` and `#[Option]` attributes to define command inputs directly on the `__invoke()` method. This approach reduces boilerplate code significantly.
-
-```php
-namespace App\Console;
-
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Attribute\Argument;
-use Symfony\Component\Console\Attribute\Option;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Style\SymfonyStyle;
-
-#[AsCommand(
- name: 'app:create-user',
- description: 'Creates a new user',
-)]
-final class CreateUserCommand extends Command
-{
-
- public function __construct(
- private UserFacade $userFacade,
- )
- {
- parent::__construct();
- }
-
- public function __invoke(
- SymfonyStyle $io,
- #[Argument(description: 'Username for the new user')]
- string $username,
- #[Argument(description: 'Email address')]
- string $email,
- #[Option(description: 'Grant admin privileges')]
- bool $admin = false,
- #[Option(name: 'send-email', description: 'Send welcome email')]
- bool $sendEmail = true,
- ): int
- {
- $this->userFacade->create($username, $email, $admin);
-
- $io->success(sprintf('User "%s" created successfully!', $username));
-
- if ($sendEmail) {
- $io->note('Welcome email has been sent.');
- }
-
- return Command::SUCCESS;
- }
-
-}
-```
-
-Usage:
-
-```bash
-php bin/console app:create-user john john@example.com --admin --no-send-email
-```
-
-The `#[Argument]` and `#[Option]` attributes support these parameters:
-
-- `name` - Override the argument/option name (defaults to parameter name)
-- `description` - Help text shown in `--help`
-- `mode` - For arguments: `REQUIRED`, `OPTIONAL`, `IS_ARRAY`
-- `shortcut` - For options: single letter shortcut (e.g., `-a` for `--admin`)
-- `default` - Default value (can also use PHP default parameter value)
-
-> See [Console Input](https://symfony.com/doc/current/console/input.html) in Symfony docs.
-
----
-
-# UI
-
-## Styled output
-
-`SymfonyStyle` provides a consistent, beautiful output formatting API. It reduces boilerplate and ensures your commands have a professional look.
-
-```php
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Style\SymfonyStyle;
-
-#[AsCommand(name: 'app:demo')]
-final class DemoCommand extends Command
-{
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $io = new SymfonyStyle($input, $output);
-
- // Titles and sections
- $io->title('Application Setup');
- $io->section('Step 1: Configuration');
-
- // Text output
- $io->text('Processing configuration files...');
- $io->text(['Line 1', 'Line 2', 'Line 3']);
-
- // Lists
- $io->listing(['First item', 'Second item', 'Third item']);
-
- // Tables
- $io->table(
- ['Name', 'Email', 'Role'],
- [
- ['Alice', 'alice@example.com', 'Admin'],
- ['Bob', 'bob@example.com', 'User'],
- ]
- );
-
- // Admonition blocks
- $io->note('This is additional information.');
- $io->caution('Be careful with this operation!');
- $io->warning('This action cannot be undone.');
-
- // Result blocks
- $io->success('All tasks completed successfully!');
- $io->error('Something went wrong.');
- $io->info('Operation finished.');
-
- return Command::SUCCESS;
- }
-
-}
-```
-
-### Interactive prompts
-
-`SymfonyStyle` also simplifies user interaction:
-
-```php
-// Simple question
-$name = $io->ask('What is your name?', 'Anonymous');
-
-// Hidden input (for passwords)
-$password = $io->askHidden('Enter password');
-
-// Confirmation
-if ($io->confirm('Do you want to continue?', true)) {
- // ...
-}
-
-// Choice selection
-$color = $io->choice('Select a color', ['red', 'green', 'blue'], 'blue');
-```
-
-### Progress bars
-
-```php
-$io->progressStart(100);
-
-for ($i = 0; $i < 100; $i++) {
- // Process item...
- $io->progressAdvance();
-}
-
-$io->progressFinish();
-```
-
-> See [How to Style a Console Command](https://symfony.com/doc/current/console/style.html) and [Progress Bar](https://symfony.com/doc/current/components/console/helpers/progressbar.html) in Symfony docs.
-
-## Cursor control
-
-The `Cursor` class allows direct manipulation of the terminal cursor position. This is useful for building interactive TUIs, dashboards, or real-time displays.
-
-```php
-use Symfony\Component\Console\Cursor;
-use Symfony\Component\Console\Output\OutputInterface;
-
-$cursor = new Cursor($output);
-
-// Movement
-$cursor->moveUp(2); // Move 2 lines up
-$cursor->moveDown(1); // Move 1 line down
-$cursor->moveLeft(5); // Move 5 columns left
-$cursor->moveRight(3); // Move 3 columns right
-$cursor->moveToPosition(10, 5); // Move to column 10, row 5
-
-// Visibility
-$cursor->hide(); // Hide cursor
-$cursor->show(); // Show cursor
-
-// Save/restore position
-$cursor->savePosition(); // Save current position
-// ... do something ...
-$cursor->restorePosition(); // Return to saved position
-
-// Clearing
-$cursor->clearLine(); // Clear entire current line
-$cursor->clearLineAfter(); // Clear from cursor to end of line
-$cursor->clearOutput(); // Clear from cursor to end of screen
-$cursor->clearScreen(); // Clear entire screen
-
-// Get current position (returns [column, row])
-[$column, $row] = $cursor->getCurrentPosition();
-```
-
-### Example: Real-time status display
-
-```php
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Cursor;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-#[AsCommand(name: 'app:monitor')]
-final class MonitorCommand extends Command
-{
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $cursor = new Cursor($output);
- $cursor->hide();
-
- $output->writeln('System Monitor');
- $output->writeln('==============');
- $output->writeln('CPU: ');
- $output->writeln('Memory: ');
- $output->writeln('');
- $output->writeln('Press Ctrl+C to stop');
-
- for ($i = 0; $i < 100; $i++) {
- $cursor->moveToPosition(8, 3);
- $output->write(sprintf('%3d%%', rand(0, 100)));
-
- $cursor->moveToPosition(8, 4);
- $output->write(sprintf('%3d%%', rand(0, 100)));
-
- usleep(500000);
- }
-
- $cursor->show();
-
- return Command::SUCCESS;
- }
-
-}
-```
-
-> See [Cursor Helper](https://symfony.com/doc/current/components/console/helpers/cursor.html) in Symfony docs.
-
-## Tree display
-
-The `TreeHelper` (Symfony 7.3+) renders hierarchical data as ASCII trees, useful for displaying file structures, dependency trees, or any nested data.
-
-```php
-use Symfony\Component\Console\Helper\TreeHelper;
-use Symfony\Component\Console\Helper\TreeNode;
-use Symfony\Component\Console\Output\OutputInterface;
-
-// Create root node
-$root = new TreeNode('src/');
-
-// Add children
-$root->addChild(new TreeNode('Console/'))
- ->addChild(new TreeNode('Command/'))
- ->addChild(new TreeNode('CreateUserCommand.php'))
- ->addChild(new TreeNode('ImportCommand.php'));
-
-$root->addChild(new TreeNode('Model/'))
- ->addChild(new TreeNode('User.php'))
- ->addChild(new TreeNode('UserFacade.php'));
-
-$root->addChild(new TreeNode('bootstrap.php'));
-
-// Render
-TreeHelper::render($output, $root);
-```
-
-Output:
-```
-src/
-├── Console/
-│ └── Command/
-│ ├── CreateUserCommand.php
-│ └── ImportCommand.php
-├── Model/
-│ ├── User.php
-│ └── UserFacade.php
-└── bootstrap.php
-```
-
-### Building trees from arrays
-
-```php
-function buildTree(array $items, TreeNode $parent): void
-{
- foreach ($items as $key => $value) {
- if (is_array($value)) {
- $node = new TreeNode($key . '/');
- $parent->addChild($node);
- buildTree($value, $node);
- } else {
- $parent->addChild(new TreeNode($value));
- }
- }
-}
-
-$structure = [
- 'app' => [
- 'Commands' => ['FooCommand.php', 'BarCommand.php'],
- 'Models' => ['User.php'],
- ],
- 'config' => ['app.neon', 'services.neon'],
-];
-
-$root = new TreeNode('project/');
-buildTree($structure, $root);
-TreeHelper::render($output, $root);
-```
-
-> See [Tree Helper](https://symfony.com/doc/current/components/console/helpers/tree.html) in Symfony docs.
-
----
-
-# Advanced
-
-## Shell completion
-
-Symfony Console provides built-in shell completion for Bash, Zsh, and Fish shells. This allows tab-completion of command names, options, and even argument values.
-
-### Installation
-
-Run the completion command with your shell name to get installation instructions:
-
-```bash
-# For Bash
-php bin/console completion bash
-
-# For Zsh
-php bin/console completion zsh
-
-# For Fish
-php bin/console completion fish
-```
-
-Each shell has specific setup requirements:
-
-**Bash** - Install the `bash-completion` package first:
-```bash
-# Debian/Ubuntu
-apt install bash-completion
-
-# macOS with Homebrew
-brew install bash-completion
-```
-
-**Zsh** - Usually works out of the box with Oh My Zsh or similar frameworks.
-
-**Fish** - Automatically discovers completions in `~/.config/fish/completions/`.
-
-### Custom completion values
-
-You can provide custom completion suggestions for your command arguments and options by implementing the `complete()` method:
-
-```php
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
-
-#[AsCommand(name: 'app:greet')]
-final class GreetCommand extends Command
-{
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('name')) {
- $suggestions->suggestValues(['Alice', 'Bob', 'Charlie']);
- }
-
- if ($input->mustSuggestOptionValuesFor('format')) {
- $suggestions->suggestValues(['json', 'xml', 'csv']);
- }
- }
-
-}
-```
-
-Now pressing Tab after the command will suggest `Alice`, `Bob`, or `Charlie` for the `name` argument.
-
-> See [How to Add Console Command Completion](https://symfony.com/doc/current/console/completion.html) in Symfony docs.
-
-## Signal handling
-
-For long-running commands (workers, daemons, queue consumers), you may need to handle OS signals like `SIGINT` (Ctrl+C) or `SIGTERM` for graceful shutdown. Implement `SignalableCommandInterface` to subscribe to signals:
-
-```php
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Command\SignalableCommandInterface;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-#[AsCommand(name: 'app:worker')]
-final class WorkerCommand extends Command implements SignalableCommandInterface
-{
-
- private bool $shouldStop = false;
-
- public function getSubscribedSignals(): array
- {
- return [SIGINT, SIGTERM];
- }
-
- public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
- {
- $this->shouldStop = true;
-
- // Return false to continue execution (graceful shutdown)
- // Return an integer to exit immediately with that code
- return false;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $output->writeln('Worker started. Press Ctrl+C to stop gracefully.');
-
- while (!$this->shouldStop) {
- // Process jobs from queue
- $this->processNextJob();
-
- // Small sleep to prevent CPU spinning
- usleep(100000);
- }
-
- $output->writeln('Shutting down gracefully...');
- $this->cleanup();
-
- return Command::SUCCESS;
- }
-
-}
-```
-
-Common signals:
-- `SIGINT` - Interrupt (Ctrl+C)
-- `SIGTERM` - Termination request (default kill signal)
-- `SIGQUIT` - Quit with core dump (Ctrl+\)
-- `SIGUSR1`, `SIGUSR2` - User-defined signals
-
-> [!NOTE]
-> Signal handling requires the `pcntl` PHP extension to be installed.
-
-> See [Console Signals](https://symfony.com/doc/current/components/console/events.html#console-events-signal) in Symfony docs.
-
-## Console events
-
-Symfony Console dispatches events during command execution. You can use these events for logging, profiling, error handling, and more. This extension automatically registers the EventDispatcher if available in the container.
-
-### Available events
-
-| Event | When dispatched |
-|-------|-----------------|
-| `ConsoleEvents::COMMAND` | Before command execution |
-| `ConsoleEvents::TERMINATE` | After command execution (including exceptions) |
-| `ConsoleEvents::ERROR` | When an exception is thrown |
-| `ConsoleEvents::SIGNAL` | When a signal is received |
-
-### Setup with Nette
-
-First, install the Symfony EventDispatcher:
-
-```bash
-composer require symfony/event-dispatcher
-```
-
-Register it as a service:
-
-```neon
-services:
- eventDispatcher:
- class: Symfony\Component\EventDispatcher\EventDispatcher
-```
-
-The console extension will automatically detect and use it.
-
-### Creating event subscribers
-
-```php
-use Symfony\Component\Console\ConsoleEvents;
-use Symfony\Component\Console\Event\ConsoleCommandEvent;
-use Symfony\Component\Console\Event\ConsoleErrorEvent;
-use Symfony\Component\Console\Event\ConsoleTerminateEvent;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-
-final class ConsoleEventSubscriber implements EventSubscriberInterface
-{
-
- public function __construct(
- private Logger $logger,
- )
- {
- }
-
- public static function getSubscribedEvents(): array
- {
- return [
- ConsoleEvents::COMMAND => 'onCommand',
- ConsoleEvents::TERMINATE => 'onTerminate',
- ConsoleEvents::ERROR => 'onError',
- ];
- }
-
- public function onCommand(ConsoleCommandEvent $event): void
- {
- $command = $event->getCommand();
- $this->logger->info('Executing command: ' . $command?->getName());
- }
-
- public function onTerminate(ConsoleTerminateEvent $event): void
- {
- $exitCode = $event->getExitCode();
- $this->logger->info('Command finished with exit code: ' . $exitCode);
- }
-
- public function onError(ConsoleErrorEvent $event): void
- {
- $error = $event->getError();
- $this->logger->error('Command error: ' . $error->getMessage());
-
- // Optionally change the exit code
- $event->setExitCode(1);
- }
-
-}
-```
-
-Register the subscriber:
-
-```neon
-services:
- - App\Console\ConsoleEventSubscriber
-
- eventDispatcher:
- class: Symfony\Component\EventDispatcher\EventDispatcher
- setup:
- - addSubscriber(@App\Console\ConsoleEventSubscriber)
-```
-
-> See [Using Console Events](https://symfony.com/doc/current/components/console/events.html) in Symfony docs.
-
----
-
-# Testing
-
-Symfony Console provides `CommandTester` and `ApplicationTester` for testing commands without executing them in a real terminal.
-
-## Testing a single command
-
-```php
-use App\Console\CreateUserCommand;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Tester\CommandTester;
-
-final class CreateUserCommandTest extends TestCase
-{
-
- public function testExecute(): void
- {
- $command = new CreateUserCommand(/* dependencies */);
- $tester = new CommandTester($command);
-
- $tester->execute([
- 'username' => 'john',
- '--admin' => true,
- ]);
-
- // Assert exit code
- $tester->assertCommandIsSuccessful();
- // or
- $this->assertSame(0, $tester->getStatusCode());
-
- // Assert output contains expected text
- $output = $tester->getDisplay();
- $this->assertStringContainsString('User "john" created', $output);
- }
-
-}
-```
-
-## Testing with Nette DI
-
-For commands with dependencies, use Nette's container:
-
-```php
-use Contributte\Tester\Utils\ContainerBuilder;
-use Symfony\Component\Console\Application;
-use Symfony\Component\Console\Tester\ApplicationTester;
-use Tester\Assert;
-use Tester\TestCase;
-
-final class CreateUserCommandTest extends TestCase
-{
-
- public function testCommand(): void
- {
- $container = ContainerBuilder::of()
- ->withCompiler(function ($compiler) {
- $compiler->addConfig(__DIR__ . '/config.neon');
- })
- ->build();
-
- $application = $container->getByType(Application::class);
- $application->setAutoExit(false);
-
- $tester = new ApplicationTester($application);
- $tester->run(['command' => 'app:create-user', 'username' => 'john']);
-
- Assert::same(0, $tester->getStatusCode());
- Assert::contains('User "john" created', $tester->getDisplay());
- }
-
-}
-```
-
-## Testing interactive commands
-
-For commands with user prompts, use `setInputs()`:
-
-```php
-$tester = new CommandTester($command);
-
-// Simulate user typing "yes" then "john@example.com"
-$tester->setInputs(['yes', 'john@example.com']);
-
-$tester->execute(['username' => 'john']);
-```
-
-## Useful assertions
-
-```php
-// Check exit code
-$tester->assertCommandIsSuccessful();
-
-// Get output
-$output = $tester->getDisplay();
-$output = $tester->getDisplay(true); // Normalized (no decorations)
-
-// Get error output (stderr)
-$errorOutput = $tester->getErrorOutput();
-
-// Get status code
-$exitCode = $tester->getStatusCode();
-
-// Get input used
-$input = $tester->getInput();
-```
-
-> See [How to Test Commands](https://symfony.com/doc/current/console.html#testing-commands) in Symfony docs.
diff --git a/README.md b/README.md
index 0d7101e..9495970 100644
--- a/README.md
+++ b/README.md
@@ -19,28 +19,882 @@ Website 🚀 contributte.org | Contact
-
+
-## Usage
+Best Symfony Console for Nette Framework.
-To install latest version of `contributte/console` use [Composer](https://getcomposer.org).
+## Versions
+
+| State | Version | Branch | Nette | PHP |
+|--------|-----------|----------|--------|---------|
+| dev | `^0.12.0` | `master` | `3.2+` | `>=8.2` |
+| stable | `^0.11.0` | `master` | `3.2+` | `>=8.2` |
+
+Integration of [Symfony Console](https://symfony.com/doc/current/console.html) into Nette Framework.
+
+## Contents
+
+- [Getting started](#getting-started)
+ - [Installation](#installation)
+ - [Configuration](#configuration)
+ - [Entrypoint](#entrypoint)
+- [Commands](#commands)
+ - [Example command](#example-command)
+ - [Invokable commands](#invokable-commands)
+- [UI](#ui)
+ - [Styled output](#styled-output)
+ - [Cursor control](#cursor-control)
+ - [Tree display](#tree-display)
+- [Advanced](#advanced)
+ - [Shell completion](#shell-completion)
+ - [Signal handling](#signal-handling)
+ - [Console events](#console-events)
+- [Testing](#testing)
+
+---
+
+## Getting started
+
+## Installation
```bash
composer require contributte/console
```
-## Documentation
+```neon
+extensions:
+ console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)
+```
-For details on how to use this package, check out our [documentation](.docs).
+The extension will look for all commands extending from [`Symfony\Component\Console\Command\Command`](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Command/Command.php) and automatically add them to the console application.
+That's all. You don't have to worry about anything else.
-## Versions
+## Configuration
-| State | Version | Branch | Nette | PHP |
-|--------|-----------|----------|--------|---------|
-| dev | `^0.12.0` | `master` | `3.2+` | `>=8.2` |
-| stable | `^0.11.0` | `master` | `3.2+` | `>=8.2` |
+```neon
+console:
+ name: Acme Project
+ version: '1.0'
+ catchExceptions: true / false
+ autoExit: true / false
+ url: https://example.com
+```
+
+In SAPI (CLI) mode, there is no HTTP request and thus no URL address.
+You have to set base URL on your own so that link generator works. Use `console.url` option:
+
+```neon
+console:
+ url: https://example.com
+```
+
+### Helpers
+
+You have the option to define your own helperSet if needed. There are two methods to do this. One way is to register your `App\Model\MyCustomHelperSet` as a service in the services section.
+Alternatively, you can directly provide it to the extension configuration helperSet.
+
+```neon
+console:
+ # directly
+ helperSet: App\Model\MyCustomHelperSet
+
+ # or reference service
+ helperSet: @customHelperSet
+
+services:
+ customHelperSet: App\Model\MyCustomHelperSet
+```
+
+By default, helperSet contains 4 helpers defined in `Symfony\Component\Console\Application`. You can add your own helpers to the helperSet.
+
+```neon
+console:
+ helpers:
+ - App\Model\MyReallyGreatHelper
+```
+
+> See [Console Helpers](https://symfony.com/doc/current/components/console/helpers/index.html) in Symfony docs.
+
+### Lazy-loading
+
+By default, all commands are registered in the console application during the extension registration. This means that all commands are instantiated and their dependencies are injected.
+This can be a problem if you have a lot of commands and you don't need all of them at once. In this case, this extension setup lazy-loading of commands.
+This means that commands are instantiated only when they are needed.
+
+```php
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Attribute\AsCommand;
+
+#[AsCommand(name: 'app:foo')]
+class FooCommand extends Command
+{
+}
+```
+
+Or via a service tag.
+
+```neon
+services:
+ commands.foo:
+ class: App\FooCommand
+ tags: [console.command: app:foo]
+ # or
+ tags: [console.command: {name: app:foo}]
+```
+
+## Entrypoint
+
+The very last piece of the puzzle is the console entrypoint. It is a simple script that loads the DI container and fires `Contributte\Console\Application::run`.
+
+You can copy & paste it to your project, for example to `/bin/console`.
+
+Make sure to set it as executable. `chmod +x /bin/console`.
+
+```php
+#!/usr/bin/env php
+createContainer()
+ ->getByType(Symfony\Component\Console\Application::class)
+ ->run());
+```
+
+---
+
+## Commands
+
+## Example command
+
+In case of having `console.php` as entrypoint (see above), this would add a user with username `john.doe`:
+
+> `php console.php user:add john.doe`
+
+```php
+namespace App\Console;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+
+#[AsCommand(
+ name: 'app:foo',
+ description: 'Adds user with given username to database',
+)]
+final class AddUserCommand extends Command
+{
+
+ private UserFacade $userFacade;
+
+ public function __construct(UserFacade $userFacade)
+ {
+ parent::__construct();
+ $this->userFacade = $userFacade;
+ }
+
+ protected function configure(): void
+ {
+ $this->addArgument('username', InputArgument::REQUIRED, "User's username");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ // retrieve passed arguments/options
+ $username = $input->getArgument('username');
+
+ // you can use symfony/console output
+ $output->writeln(\sprintf('Adding user %s…', $username));
+
+ try {
+ // do your logic
+ $this->userFacade->add($username);
+ // styled output is supported as well
+ $output->writeln('✔ Successfully added');
+ return 0;
+
+ } catch (\Exception $e) {
+ // handle error
+ $output->writeln(\sprintf(
+ '❌ Error occurred: ',
+ $e->getMessage(),
+ ));
+ return 1;
+ }
+ }
+
+}
+```
+
+Register your command as a service in NEON file.
+
+```neon
+services:
+ - App\Console\AddUserCommand
+```
+
+> [!IMPORTANT]
+> Remember! Flush `temp/cache` directory before running the command.
+
+> See [Console Commands](https://symfony.com/doc/current/console.html) in Symfony docs.
+
+## Invokable commands
+
+Since Symfony 6.4, you can use `#[Argument]` and `#[Option]` attributes to define command inputs directly on the `__invoke()` method. This approach reduces boilerplate code significantly.
+
+```php
+namespace App\Console;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Attribute\Argument;
+use Symfony\Component\Console\Attribute\Option;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+#[AsCommand(
+ name: 'app:create-user',
+ description: 'Creates a new user',
+)]
+final class CreateUserCommand extends Command
+{
+
+ public function __construct(
+ private UserFacade $userFacade,
+ )
+ {
+ parent::__construct();
+ }
+
+ public function __invoke(
+ SymfonyStyle $io,
+ #[Argument(description: 'Username for the new user')]
+ string $username,
+ #[Argument(description: 'Email address')]
+ string $email,
+ #[Option(description: 'Grant admin privileges')]
+ bool $admin = false,
+ #[Option(name: 'send-email', description: 'Send welcome email')]
+ bool $sendEmail = true,
+ ): int
+ {
+ $this->userFacade->create($username, $email, $admin);
+
+ $io->success(sprintf('User "%s" created successfully!', $username));
+
+ if ($sendEmail) {
+ $io->note('Welcome email has been sent.');
+ }
+
+ return Command::SUCCESS;
+ }
+
+}
+```
+
+Usage:
+
+```bash
+php bin/console app:create-user john john@example.com --admin --no-send-email
+```
+
+The `#[Argument]` and `#[Option]` attributes support these parameters:
+
+- `name` - Override the argument/option name (defaults to parameter name)
+- `description` - Help text shown in `--help`
+- `mode` - For arguments: `REQUIRED`, `OPTIONAL`, `IS_ARRAY`
+- `shortcut` - For options: single letter shortcut (e.g., `-a` for `--admin`)
+- `default` - Default value (can also use PHP default parameter value)
+
+> See [Console Input](https://symfony.com/doc/current/console/input.html) in Symfony docs.
+
+---
+
+## UI
+
+## Styled output
+
+`SymfonyStyle` provides a consistent, beautiful output formatting API. It reduces boilerplate and ensures your commands have a professional look.
+
+```php
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+#[AsCommand(name: 'app:demo')]
+final class DemoCommand extends Command
+{
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ // Titles and sections
+ $io->title('Application Setup');
+ $io->section('Step 1: Configuration');
+
+ // Text output
+ $io->text('Processing configuration files...');
+ $io->text(['Line 1', 'Line 2', 'Line 3']);
+
+ // Lists
+ $io->listing(['First item', 'Second item', 'Third item']);
+
+ // Tables
+ $io->table(
+ ['Name', 'Email', 'Role'],
+ [
+ ['Alice', 'alice@example.com', 'Admin'],
+ ['Bob', 'bob@example.com', 'User'],
+ ]
+ );
+
+ // Admonition blocks
+ $io->note('This is additional information.');
+ $io->caution('Be careful with this operation!');
+ $io->warning('This action cannot be undone.');
+
+ // Result blocks
+ $io->success('All tasks completed successfully!');
+ $io->error('Something went wrong.');
+ $io->info('Operation finished.');
+
+ return Command::SUCCESS;
+ }
+
+}
+```
+
+### Interactive prompts
+
+`SymfonyStyle` also simplifies user interaction:
+
+```php
+// Simple question
+$name = $io->ask('What is your name?', 'Anonymous');
+
+// Hidden input (for passwords)
+$password = $io->askHidden('Enter password');
+
+// Confirmation
+if ($io->confirm('Do you want to continue?', true)) {
+ // ...
+}
+
+// Choice selection
+$color = $io->choice('Select a color', ['red', 'green', 'blue'], 'blue');
+```
+
+### Progress bars
+
+```php
+$io->progressStart(100);
+
+for ($i = 0; $i < 100; $i++) {
+ // Process item...
+ $io->progressAdvance();
+}
+
+$io->progressFinish();
+```
+
+> See [How to Style a Console Command](https://symfony.com/doc/current/console/style.html) and [Progress Bar](https://symfony.com/doc/current/components/console/helpers/progressbar.html) in Symfony docs.
+
+## Cursor control
+
+The `Cursor` class allows direct manipulation of the terminal cursor position. This is useful for building interactive TUIs, dashboards, or real-time displays.
+
+```php
+use Symfony\Component\Console\Cursor;
+use Symfony\Component\Console\Output\OutputInterface;
+
+$cursor = new Cursor($output);
+
+// Movement
+$cursor->moveUp(2); // Move 2 lines up
+$cursor->moveDown(1); // Move 1 line down
+$cursor->moveLeft(5); // Move 5 columns left
+$cursor->moveRight(3); // Move 3 columns right
+$cursor->moveToPosition(10, 5); // Move to column 10, row 5
+
+// Visibility
+$cursor->hide(); // Hide cursor
+$cursor->show(); // Show cursor
+
+// Save/restore position
+$cursor->savePosition(); // Save current position
+// ... do something ...
+$cursor->restorePosition(); // Return to saved position
+
+// Clearing
+$cursor->clearLine(); // Clear entire current line
+$cursor->clearLineAfter(); // Clear from cursor to end of line
+$cursor->clearOutput(); // Clear from cursor to end of screen
+$cursor->clearScreen(); // Clear entire screen
+
+// Get current position (returns [column, row])
+[$column, $row] = $cursor->getCurrentPosition();
+```
+
+### Example: Real-time status display
+
+```php
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Cursor;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(name: 'app:monitor')]
+final class MonitorCommand extends Command
+{
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $cursor = new Cursor($output);
+ $cursor->hide();
+
+ $output->writeln('System Monitor');
+ $output->writeln('==============');
+ $output->writeln('CPU: ');
+ $output->writeln('Memory: ');
+ $output->writeln('');
+ $output->writeln('Press Ctrl+C to stop');
+
+ for ($i = 0; $i < 100; $i++) {
+ $cursor->moveToPosition(8, 3);
+ $output->write(sprintf('%3d%%', rand(0, 100)));
+
+ $cursor->moveToPosition(8, 4);
+ $output->write(sprintf('%3d%%', rand(0, 100)));
+
+ usleep(500000);
+ }
+
+ $cursor->show();
+
+ return Command::SUCCESS;
+ }
+
+}
+```
+
+> See [Cursor Helper](https://symfony.com/doc/current/components/console/helpers/cursor.html) in Symfony docs.
+
+## Tree display
+
+The `TreeHelper` (Symfony 7.3+) renders hierarchical data as ASCII trees, useful for displaying file structures, dependency trees, or any nested data.
+
+```php
+use Symfony\Component\Console\Helper\TreeHelper;
+use Symfony\Component\Console\Helper\TreeNode;
+use Symfony\Component\Console\Output\OutputInterface;
+
+// Create root node
+$root = new TreeNode('src/');
+
+// Add children
+$root->addChild(new TreeNode('Console/'))
+ ->addChild(new TreeNode('Command/'))
+ ->addChild(new TreeNode('CreateUserCommand.php'))
+ ->addChild(new TreeNode('ImportCommand.php'));
+
+$root->addChild(new TreeNode('Model/'))
+ ->addChild(new TreeNode('User.php'))
+ ->addChild(new TreeNode('UserFacade.php'));
+
+$root->addChild(new TreeNode('bootstrap.php'));
+
+// Render
+TreeHelper::render($output, $root);
+```
+
+Output:
+```
+src/
+├── Console/
+│ └── Command/
+│ ├── CreateUserCommand.php
+│ └── ImportCommand.php
+├── Model/
+│ ├── User.php
+│ └── UserFacade.php
+└── bootstrap.php
+```
+
+### Building trees from arrays
+
+```php
+function buildTree(array $items, TreeNode $parent): void
+{
+ foreach ($items as $key => $value) {
+ if (is_array($value)) {
+ $node = new TreeNode($key . '/');
+ $parent->addChild($node);
+ buildTree($value, $node);
+ } else {
+ $parent->addChild(new TreeNode($value));
+ }
+ }
+}
+
+$structure = [
+ 'app' => [
+ 'Commands' => ['FooCommand.php', 'BarCommand.php'],
+ 'Models' => ['User.php'],
+ ],
+ 'config' => ['app.neon', 'services.neon'],
+];
+
+$root = new TreeNode('project/');
+buildTree($structure, $root);
+TreeHelper::render($output, $root);
+```
+
+> See [Tree Helper](https://symfony.com/doc/current/components/console/helpers/tree.html) in Symfony docs.
+
+---
+
+## Advanced
+
+## Shell completion
+
+Symfony Console provides built-in shell completion for Bash, Zsh, and Fish shells. This allows tab-completion of command names, options, and even argument values.
+
+### Completion installation
+
+Run the completion command with your shell name to get installation instructions:
+
+```bash
+# For Bash
+php bin/console completion bash
+
+# For Zsh
+php bin/console completion zsh
+
+# For Fish
+php bin/console completion fish
+```
+
+Each shell has specific setup requirements:
+
+**Bash** - Install the `bash-completion` package first:
+```bash
+# Debian/Ubuntu
+apt install bash-completion
+
+# macOS with Homebrew
+brew install bash-completion
+```
+
+**Zsh** - Usually works out of the box with Oh My Zsh or similar frameworks.
+
+**Fish** - Automatically discovers completions in `~/.config/fish/completions/`.
+
+### Custom completion values
+
+You can provide custom completion suggestions for your command arguments and options by implementing the `complete()` method:
+
+```php
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+
+#[AsCommand(name: 'app:greet')]
+final class GreetCommand extends Command
+{
+
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ if ($input->mustSuggestArgumentValuesFor('name')) {
+ $suggestions->suggestValues(['Alice', 'Bob', 'Charlie']);
+ }
+
+ if ($input->mustSuggestOptionValuesFor('format')) {
+ $suggestions->suggestValues(['json', 'xml', 'csv']);
+ }
+ }
+
+}
+```
+
+Now pressing Tab after the command will suggest `Alice`, `Bob`, or `Charlie` for the `name` argument.
+
+> See [How to Add Console Command Completion](https://symfony.com/doc/current/console/completion.html) in Symfony docs.
+
+## Signal handling
+
+For long-running commands (workers, daemons, queue consumers), you may need to handle OS signals like `SIGINT` (Ctrl+C) or `SIGTERM` for graceful shutdown. Implement `SignalableCommandInterface` to subscribe to signals:
+
+```php
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Command\SignalableCommandInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(name: 'app:worker')]
+final class WorkerCommand extends Command implements SignalableCommandInterface
+{
+
+ private bool $shouldStop = false;
+
+ public function getSubscribedSignals(): array
+ {
+ return [SIGINT, SIGTERM];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ $this->shouldStop = true;
+
+ // Return false to continue execution (graceful shutdown)
+ // Return an integer to exit immediately with that code
+ return false;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->writeln('Worker started. Press Ctrl+C to stop gracefully.');
+
+ while (!$this->shouldStop) {
+ // Process jobs from queue
+ $this->processNextJob();
+
+ // Small sleep to prevent CPU spinning
+ usleep(100000);
+ }
+
+ $output->writeln('Shutting down gracefully...');
+ $this->cleanup();
+
+ return Command::SUCCESS;
+ }
+
+}
+```
+
+Common signals:
+- `SIGINT` - Interrupt (Ctrl+C)
+- `SIGTERM` - Termination request (default kill signal)
+- `SIGQUIT` - Quit with core dump (Ctrl+\)
+- `SIGUSR1`, `SIGUSR2` - User-defined signals
+
+> [!NOTE]
+> Signal handling requires the `pcntl` PHP extension to be installed.
+
+> See [Console Signals](https://symfony.com/doc/current/components/console/events.html#console-events-signal) in Symfony docs.
+
+## Console events
+
+Symfony Console dispatches events during command execution. You can use these events for logging, profiling, error handling, and more. This extension automatically registers the EventDispatcher if available in the container.
+
+### Available events
+
+| Event | When dispatched |
+|-------|-----------------|
+| `ConsoleEvents::COMMAND` | Before command execution |
+| `ConsoleEvents::TERMINATE` | After command execution (including exceptions) |
+| `ConsoleEvents::ERROR` | When an exception is thrown |
+| `ConsoleEvents::SIGNAL` | When a signal is received |
+
+### Setup with Nette
+
+First, install the Symfony EventDispatcher:
+
+```bash
+composer require symfony/event-dispatcher
+```
+
+Register it as a service:
+
+```neon
+services:
+ eventDispatcher:
+ class: Symfony\Component\EventDispatcher\EventDispatcher
+```
+
+The console extension will automatically detect and use it.
+
+### Creating event subscribers
+
+```php
+use Symfony\Component\Console\ConsoleEvents;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleErrorEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+final class ConsoleEventSubscriber implements EventSubscriberInterface
+{
+
+ public function __construct(
+ private Logger $logger,
+ )
+ {
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ ConsoleEvents::COMMAND => 'onCommand',
+ ConsoleEvents::TERMINATE => 'onTerminate',
+ ConsoleEvents::ERROR => 'onError',
+ ];
+ }
+
+ public function onCommand(ConsoleCommandEvent $event): void
+ {
+ $command = $event->getCommand();
+ $this->logger->info('Executing command: ' . $command?->getName());
+ }
+
+ public function onTerminate(ConsoleTerminateEvent $event): void
+ {
+ $exitCode = $event->getExitCode();
+ $this->logger->info('Command finished with exit code: ' . $exitCode);
+ }
+
+ public function onError(ConsoleErrorEvent $event): void
+ {
+ $error = $event->getError();
+ $this->logger->error('Command error: ' . $error->getMessage());
+
+ // Optionally change the exit code
+ $event->setExitCode(1);
+ }
+
+}
+```
+
+Register the subscriber:
+
+```neon
+services:
+ - App\Console\ConsoleEventSubscriber
+
+ eventDispatcher:
+ class: Symfony\Component\EventDispatcher\EventDispatcher
+ setup:
+ - addSubscriber(@App\Console\ConsoleEventSubscriber)
+```
+
+> See [Using Console Events](https://symfony.com/doc/current/components/console/events.html) in Symfony docs.
+
+---
+
+## Testing
+
+Symfony Console provides `CommandTester` and `ApplicationTester` for testing commands without executing them in a real terminal.
+
+## Testing a single command
+
+```php
+use App\Console\CreateUserCommand;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+final class CreateUserCommandTest extends TestCase
+{
+
+ public function testExecute(): void
+ {
+ $command = new CreateUserCommand(/* dependencies */);
+ $tester = new CommandTester($command);
+
+ $tester->execute([
+ 'username' => 'john',
+ '--admin' => true,
+ ]);
+
+ // Assert exit code
+ $tester->assertCommandIsSuccessful();
+ // or
+ $this->assertSame(0, $tester->getStatusCode());
+
+ // Assert output contains expected text
+ $output = $tester->getDisplay();
+ $this->assertStringContainsString('User "john" created', $output);
+ }
+
+}
+```
+
+## Testing with Nette DI
+
+For commands with dependencies, use Nette's container:
+
+```php
+use Contributte\Tester\Utils\ContainerBuilder;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\ApplicationTester;
+use Tester\Assert;
+use Tester\TestCase;
+
+final class CreateUserCommandTest extends TestCase
+{
+
+ public function testCommand(): void
+ {
+ $container = ContainerBuilder::of()
+ ->withCompiler(function ($compiler) {
+ $compiler->addConfig(__DIR__ . '/config.neon');
+ })
+ ->build();
+
+ $application = $container->getByType(Application::class);
+ $application->setAutoExit(false);
+
+ $tester = new ApplicationTester($application);
+ $tester->run(['command' => 'app:create-user', 'username' => 'john']);
+
+ Assert::same(0, $tester->getStatusCode());
+ Assert::contains('User "john" created', $tester->getDisplay());
+ }
+
+}
+```
+
+## Testing interactive commands
+
+For commands with user prompts, use `setInputs()`:
+
+```php
+$tester = new CommandTester($command);
+
+// Simulate user typing "yes" then "john@example.com"
+$tester->setInputs(['yes', 'john@example.com']);
+
+$tester->execute(['username' => 'john']);
+```
+
+## Useful assertions
+
+```php
+// Check exit code
+$tester->assertCommandIsSuccessful();
+
+// Get output
+$output = $tester->getDisplay();
+$output = $tester->getDisplay(true); // Normalized (no decorations)
+
+// Get error output (stderr)
+$errorOutput = $tester->getErrorOutput();
+
+// Get status code
+$exitCode = $tester->getStatusCode();
+
+// Get input used
+$input = $tester->getInput();
+```
+> See [How to Test Commands](https://symfony.com/doc/current/console.html#testing-commands) in Symfony docs.
## Development