Как оформить консольную команду

Дата обновления перевода 2024-07-17

Как оформить консольную команду

Одним из наиболее скучных занятий при создании консольных команд является офомрление ввода и вывода команды. Отображение заголовков и таблиц или вопросов к пользователю задействует большое количество повторяющегося кода.

К примеру, рассмотрите код, используемый для отображение заголовка следующей команды:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Command/GreetCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln([
            '<info>Lorem Ipsum Dolor Sit Amet</>',
            '<info>==========================</>',
            '',
        ]);

        // ...
    }
}

Отображение простого заголовка требует трёх строчек кода: чтобы изменить фронт контроллер, подчеркнуть содержимое и оставить дополнительную пустую строку после заглавия. Оформление требуется для всех хорошо продуманных команд, но оно излишне усложняет код.

Для того, чтобы уменьшить этот шаблонный код, команды Symfony могут по желанию использовать Справочник стилей Symfony. Эти стили реализуются, как набор методов-помощников, которые позволяют создавать семантические команды и забыть об их оформлении.

Базовое использование

В вашей команде, инстанциируйте класс SymfonyStyle и передайте переменные $input и $output в качестве его аргументов. Далее, вы можете начать использовать любой из его помощников, как, например, title(), который отображает заглавие команды:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Command/GreetCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class GreetCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->title('Lorem Ipsum Dolor Sit Amet');

        // ...
    }
}

Методы-помощники

Класс SymfonyStyle определяет некоторые методы-помощники, которые охватывают наиболее распространённый действия, выполняемые консольными командами.

Методы титрования

title()

Отображает заданную строку, как заглавие команды. Этот метод должен был бы использваться в данной команде только единожды, но ничто не останавливает вас от его повторного использования:

1
$io->title('Lorem ipsum dolor sit amet');
section()

Отображает заданную строку, как заглавие некоторой части команды. Он необходим только в сложных командах, которые требуют лучшего разделения своего содержимого:

1
2
3
4
5
6
7
$io->section('Adding a User');

// ...

$io->section('Generating the Password');

// ...

Методы содержания

text()

Отображает заданную строку или массив строк, как обычный текст. Это полезно для отображения сообщений помощи и инструкций для пользователя, выполняющего команду:

1
2
3
4
5
6
7
8
9
10
11
// используйте простые строки для коротких сообщений
$io->text('Lorem ipsum dolor sit amet');

// ...

// рассмотрите использование массивов при отображении длинных сообщений
$io->text([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
    'Aenean sit amet arcu vitae sem faucibus porta',
]);
listing()

Отображает неупорядоченный список элементов, переданных в виде массива:

1
2
3
4
5
$io->listing([
    'Element #1 Lorem ipsum dolor sit amet',
    'Element #2 Lorem ipsum dolor sit amet',
    'Element #3 Lorem ipsum dolor sit amet',
]);
table()

Отображает заданный массив заголовков и строк в виде компактной таблицы:

1
2
3
4
5
6
7
8
$io->table(
    ['Header 1', 'Header 2'],
    [
        ['Cell 1-1', 'Cell 1-2'],
        ['Cell 2-1', 'Cell 2-2'],
        ['Cell 3-1', 'Cell 3-2'],
    ]
);
horizontalTable()

Отображает заданный массив заголовков и строк в виде компактной горизонтальной таблицы:

1
2
3
4
5
6
7
8
$io->horizontalTable(
    ['Header 1', 'Header 2'],
    [
        ['Cell 1-1', 'Cell 1-2'],
        ['Cell 2-1', 'Cell 2-2'],
        ['Cell 3-1', 'Cell 3-2'],
    ]
);
definitionList()

Отображает заданные пары key => value в виде компактного списка элементов:

1
2
3
4
5
6
7
8
9
$io->definitionList(
    'This is a title',
    ['foo1' => 'bar1'],
    ['foo2' => 'bar2'],
    ['foo3' => 'bar3'],
    new TableSeparator(),
    'This is another title',
    ['foo4' => 'bar4']
);
createTable()
Создает экземпляр Table, оформленный в соответствии с Руководством Symfony об оформлении, что позволяет вам использовать функции вроде добавления строк динамически.
newLine()

Отображает пустую строчку в выводе команды. Несмотря на то, что это может выглядеть полезным, в большинстве случаев, он не понадобится вам вообще. Причина кроется в том, что каждый помощник уже добавляет собственные пустые строки, так что вам не придётся заботиться о вертикальной разбивке:

1
2
3
4
5
// выводит одну пустую строку
$io->newLine();

// выводит три пустых строки подряд
$io->newLine(3);

Методы предупреждения

note()

Отображает заданную строку или массив строк, как выделенное предупреждение. Используйте этого помощника осторожно, чтобы избежать засорения вывода команды:

