How to Add Custom CLI Commands in Magento 2

This entry was posted on March 13, 2020 by Vladimir Đurović, Backend Developer.

Magento Custom CLI

The Command Line Interface (CLI) is a powerful tool that can greatly facilitate the development, use or maintenance of the Magento 2 platform. In this article, you can find everything about creating and using Magento 2 CLI commands. 

We will use the SyncIt_Commands module as a demo module in this article. In the following link, you can see how to create a simple Magento 2 module.

As with any other implementation, it is also important to familiarize yourself with the general conventions so that your commands are consistent with those of other developers. This avoids implementation problems and reduces the learning curve of users by a large percentage.

Command Name - Format

The name of the command itself is one part of the command that describes its behavior. To avoid the problem stated in the famous Phil quote:

“There are only two hard things in Computer Science: 
cache invalidation and naming things.” -- Phil Karlton

Magento documentation suggests the following format and method for naming the command:

Format: group:[subject:]action

  • group

group represents a group of related commands.

  • subject

subject is optional but very useful in defining the object more closely. You can use multiple words to use the subject in your command, where a dash or a hyphen character will be used for space.

  • action

action represents a command action, for example, start, stop, and so on.

Command Arguments and Options

The arguments and options of the command stand next to the name of the command itself and change its behavior. Also, arguments and options create a different user experience, and as a programmer, you can choose what type of input is needed in this case.

Simple Command

Before starting with explanations and examples with arguments and options, it is a practice to create a ‘Hello World’ structure. The following example is a simple command that displays a message in the console.

// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;


/**
 * Class SimpleCommandExample
 * @package SyncIt\Commands\Console\Command
 */
class SimpleCommandExample extends Command
{
    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Hello World!');
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName("blog:simple:example");
        $this->setDescription(__("Run Blog Post Simple Command Example"));
        parent::configure();
    }
}

Command Arguments

Arguments are values ​​passed to the user when the commute is fired. The name of the argument is not visible to the command user, only to the developer. Argument commands are used when you need information from the user, and no more than 3 command arguments are recommended in order to avoid the user’s confusion.

Example: 

bin/magento blog:command-arguments:example Vladimir Developer

Where:

“Vladimir” is $name argument
“Developer” is $role argument

// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;


/**
 * Class CommandArgumentsExample
 * @package SyncIt\Commands\Console\Command
 */
class CommandArgumentsExample extends Command
{
    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Value "name" = ' . $input->getArgument('name'));
        $output->writeln('Value "role" = ' . $input->getArgument('role'));
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName("blog:command-arguments:example");
        $this->setDescription(__("Run Blog Post Command With Arguments Example"));
        $this->setDefinition([
             new InputArgument(
                  'Name',
                   InputArgument::REQUIRED, 
                  __("Employee Name")
             ),
             new InputArgument(
                  'role',
                  InputArgument::REQUIRED, 
                  __("Employee Role")
             )
        ]);
        parent::configure();
    }
}

Command Options

The options represent the name-value of the pair. An option as such may have a value or not. An option that does not require a value is called a flag, and it has a simple yes or no value. The sequence of options entered does not matter. Options can also have shortcuts used in one letter or full word, such as -f for --force.

Example:

bin/magento blog:command-option:example -o Vladimir --role Developer

Where:

“-o” is a shortcut for “--name”
Shortcut for “--role” is “-r”

// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;


/**
 * Class CommandArgumentsExample
 * @package SyncIt\Commands\Console\Command
 */
class CommandOptionsExample extends Command
{
    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Value "name" = ' . $input->getOption('name'));
        $output->writeln('Value "role" = ' . $input->getOption('role'));
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName("blog:command-options:example");
        $this->setDescription(__("Run Blog Post Command With Options Example"));
        $this->setDefinition([
             new InputOption(
                  'name', 'o',
                  InputOption::VALUE_REQUIRED, 
                  __("Employee Name")
             ),
             new InputOption(
                  'role', 'r',
                  InputOption::VALUE_OPTIONAL, 
                  __("Employee Role")
             )
        ]);
        parent::configure();
    }
}

In these classes, we will define the following 2 methods:

  • configure() sets the name, description, command-line arguments of the Magento 2 add command line
  • execute() runs when this command line is called via console

Declare your Command classes in Magento\Framework\Console\CommandListInterface and configure the command name using dependency injection (<your component root dir>/etc/di.xml):

SyncIt/Commands/etc/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="SimpleCommandExample" xsi:type="object">SyncIt\Commands\Console\Command\SimpleCommandExample</item>
                <item name="CommandArgumentsExample" xsi:type="object">SyncIt\Commands\Console\Command\CommandArgumentsExample</item>
                <item name="CommandOptionsExample" xsi:type="object">SyncIt\Commands\Console\Command\CommandOptionsExample</item>
            </argument>
        </arguments>
    </type>
