Add new console command in Magento 2

Remember that time when you wanted to create a Magento shell or php script to perform some action, but didn’t know where to put it?
Remember the /shell folder in root Magento installation (or root folder, for that matter)?
Remember how it used to have, like, 50 shell and php scripts all mixed up, and you get a headache just by looking at them?
Pepperidge Farm remembers.

Those were good old times, and like all good times, they have now come to pass. There’s a new kid on the block, more powerful and way cooler, and the best part is – it’s integrated into Magento 2.

We’re talking, of course, about a new feature of Magento 2, its console component.
Root folder now contains a new directory – /bin, and in it a ‘magento’ script, used to start the console component. By typing bin/magento in terminal, we receive a number of commands we can run like:

  • create an admin user (admin:user:create)
  • clear, disable, enable cache (cache:clean, cache:enable, cache:disable, etc.)
  • run cron (cron:run)
  • enable and disable modules (module:enable, module:disable)
  • check indexer status, and reindex if needed (indexer:info, indexer:reindex, etc.)
  • and many more

This is a major improvement when compared to Magento 1, and like everything Magento does (although this one was borrowed from Symfony), it’s highly customizable and extendable. We can add our own commands fairly easily, and that’s exactly what we’re going to do in this article.

We won’t go into details of creating a new module in Magento 2, as there are already many tutorials you can follow.

In order to add a new command, we only need to perform a few steps. Firstly, create a di.xml file (if you do not have one already) in etc folder in your module, and put this in:

<?xml version="1.0"?>
	<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="hello_world" xsi:type="object">Inchoo\Console\Console\Command\HelloWorldCommand</item>
				</argument>
			</arguments>
		</type>
	</config>

We have added a new entry in di.xml file. What does it do? If we check the type tag, and its name, we see it is pointing us to "Magento\Framework\Console\CommandList". Also, it has some arguments named “commands”, and an item tag. It looks weird. Let’s go to the CommandList class, and see what the fuss is all about:

class CommandList implements CommandListInterface
{
    /**
     * @var string[]
     */
    protected $commands;
 
    /**
     * Constructor
     *
     * @param array $commands
     */
    public function __construct(array $commands = [])
    {
        $this->commands = $commands;
    }
 
    /**
     * {@inheritdoc}
     */
    public function getCommands()
    {
        return $this->commands;
    }
}

Not much going on here. Simple class, where constructor is receiving a commands array as a parameter, and it has a getter method, and a protected variable, and.. umm.. Wait, constructor is receiving a commands array? That looks familiar. Our di.xml has something mentioning an array called commands. Let’s see it again:

<arguments>
	<argument name="commands" xsi:type="array">
		<item name="hello_world" xsi:type="object">Inchoo\Console\Console\Command\HelloWorldCommand</item>
	</argument>
</arguments>

Interesting. Our di.xmls argument tag is indeed called commands, and it’s type is set to “array”. It seems like Magento is using some kind of magic to send our argument to CommandList class. <argument> has an <item>, and this must be the thing it is sending.
If we check Magento documentation (yeah, you read that right):

“Arguments are injected into a class instance during its creation. Argument names must correspond to constructor parameters of the configured class.”

We see that this is actually dependency injection at work – injecting our Command object into an array of all commands. Neat.

Let’s see this HelloWorldCommand class (it is located in Console directory in our module).

class HelloWorldCommand extends Command
{
 
    protected function configure()
    {
        $this->setName('inchoo:hello_world')->setDescription('Prints hello world.');
    }
 
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Hello World!');
    }
 
}

There are only two methods here: configure and execute. We inherit them from parent class (Command), and they allow us to set up our command.

Configure method is used to set initial configuration to the command like: name, description, command line arguments etc.
Execute method is the place where you write your logic. In this example we’ve just printed “Hello World!” to the screen, but hopefully you’ll have more imagination.

To see the command in action, invoke it with bin/magento like this:

bin/magento inchoo:hello_world

and you would be greeted by your command line. What a feeling. đŸ™‚

To make your life easier, we’ve prepared a composer repository you can install, to try the command without having to type everything out.

Here are the instructions:

add our module as a dependency:

composer config repositories.inchoo_console vcs git@bitbucket.org:lurajcevi/inchoo_console.git

ask Composer to download it:

composer require inchoo/console:dev-master

if Composer asks you for a set of credentials, please follow these instructions.

When composer does his magic, we can enable our module:

bin/magento module:enable Inchoo_Console

and later

bin/magento setup:upgrade

Our module has been enabled, and we can use it. Since the idea was to add a new command to console application, let’s see if it worked. Call bin/magento again.

Hopefully, you’ll see the following command (among all others):

...
inchoo
inchoo:hello_world Prints hello world.
...

Let’s invoke it:

bin/magento inchoo:hello_world

and response is, as expected:

Hello World!

However, if this or anything else regarding Magento development confuses you, we will gladly check out your site and offer technical insights on what to improve based on our detailed custom report. Feel free to get in touch!


About Luka Rajcevic

Backend Developer

Big Tolkien and LOTR fan who plays guitar for his pleasure and relaxes while listening to jazz, blues or classical music.

Read more posts by Luka / Visit Luka's profile

