Консольные команды в Symfony: расширенные возможности
Консольные команды Symfony - прекрасный инструмент для решения определенных задач. В этой статье поговорим о расширенных и неожиданных возможностях компонента Symfony/Console.
Будем использовать Docker проект, который написали в прошлой статье, поэтому зайдите в него и в корневой директории запустите контейнеры командой docker-compose up -d. После запуска зайдите в контейнер с php-cli:
docker exec -it symfony-app-php-cli bash
Создание простой команды
Чтобы создать команду и зарегистрировать ее, вам необходимо в папке Command или любой другой создать класс и отнаследоваться от Symfony\Component\Console\Command\Command. В примитивном случае ваша команда будет выглядеть следующим образом:
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
final class AdvancedCommand extends Command
{
}
Чтобы команда была доступна из консоли, ей нужно дать имя. Сделать это можно тремя способами:
- Через свойство $defaultName
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
final class AdvancedCommand extends Command
{
protected static $defaultName = 'command:advance';
}
- Через конструктор
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
final class AdvancedCommand extends Command
{
public function __construct(string $name = 'command:advance')
{
parent::__construct($name);
}
}
- Через метод configure
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this->setName('command:advance');
}
}
Выберите один из вариантов и выполните в терминале следующую команду:
php bin/console command:advance
Сейчас вы получите ошибку о том, что не определили метод execute, но мы увидели, что хотели, - команда работает.
Итак, всю полезную работу команда должна выполнять в методе execute, именно его вызывает Symfony, когда вы обращаетесь к своей команде:
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this->setName('command:advance');
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}
Метод execute принимает два аргумента: InputInterface и OutputInterface. Как вы уже догадались, первый нужен для того, чтобы получить пользовательский ввод, а второй - вывести ответ обратно пользователю. Давайте напишем команду, которая будет спрашивать у пользователя его имя и фамилию, а затем выводить их вместе:
<?php
declare(strict_types=1);
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;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('This command will ask you for name and surname and print them back.')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
do {
$name = $io->ask('Ваше имя');
} while (null === $name);
do {
$surname = $io->ask('Ваша фамилия');
} while (null === $surname);
$io->success(\sprintf('Ваше полное имя: %s %s', $surname, $name));
return 1;
}
}
В методе configure мы добавили описание команды через setDescription, оно будет выводиться напротив вашей команды в терминале, когда вы запросите список всех команд. В методе execute мы используем специальный компонент SymfonyStyle, который умеет красиво выводить в консоль, спрашивать вопросы, рисовать таблицы, прогресс-бары и многое другое. В двух циклах do..while мы спрашиваем имя и фамилию пользователя до тех пор, пока он их не введет. Потом через метод success выводим обратно в терминал.
Расширенные возможности
А что если нам надо дать возможность пользователю передавать команде флаги для изменения ее поведения? Не проблема, компонент дает возможность задавать как аргументы, так и опции. Аргументы - это упорядоченные строки, разделенные пробелами. Они могут быть обязательными, опциональными и приходить в виде массива. Давайте запросим у пользователя имя и фамилию вместе с командой:
<?php
declare(strict_types=1);
namespace App\Command;
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\Style\SymfonyStyle;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('This command will ask you for name and surname and print them back.')
->addArgument('surname', InputArgument::REQUIRED)
->addArgument('name', InputArgument::REQUIRED)
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
$surname = $input->getArgument('surname');
$io->success(\sprintf('Ваше имя: %s %s', $surname, $name));
return 1;
}
}
Вызвать эту команду можно следующим образом:
php bin/console command:advance Doe John
В методе configure мы определили аргументы, а в execute достали их. Все просто. Метод addArgument также принимает описание аргумента и значение по умолчанию, если оно есть. Чтобы пользователи вашей команды знали, что ждет ваша команда, вы можете вызвать вашу команду с флагом -h:
root@f876fdb5dd87:/symfony# php bin/console command:advance -h
Description:
This command will ask you for name and surname and print them back.
Usage:
command:advance <surname> <name>
Arguments:
surname
name
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-e, --env=ENV The Environment name. [default: "dev"]
--no-debug Switches off debug mode.
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Вы также можете добавить дополнительную справку к вашей команде, вызвав в методе configure метод setHelp.
Опции - это, наоборот, не упорядоченные строки, передавать их можно в любом порядке, указывая их следующим образом:
php bin/console command:advance --surname=Doe --name=John
Чтобы использовать опции, надо определить их точно так же, как и аргументы:
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('This command will ask you for name and surname and print them back.')
->addOption('surname', 's', InputOption::VALUE_REQUIRED)
->addOption('name', 'm', InputOption::VALUE_REQUIRED)
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$surname = $input->getOption('surname');
$name = $input->getOption('name');
$io->success(\sprintf('Ваше имя: %s %s', $surname, $name));
return 1;
}
}
Вы можете определить сокращенные имена для ваших опций, тогда команду можно будет вызвать так:
php bin/console -s Doe -m John
Рисуем таблицы и прогресс-бары
Таблицы могут применяться для удобного вывода структурированной информации. Предположим, у вас в приложении есть система ролей и прав, у каждой роли - свои права. Вы хотите менять права пользователям из консольной команды. Для этого хорошо бы иметь перед глазами полную информацию о том, что может каждая роль. Давайте это сделаем.
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('Command print roles and they permissions')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$table = new Table($output);
$roles = [
[
'id' => 1,
'role' => 'ROLE_SUPERADMIN',
'permissions' => \implode(', ', ['EDIT', 'CREATE', 'DELETE', 'CHANGE ROLE'])
],
[
'id' => 2,
'role' => 'ROLE_ADMIN',
'permissions' => \implode(', ', ['EDIT', 'CREATE', 'DELETE'])
],
[
'id' => 3,
'role' => 'ROLE_EDITOR',
'permissions' => \implode(', ', ['EDIT', 'CREATE'])
],
[
'id' => 4,
'role' => 'ROLE_USER',
'permissions' => \implode(', ', ['CREATE'])
]
];
$table
->setHeaders(['Id', 'Роль', 'Права'])
->setRows($roles)
;
$table->render();
return 1;
}
}
Представим, что мы достали из базы roles, а не создали массив тут. Это удобно, если у вас будет изменяться система прав. Теперь, если вы вызовите команду, вы увидите следующее:
Удобно, не так ли? Теперь давайте объединим вопрос и таблицу. Мы должны получить от пользователя id роли и назначить ее какому-либо из юзеров, почту которого мы так же попросим ввести:
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('Command print roles and they permissions')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$table = new Table($output);
$roles = [
[
'id' => 1,
'role' => 'ROLE_SUPERADMIN',
'permissions' => \implode(', ', ['EDIT', 'CREATE', 'DELETE', 'CHANGE ROLE'])
],
[
'id' => 2,
'role' => 'ROLE_ADMIN',
'permissions' => \implode(', ', ['EDIT', 'CREATE', 'DELETE'])
],
[
'id' => 3,
'role' => 'ROLE_EDITOR',
'permissions' => \implode(', ', ['EDIT', 'CREATE'])
],
[
'id' => 4,
'role' => 'ROLE_USER',
'permissions' => \implode(', ', ['CREATE'])
]
];
$table
->setHeaders(['Id', 'Роль', 'Права'])
->setRows($roles)
;
$table->render();
do {
$role = $io->ask('Введит id роли, которую хотите присвоить пользователю');
} while (null === $role);
do {
$email = $io->ask('Введите почту пользователя, которому хотите присвоить роль');
} while (null === $email);
$io->success(\sprintf('Пользователю %s будет присвоена роль %s', $email, $role));
$result = $io->confirm('Подтвердить?');
if (true === $result) {
// change role
$io->success('Роль присвоена');
return 1;
}
$io->comment('Изменение прав отменено');
return 1;
}
}
Мы использовали метод confirm, которой запросит у пользователя подтверждение и вернет его нам. Если мы получили подтверждение, меняем роль. Про все возможности таблицы в терминале вы можете узнать из документации.
Прогресс-бары
Прогресс-бары так же, как и таблицы, нужны для интерактивности, чтобы юзер видел, что команда работает, а не уснула. Например, у вас есть большая операция по изменению большой пачки данных из таблиц. Вы собираетесь выполнять ее в цикле с множеством операций для каждой из записи. Давайте сымитируем это поведение:
<?php
declare(strict_types=1);
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;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('Command print roles and they permissions')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$progressBar = $io->createProgressBar(100);
for ($i = 0; $i < 100; $i++) {
\sleep(1);
$progressBar->advance();
}
$progressBar->finish();
return 1;
}
}
Мы создали прогресс бар, передали ему максимальное кол-во элементов. Дальше мы в простом цикле обходим элементы, засыпаем на 1 секунду и после этого увеличиваем прогресс-бар. Запустите нашу команду и посмотрите, что будет. Больше про прогресс-бары читайте в документации.
Вызываем другие консольные команды
Возможно, вы захотите вызывать одни консольные команды из других. Сделать это можно следующим образом:
<?php
declare(strict_types=1);
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;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('Command print roles and they permissions')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$command = $this->getApplication()->find('list');
$code = $command->run($input, $output);
$io->success($code);
return 1;
}
}
Мы вызвали встроенную команду list, которая вернет полный список команд нашего приложения. Чтобы узнать статус выполнения команды - успешно или нет, - можно использовать код, которая она вернула.
Если же вы хотите вызвать команду, которая принимает аргументы или опции, сделать это можно так:
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class AdvancedCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function configure(): void
{
$this
->setName('command:advance')
->setDescription('Command print roles and they permissions')
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$command = $this->getApplication()->find('cache:warmup');
$arrayInput = new ArrayInput([
'--env' => 'dev',
]);
$code = $command->run($arrayInput, $output);
$io->success($code);
return 1;
}
}
Мы вызвали встроенную команду симфони для прогрева кэша и передали туда опцию, указывающую на окружение, в котором мы находимся.
Разумеется, вы можете внедрять любые сервисы через конструктор, Symfony их вам заинжектит. Вы также можете использовать ваши команды в качестве крон команд, указав полное имя к вашему проекту плюс php bin/console <command_name>.
Надеюсь, этих возможностей вам хватит, чтобы автоматизировать часть рутинных задач вашего приложения.
Комментарии