Консольные команды в Symfony: расширенные возможности

21.02.2020 в 11:42
5297
+80

Консольные команды 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
{
}

Чтобы команда была доступна из консоли, ей нужно дать имя. Сделать это можно тремя способами:

  1. Через свойство $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';
}
  1. Через конструктор
<?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);
    }
}
  1. Через метод 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>.

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

loader
21.02.2020 в 11:42
5297
+80
Логические задачи с собеседований