1
2
3
4
5
6
7
8
9
10
11
// используйте простые строки для коротких заметок
$io->note('Lorem ipsum dolor sit amet');

// ...

// рассмотрите исползование массивов при отображении длиных заметок
$io->note([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
    'Aenean sit amet arcu vitae sem faucibus porta',
]);
caution()

Схож с помощником note(), но содержимое выделяется более заметно. Результирующее содержимое имеет сходство с сообщенем об ошибке,так что вам стоит избегать использования этого помощника кроме случаев, когда это крайне необходимо:

1
2
3
4
5
6
7
8
9
10
11
// используйте простые строки для коротких предупреждающих сообщений
$io->caution('Lorem ipsum dolor sit amet');

// ...

// рассмотрите использование массивов при отображении длинных предупреждающих сообщений
$io->caution([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
    'Aenean sit amet arcu vitae sem faucibus porta',
]);

Методы индикатора выполнения

progressStart()

Отображает индикатор выполнения с количеством шагов, равным аргументу, переданному методу (не передавайте значение, если длина индикатора выполнения неизвестна):

1
2
3
4
5
// отображает индикатор выполнения неизвестной длины
$io->progressStart();

// отображает индикатор выполнения с длиной в 100 шагов
$io->progressStart(100);
progressAdvance()

Заставляет индикатор выполнения продвинуться на заданное количество шагов (или 1 шаг, если не было передано аргументов):

1
2
3
4
5
// продвигает индикатор выполнения на 1 шаг
$io->progressAdvance();

// продвигает индикатор выполнения на 10 шагов
$io->progressAdvance(10);
progressFinish()

Завершает индикатор выполнения (заполняет все оставшиеся шаги при неизвестной длине):

1
$io->progressFinish();
progressIterate()

Если ваш индикатор выполнения накладывается на итерируемую коллекцию, используйте помощника progressIterate():

1
2
3
4
5
$iterable = [1, 2];

foreach ($io->progressIterate($iterable) as $value) {
    // ... сделать какую-то работу
}
createProgressBar()
Создает экземпляр ProgressBar, оформленный в соответствии с Руководством Symfony об оформлении.

Методы ввода пользователя

ask()

Просит пользователя предоставить какие-то данные:

1
$io->ask('What is your name?');

Вы можете передать значение по умолчанию в качестве второго аргумента, чтобы пользователь мог просто нажать клавишу <Enter> для выбора этого значения:

1
$io->ask('Where are you from?', 'United States');

В случае, если вам нужно валидировать данное значение, передайте валидатор обратного вызова в качестве третьего аргумента:

1
2
3
4
5
6
7
$io->ask('Number of workers to start', '1', function (string $number): int {
    if (!is_numeric($number)) {
        throw new \RuntimeException('You must type a number.');
    }

    return (int) $number;
});
askHidden()

Очень похож на метод ask(), но ввод пользователя будет скрыт и не может определять значение по умолчанию. Используйте его, когда запрашиваете чувствительную информацию:

1
$io->askHidden('What is your password?');

В случае, если вам нужно валидировать заданное значение, передайте валидатор обратного вызова в качестве второго аргумента:

1
2
3
4
5
6
7
$io->askHidden('What is your password?', function (string $password): string {
    if (empty($password)) {
        throw new \RuntimeException('Password cannot be empty.');
    }

    return $password;
});
confirm()

Задаёт пользователю вопрос, на который можно ответить Да/Нет и возвращает только true или false:

1
$io->confirm('Restart the web server?');

Вы можете передать значение по умолчанию в качестве второго аргумента, чтобы пользователь мог просто нажать клавишу <Enter>, чтобы выбрать это значение:

1
$io->confirm('Restart the web server?', true);
choice()

Задаёт вопрос, ответ которого ограничен заданным списком валидных ответов:

1
$io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3']);

Вы можете передать значение по умолчанию в качестве третьего аргумента, чтобы пользователь мог просто нажать клавишу <Enter>, чтобы выбрать это значение:

1
$io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1');

Наконец-то вы можете позволить пользователям выбирать несколько вариантов. Чтобы сделать это, пользователи должны разделить каждый выбор запятой (например, введение 1, 2 выберет варианты 1 и 2):

1
$io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], multiSelect: true);

Методы результатов

Note

Если вы вводите любой URL, он не будет поломан/обрезан, он будет кликабельным - если терминал это предоставляет. Если "хорошо сформатированный вывод" важнее, вы можете отключить это:

1
$io->getOutputWrapper()->setAllowCutUrls(true);
success()

Отображает заданную строку или массив строк, выделенных, как сообщение об успехе (зелёный фон с ярлыком [OK]). Должен был бы использоваться единожды для отображения финального результата выполнения данной команды, но вы можете использовать его повторно во время выполнения команды:

