MidПрактика6 min

Input/Output и Helpers

InputInterface, OutputInterface, SymfonyStyle, Table, ProgressBar, QuestionHelper

OutputInterface

OutputInterface предоставляет методы для вывода текста в консоль.

<?php

declare(strict_types=1);

namespace App\Command;

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

#[AsCommand(name: 'app:output-demo')]
final class OutputDemoCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        // Basic output
        $output->writeln('Regular line');
        $output->write('Without newline... ');
        $output->writeln('continuing');

        // Formatted output with tags
        $output->writeln('<info>Info message (green)</info>');
        $output->writeln('<comment>Comment (yellow)</comment>');
        $output->writeln('<question>Question (black on cyan)</question>');
        $output->writeln('<error>Error message (white on red)</error>');

        // Custom formatting
        $output->writeln('<fg=red>Red text</>');
        $output->writeln('<fg=blue;bg=white>Blue on white</>');
        $output->writeln('<fg=green;options=bold>Bold green</>');
        $output->writeln('<fg=cyan;options=underscore>Underlined cyan</>');

        // Multiple lines at once
        $output->writeln([
            'Line 1',
            'Line 2',
            'Line 3',
        ]);

        return Command::SUCCESS;
    }
}

SymfonyStyle

SymfonyStyle -- рекомендуемый способ форматирования ввода-вывода. Предоставляет единообразный стиль для всех команд.

<?php

declare(strict_types=1);

namespace App\Command;

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:style-demo')]
final class StyleDemoCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);

        // Title and section
        $io->title('Application Style Demo');
        $io->section('Section 1: Messages');

        // Message types
        $io->success('Operation completed successfully!');
        $io->error('Something went wrong.');
        $io->warning('Be careful with this operation.');
        $io->note('This is an informational note.');
        $io->caution('This action cannot be undone!');
        $io->info('Processing started...');

        // Text output
        $io->text('Simple text message');
        $io->text([
            'First paragraph',
            'Second paragraph',
        ]);

        // Listing
        $io->listing([
            'First item',
            'Second item',
            'Third item',
        ]);

        // Definition list
        $io->definitionList(
            ['Name' => 'John Doe'],
            ['Email' => '[email protected]'],
            ['Role' => 'Admin'],
        );

        // Horizontal table
        $io->horizontalTable(
            ['Name', 'Email', 'Role'],
            [
                ['John', '[email protected]', 'Admin'],
            ],
        );

        // New line
        $io->newLine(2);

        return Command::SUCCESS;
    }
}

Интерактивный ввод через SymfonyStyle

<?php

declare(strict_types=1);

namespace App\Command;

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:interactive')]
final class InteractiveCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);

        // Simple question
        $name = $io->ask('What is your name?', 'Anonymous');

        // Hidden input (password)
        $password = $io->askHidden('Enter password');

        // Confirmation (yes/no)
        $confirmed = $io->confirm('Do you want to continue?', true);

        // Choice question
        $color = $io->choice(
            'Select your favorite color',
            ['Red', 'Green', 'Blue'],
            'Blue', // default
        );

        // Multi-select choice
        // (user can select multiple options by separating with comma)

        if ($confirmed) {
            $io->success("Hello, $name! Your color: $color");
        }

        return Command::SUCCESS;
    }
}

Table Helper

<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: 'app:table-demo')]
final class TableDemoCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        // Basic table
        $table = new Table($output);
        $table
            ->setHeaders(['ID', 'Name', 'Email', 'Role'])
            ->setRows([
                [1, 'Alice', '[email protected]', 'Admin'],
                [2, 'Bob', '[email protected]', 'User'],
                new TableSeparator(),
                [3, 'Charlie', '[email protected]', 'Editor'],
            ]);

        $table->setStyle('box'); // box, compact, borderless, box-double
        $table->render();

        // Table via SymfonyStyle
        $io = new \Symfony\Component\Console\Style\SymfonyStyle($input, $output);
        $io->table(
            ['Name', 'Status'],
            [
                ['Service A', '<info>Running</info>'],
                ['Service B', '<error>Stopped</error>'],
                ['Service C', '<comment>Starting</comment>'],
            ],
        );

        // Table with cell spanning
        $table2 = new Table($output);
        $table2
            ->setHeaders(['Name', 'Status', 'Details'])
            ->setRows([
                ['Server 1', 'OK', 'All good'],
                new TableSeparator(),
                [new TableCell('Summary', ['colspan' => 3])],
            ]);
        $table2->render();

        return Command::SUCCESS;
    }
}

ProgressBar