</config>

What is left is to enable the module and delete the cache. After these few commands, you can test the new commands. 

bin/magento module:enable SyncIt_Commands
bin/magento setup:upgrade
bin/magento cache:clean

Special Cases (Question Helper)

Question Helper provides features to ask users for more information. In the following examples, we will be introduced to several methods of this class.

Ask the User for Confirmation

When you have a case where a user's confirmation is required before executing it, it is easiest to use a single ask() method that needs InputInterface, OutputInterface, and ConfirmationQuestion arguments.

use Symfony\Component\Console\Question\ConfirmationQuestion;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    $helper = $this->getHelper('question');
    $question = new ConfirmationQuestion('Continue with this action?', false);

    if (!$helper->ask($input, $output, $question)) {
        return;
    }
}

In this case, the user will be asked the question "Continue with this action?", and they will be able to answer yes or no (in our case true or false). Also, the second parameter of the constructor of ConfirmationQuestion class is the default value, if the user does not enter any valid input.

Ask the User for More Information

Let's say we need more than a user's confirmation with a simple answer. In that case, we will use the Question class. Of course in this example, we also use the ask() method. The Question class first argument is a question while the second argument represents the default value if they leave it empty.

use Symfony\Component\Console\Question\Question;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $question = new Question('Please enter your name? ', 'User');

    $bundleName = $helper->ask($input, $output, $question);
}

A Set of Predefined Answers (List of Answers)

In case you need to provide the user with a set of predefined answers, you can use the ChoiceQuestion class whose first argument is the question to ask the user, the second argument is the list of available choices, and the last argument is the default answer.

If the user enters an invalid string, an error message will be displayed, after which the user will be asked again.

use Symfony\Component\Console\Question\ChoiceQuestion;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $helper = $this->getHelper('question');
    $question = new ChoiceQuestion(
        'Please select your favorite programming language (default is PHP)',
        ['PHP', 'C#', 'Java'],
        0
    );
    $question->setErrorMessage('Programming language %s is invalid.');

    $favoritePL = $helper->ask($input, $output, $question);
    $output->writeln('Your favorite programming language is: '.$favoritePL);

    // ... do something with the favorite programming language
}

Multiple Choices

We will now extend the previous case so that more programming languages can be selected. The ChoiceQuestion class provides us with this functionality, which is disabled by default, and we need to enable this setMultiselect(). The user enters comma-separated values.

use Symfony\Component\Console\Question\ChoiceQuestion;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
   // ...
   $helper = $this->getHelper('question');
   $question = new ChoiceQuestion(
      'Please select your favorite programming language (defaults are PHP and C#)',
      ['PHP', 'C#', 'Java'],
      '0,1'
   );
   $question->setMultiselect(true);

   $favoritePL = $helper->ask($input, $output, $question);
   $output->writeln('Your favorite programming language are: ' . implode(', ',  $favoritePL));
}

Autocompletion

With this functionality, we will greatly facilitate the user to use our command by defining potential answers to a given question. We pass potential answers as an array to the setAutocompleterValues() method.

use Symfony\Component\Console\Question\Question;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $helper = $this->getHelper('question');
    $posts = ['SyncIt_Post01', 'SyncIt_Post02', 'SyncIt_Post03'];
    $question = new Question('Please enter the name of a post', 'SyncIt_Post');
    $question->setAutocompleterValues($posts);
    $postName = $helper->ask($input, $output, $question);
}

Progress Bar

If the command is executed for a long period of time, it is useful for the user to preview the progress information.

Version One (with ProgressBar)

To view progress details, use the ProgressBar class, which we pass the total number of units to. As the command progresses, we increment the units (forward) with command advance().

use Symfony\Component\Console\Helper\ProgressBar;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
        $progressBar = new ProgressBar($output, 100);
        $progressBar->start();
        $i = 0;
        while ($i++ < 100) {
            $progressBar->advance();
            sleep(1);
        }
        $progressBar->finish();
}

Version Two (with SymfonyStyle)

We can also use the progress bar via the SymfonyStyle class.

use Symfony\Component\Console\Style\SymfonyStyle;

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
        $io = new SymfonyStyle($input, $output);
        $io->progressStart(100);
        for ($i = 0; $i <  100; $i++) {
            $io->progressAdvance();
            sleep(1);
        }
        $io->progressFinish();
}

 

Enjoyed the article? Spread the word to your friends and follow our blog for more interesting posts.

This entry was posted in Magento 2 and tagged Web Development, SyncIt Group, Web, Magento 2 Development, Backend Development, CLI Commands, Custom CLI Commands, Command Name, Command Arguments, Command Options, Command Classes, Progress Bar on March 13, 2020 by Vladimir Đurović, Backend Developer .