1
2
3
4
5
6
7
8
9
10
// используйте простые строки для коротких сообщений об успехе
$io->success('Lorem ipsum dolor sit amet');

// ...

// рассмотрите использование массивов при отображении длинных сообщений об успехе
$io->success([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
]);
info()

Похож на метод success() (заданная строка или массив строк отображаются на зеленом фоне), но ярлык [OK] не имеет префикса, Должен быть использован единожды для отображения финального результата выполнения заданной команды, не отображая результат как успешный или неуспешный:

1
2
3
4
5
6
7
8
9
10
// использовать простые строки для коротких информационных сообщений
$io->info('Lorem ipsum dolor sit amet');

// ...

// рассмотреть использование массивов при отображении длинных информационных сообщений
$io->info([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
]);
warning()

Отображает заданную строку или массив строк, выделенных, как сообщение предостережение (с красным фоном и ярлыком [WARNING]). Должен был бы использоваться единожды для отображения финального результата выполнения данной команды, но вы можете использовать его повторно во время выполнения команды:

1
2
3
4
5
6
7
8
9
10
// используйте простые строки для коротких сообщений предостережения
$io->warning('Lorem ipsum dolor sit amet');

// ...

// рассмотрите использование массивов при отображении длинных сообщений предостережения
$io->warning([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
]);
error()

Отображает заданную строку или массив строк, выделенных, как сообщение об ошибке (с красным фоном и ярлыком [ERROR]). Должен был бы использоваться единожды для отображения финального результата выполнения данной команды, но вы можете использовать его повторно во время выполнения команды:

1
2
3
4
5
6
7
8
9
10
// используйте простые строки для коротких сообщений об ошибке
$io->error('Lorem ipsum dolor sit amet');

// ...

// рассмотрите использование массивов при отображении длинных сообщений об ошибке
$io->error([
    'Lorem ipsum dolor sit amet',
    'Consectetur adipiscing elit',
]);

Конфигурация стилей по умолчанию

По умолчанию, стили Symfony оборачивают всё содержание, чтобы избежать строк слишком длинного текста. Единственным исключением являются URL, которые не оборачиваются, независимо от их длины. Это делается, чтобы включить кликабельные URL в терминалах, которые их поддерживают.

Если вы предпочитаете оборачивать всё содержание, включительно с URL, используйте этот метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Command/GreetCommand.php
namespace App\Command;

// ...
use Symfony\Component\Console\Style\SymfonyStyle;

class GreetCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->getOutputWrapper()->setAllowCutUrls(true);

        // ...
    }
}

Определение ваших собственных стилей

Если вам не нравится дизайн команд, использующих Оформление Symfony, вы можете определить ваш собственный набор стилей консоли. Просто создайте класс, реализующий StyleInterface:

1
2
3
4
5
6
7
8
namespace App\Console;

use Symfony\Component\Console\Style\StyleInterface;

class CustomStyle implements StyleInterface
{
    // ...реализовать методы интерфейса
}

Далее, инстанциируйте в ваших командах этот пользовательский класс вместо класса по умолчанию SymfonyStyle. Благодаря StyleInterface вам не понадобится изменять код ваших команд, чтобы изменить их внешний вид:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Command/GreetCommand.php
namespace App\Console;

use App\Console\CustomStyle;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {

        // До
        // $io = new SymfonyStyle($input, $output);

        // После
        $io = new CustomStyle($input, $output);

        // ...
    }
}

Запись в вывод ошибки

Если вы повторно используете вывод команды в качестве ввода других команд, или сбрасываете его в файл для последующего использования, вы наверное хотите исключить индикаторы выполнения, заметки и другой вывод, который не имеет реального значения.

Команды могут выводить информацию в двух разных потоках: stdout (стандартный вывод) - это поток, где должно выводиться настоящее содержимое, а stderr (стандартная ошибка) - это поток, где должны выводиться ошибки и сообщения отладки.

Класс SymfonyStyle предоставляет удобный метод под названием getErrorStyle() для переключения между потоками. Этот метод возвращает новый экземпляр SymfonyStyle, который использует вывод ошибок:

1
2
3
4
5
6
7
$io = new SymfonyStyle($input, $output);

// Написать в стандартный вывод
$io->write('Reusable information');

// Написать в вывод ошибки
$io->getErrorStyle()->warning('Debugging information or errors');

Note

Если вы создадите экземпляр SymfonyStyle с объектом OutputInterface, который не является экземпляром ConsoleOutputInterface, то метод getErrorStyle() не будет иметь никакого эффекта, а возвращённый объект будет всё равно писать в стандартный вывод, вместо вывода ошибки.