<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'app:progress-demo')]
final class ProgressDemoCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $items = range(1, 100);

        // Basic ProgressBar
        $progressBar = new ProgressBar($output, count($items));
        $progressBar->start();

        foreach ($items as $item) {
            // Process item...
            usleep(20000); // simulate work

            $progressBar->advance();
        }

        $progressBar->finish();
        $output->writeln(''); // new line after progress bar

        // Custom format
        $progressBar2 = new ProgressBar($output, 50);
        $progressBar2->setFormat(
            ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'
        );
        $progressBar2->start();

        for ($i = 0; $i < 50; $i++) {
            usleep(20000);
            $progressBar2->advance();
        }

        $progressBar2->finish();
        $output->writeln('');

        // Via SymfonyStyle (simplest)
        $io = new SymfonyStyle($input, $output);
        $io->progressStart(count($items));

        foreach ($items as $item) {
            usleep(10000);
            $io->progressAdvance();
        }

        $io->progressFinish();

        // ProgressBar with message
        $progressBar3 = new ProgressBar($output, 3);
        $progressBar3->setFormat(' %current%/%max% -- %message%');
        $progressBar3->setMessage('Starting...');
        $progressBar3->start();

        $progressBar3->setMessage('Processing users...');
        $progressBar3->advance();
        usleep(500000);

        $progressBar3->setMessage('Processing orders...');
        $progressBar3->advance();
        usleep(500000);

        $progressBar3->setMessage('Done!');
        $progressBar3->finish();
        $output->writeln('');

        return Command::SUCCESS;
    }
}

Предопределённые форматы ProgressBar

Формат Описание
normal Без подробностей
verbose С прошедшим временем
very_verbose С прошедшим и оставшимся временем
debug С памятью

QuestionHelper

<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

#[AsCommand(name: 'app:question-demo')]
final class QuestionDemoCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        /** @var QuestionHelper $helper */
        $helper = $this->getHelper('question');

        // Simple question with default
        $question = new Question('Enter your name: ', 'World');
        $name = $helper->ask($input, $output, $question);

        // Question with validation
        $emailQuestion = new Question('Enter email: ');
        $emailQuestion->setValidator(function (?string $answer): string {
            if (empty($answer) || !str_contains($answer, '@')) {
                throw new \RuntimeException('Please enter a valid email.');
            }
            return $answer;
        });
        $emailQuestion->setMaxAttempts(3); // Max 3 tries
        $email = $helper->ask($input, $output, $emailQuestion);

        // Hidden question (password)
        $passwordQuestion = new Question('Enter password: ');
        $passwordQuestion->setHidden(true);
        $passwordQuestion->setHiddenFallback(false); // Don't fallback to visible
        $password = $helper->ask($input, $output, $passwordQuestion);

        // Confirmation question
        $confirm = new ConfirmationQuestion(
            'Continue? (y/N) ',
            false, // default: no
        );
        $proceed = $helper->ask($input, $output, $confirm);

        // Choice question
        $choice = new ChoiceQuestion(
            'Select database driver',
            ['MySQL', 'PostgreSQL', 'SQLite'],
            1, // default: PostgreSQL
        );
        $choice->setErrorMessage('Invalid driver: %s');
        $driver = $helper->ask($input, $output, $choice);

        // Multi-select choice
        $multiChoice = new ChoiceQuestion(
            'Select features (comma-separated)',
            ['Auth', 'API', 'Admin', 'CLI'],
        );
        $multiChoice->setMultiselect(true);
        $features = $helper->ask($input, $output, $multiChoice);

        // Autocomplete
        $countryQuestion = new Question('Enter country: ');
        $countryQuestion->setAutocompleterValues([
            'France', 'Germany', 'Italy', 'Spain', 'Ukraine',
        ]);
        $country = $helper->ask($input, $output, $countryQuestion);

        $output->writeln("Name: $name, Email: $email, Driver: $driver");

        return Command::SUCCESS;
    }
}

Форматирование в SymfonyStyle: сводка

Метод Внешний вид
title() Заголовок с подчёркиванием
section() Подзаголовок
text() Обычный текст
listing() Маркированный список
table() Таблица
success() Зелёный блок с галочкой
error() Красный блок с крестом
warning() Жёлтый блок
note() Блок с заметкой
caution() Блок с предупреждением
ask() Вопрос
askHidden() Скрытый ввод
confirm() Да/Нет
choice() Выбор из списка
progressStart/Advance/Finish() ProgressBar

Итоги

  • SymfonyStyle -- рекомендуемый объект для ввода-вывода
  • Table поддерживает стили: box, compact, borderless, box-double
  • ProgressBar с форматами: normal, verbose, very_verbose, debug
  • QuestionHelper -- низкоуровневые вопросы с валидацией и автодополнением
  • SymfonyStyle оборачивает QuestionHelper: ask(), confirm(), choice()