11 comments

  1. Follow below steps to access command line argument.

    1] use below class in command file.
    use Symfony\Component\Console\Input\InputArgument;

    2] create below constant

    const YOUR_ARGUMENT = ‘test_value’;

    3] In configure method write below code :

    $this->setName(‘inchoo:hello_world’)
    ->setDescription(‘Prints hello world.’)
    ->setDefinition([
    new InputArgument(
    self::YOUR_ARGUMENT, //param_name
    InputArgument::OPTIONAL,
    ‘Name of file to input’ // description
    ),

    ]);

    parent::configure();

    4] To get command line parameter use below code in execute method

    $argValue = $input->getArgument(self::YOUR_ARGUMENT);

  2. damn formatting thing – it broke my previous comment and there is no preview on ot – lets see if this one works

    HelloWorldCommand.php


    setName('inchoo:hello_world')->setDescription('Prints hello world.');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
    $output->writeln('Hello World!');
    }

    }

    di.xml

    Inchoo\Helloworld\Console\Command\HelloWorldCommand

    HelloWorldCommand.php

    setName('inchoo:hello_world')->setDescription('Prints hello world.');
        }
    
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $output->writeln('Hello World!');
        }
    
    }
    

    di.xml

    
    
        
            
                
                    Inchoo\Helloworld\Console\Command\HelloWorldCommand
                
            
        
    
    
  3. To be honest I haven’t seen such an unfinished and misleading tutorial for a while. If any newbie would try to follow it, he/she would never have it working. Lemme bring my 5 cents here:

    1) you are creating a di.xml with this element

    <item name="hello_world" xsi:type="object">Inchoo\Console\Console\Command\HelloWorldCommand</item>

    and then later you are referring to it as to

    <item name="hello_world" xsi:type="object">Inchoo\Nassau\Console\Command\HelloWorldCommand</item>

    are you kidding me? with all the respect but that is such a stupid typo/ copy paste? what was that? somebody should figure out on its own what the hell the module name should be? more attention to such things please, we have enough BAD tutorials.. but it is not that bad.. the worst is that you are mentioning a command file

    “Let’s see this HelloWorldCommand class (it is located in Console directory in our module).”

    where it came from? was it brought in by Santa maybe? If that would have been a part of module creation which you have mentioned, it would be understandable you had not explained when you created it but it had not! it is a part of THIS tutorial, so you had to explain at which step it is being created since you are referring to it as to “this HelloWorldCommand class”.

    Also it is NOT in Console directory, it should be in Console/Command directory – so even its location is wrong

    but again it is not the worst thing here.. have you even tested this command? it wont work because
    a) it has no namespace – how the hell it would be reached by di?
    b) it refers to Command, InputInterface and OutputInterface – it would fail because such classes would not be found…

    oh my God I cant believe I am writing this.. I don’t ususally blame other peoples tutorials but here I had to. Really BAD.

    SO to make a long story short – for all folks who would try it:

    1) Assuming module name you create is Helloworld and it is in Inchoo namespace, so you would need to create a file Inchoo\Helloworld\Console\Command\HelloWorldCommand.php with this content

    
    setName('inchoo:hello_world')->setDescription('Prints hello world.');
        }
    
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $output->writeln('Hello World!');
        }
    }
    

    and your Inchoo\Helloworld\etc\di.xml should look like

    
    
    
        
            
                
                    Inchoo\Helloworld\Console\Command\HelloWorldCommand
                
            
        
    
    

    and then yes, it would work..

    author , please update this tutorial and test it before publishing it. thank you

    1. Hi Oleksandr,

      1. This indeed was a typo
      2. “Let’s see this HelloWorldCommand class”
      – class has been mentioned in the XML file and the assumption is people following the tutorial read the files they’re copy-pasting (or using in their own code)
      3. It has no namespaces for formatting reasons, to be more readable and succinct
      4. InputInterface and OutputInterface do not exist – check point 3.

      Thanks for the comment. The typo is now fixed. Also, while we’re at it, your comment contains the same code as ours, and does not contribute anything new (it does not have namespaces as well, so it would not work đŸ™‚ ). Check the original repository for more info: https://bitbucket.org/lurajcevi/inchoo_console/src

      Luka

    2. I would have to agree here… Your example isn’t complete (and is still incorrect with regards to the folder location- it should be namespace/module/Console/Command)

      Also, you should really include the namespace as well to make this a fully-working example:

      <?php
      namespace Inchoo\Console\Console\Command;
      use Symfony\Component\Console\Command\Command;
      use Symfony\Component\Console\Input\InputInterface;
      use Symfony\Component\Console\Output\OutputInterface;

  4. Hi, Thank you for the great tut, but I have one question, How to pass param into the command line?
    Ex: bin/magento inchoo:hello_world file_name
    The execute can read file_name then read the content of file_name, the file_name is the CSV file
    Thank you again

    1. Oh, I find out the way use Symfony\Component\Console\Input\InputArgument;

      $options = [
                  new InputArgument(
                      self::FILE_NAME,  //param_name
                      InputArgument::REQUIRED,  //optiosn or required param
                      'Name of file to input'   // description
                  ),
              ];
    2. Hello.
      Could you please describe what is the $options array? How you use it?

      I working on the same issue as you. When I add some argument like bin/magento inchoo:hello_world file_name I see:
      [RuntimeException]
      Too many arguments.
      in console. What did you do for passing argument?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.