magento2 console command module

Hi Again! I’m back with another #QuickOne tutorial. This time we will create a magento2 console command module and learn a little about DI.
NOTE: you can download the whole module from here.

Console Commands

For those who are not familiar with magento2 – it has a set of console commands that are based on Symfony framework. If you want to see the list of possible options just open a terminal, go to your magento installation directory and type

php bin/magento

It should display all available commands. As you can see there are some suseful options. But what if you want to add a new one? Do not worry, we will go through it step by step.

Module

First, we have to create a custom module. If you do not know how to do it go to my post about it -> magento2 sample module.

Dependency Injection

To let magento know about our command we need to inject our class using dependency injection. If this does not ring a bell, I recommend checking magento documentation. In short, dependency injection let us inject a class into another class constructor. To do this, we have to create di.xml file.

di.xml

Di.xml is a module configuration file that comes in handy mostly when we want to override/extend some core functionality.

etc/di.xml

<?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\CommandListInterface">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="productInfoCommand" xsi:type="object">Ruchlewicz\ConsoleCommand\Console\Command\ProductInfoCommand</item>
            </argument>
        </arguments>
    </type>
</config>

What does it do? type node let us attach to any class in magento. If you go to CommandList.php file

Magento/Framework/Console/CommandList.php

/**
 * Constructor
 *
 * @param array $commands
 */
public function __construct(array $commands = [])
{
    $this->commands = $commands;
}

you can see that the __construct() function has an $commands argument that is an array. So let us go back to our di.xml. As I wrote, in type node we reference to CommandList class, next we enter the arguments node and a concrete argument with name commands – the same name as the variable in the constructor. The last part of our injecting mechanism is to provide a new item, that we want to inject, where name is a unique name of the command, xsi:type is object – because we inject a class, and as the value of the item node we provide a path to our command class.

Other DI features

Woah woah woah, wait a minute! In our di.xml we attach to CommandListInterface but you asked us to check CommandList, what the heck? Yeah, you are right, I need to explain this. This is another “feature” of di.xml. If you open magento global di.xml file that is in magento_root/app/etc/di.xml you can find a line

<preference for="Magento\Framework\Console\CommandListInterface" type="Magento\Framework\Console\CommandList"/>

which tells magento that each time any class will call for CommandListInterface instance in constructor, magento will eventually provide CommandList class 🙂
Summing up, the file we have done injects our class to the CommandList class, so that we will be able to launch it from the console.

ConsoleCommand.php

The last thing we are missing is the actual console command class. Let us create one.

Console/Command/ProductInfoCommand.php

<?php
namespace Ruchlewicz\ConsoleCommand\Console\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\App\State;
use Magento\Framework\App\Area;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Api\SimpleDataObjectConverter;

class ProductInfoCommand extends \Symfony\Component\Console\Command\Command
{
    const PRODUCT_ID_ARGUMENT_CODE = 'product-id';
    const ATTRIBUTE_NAME_OPTION_CODE = 'attribute';
    const ATTRIBUTE_NAME_OPTION_CODE_SHORTCUT = 'a';

    /**
     * @var ProductRepositoryInterface
     */
    private $productRepository;

    /**
     * @var State
     */
    private $state;

    /**
     * SampleCommand constructor.
     * @param ProductRepositoryInterface $productRepository
     * @param State $state
     * @param null $name
     */
    public function __construct(
        ProductRepositoryInterface $productRepository,
        State $state,
        $name = null
    ) {
        parent::__construct($name);
        $this->productRepository = $productRepository;
        $this->state = $state;
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('ruchlewicz:product-info')
            ->setDescription('Get product info');

        $this->addArgument(
            self::PRODUCT_ID_ARGUMENT_CODE,
            InputArgument::REQUIRED,
            'Id of the product'
        );

        $this->addOption(
            self::ATTRIBUTE_NAME_OPTION_CODE,
            self::ATTRIBUTE_NAME_OPTION_CODE_SHORTCUT,
            InputOption::VALUE_REQUIRED,
            'Attribute of the product you want to display'
        );
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $this->state->setAreaCode(Area::AREA_ADMINHTML);
        } catch (LocalizedException $e) {
            // Area code was already set, so we do not have to do anything after catching exception
        }
        $productId = $input->getArgument(self::PRODUCT_ID_ARGUMENT_CODE);
        $output->writeln('<info>Searching for product with id - ' . $productId . '</info>');

        try {
            $product = $this->productRepository->getById($productId);

            if ($attributeCode = $input->getOption(self::ATTRIBUTE_NAME_OPTION_CODE)) {
                $value = $this->getProductAttributeValue($product, $attributeCode);
                $output->writeln('<info>value of ' . $attributeCode . ' - "' .$value . '"</info>');
            } else {
                $values = $this->getProductAllAttributeValues($product);
                $output->writeln('<info>' . $values . '</info>');
            }
        } catch (NoSuchEntityException $e) {
            $output->writeln('<error>' . $e->getMessage() . '</error>');
            return $e->getCode();
        }

        return 0;
    }

    /**
     * @param \Magento\Catalog\Api\Data\ProductInterface $product
     * @param string $attributeCode
     * @return string|null
     */
    private function getProductAttributeValue($product, $attributeCode)
    {
        $objectMethods = $this->getObjectMethods($product);
        $camelCaseAttributeGetter = SimpleDataObjectConverter::snakeCaseToCamelCase('get_' . $attributeCode);

        if (in_array($camelCaseAttributeGetter, $objectMethods)) {
            return $product->{$camelCaseAttributeGetter}();
        }

        return $product->getCustomAttribute($attributeCode);
    }

    /**
     * @param \Magento\Catalog\Api\Data\ProductInterface $product
     * @return string
     */
    private function getProductAllAttributeValues($product)
    {
        $objectMethods = $this->getObjectMethods($product);
        $value = '';

        foreach ($objectMethods as $method) {
            if (strpos($method, 'get') === 0) {
                $value .= $this->parseObjectFunctionValue($product, $method) ?
                    $this->parseObjectFunctionValue($product, $method). "\n\n"
                    : '';
            }
        }

        return $value;
    }

    /**
     * @param \Magento\Catalog\Api\Data\ProductInterface $product
     * @param string $method
     * @return string
     */
    private function parseObjectFunctionValue($product, $method)
    {
        try {
            $returnValue = $product->{$method}();
        } catch (\ArgumentCountError $e) {
            return '';
        }

        if (is_object($returnValue)) {
            $returnValue = get_class($returnValue);
        } elseif (is_array($returnValue)) {
            $returnValue = json_encode($returnValue);
        }

        return $method . ' => ' . $returnValue;
    }

    /**
     * @param \Magento\Catalog\Api\Data\ProductInterface $product
     * @return array
     */
    private function getObjectMethods($product)
    {
        return get_class_methods(get_class($product));
    }
}

Let me cover the most important and required parts.

Extend the Command class

As I wrote earlier, magento2 console feature is based on Symfony framework so our class “needs” to extend the Symfony Command as on line #15. I placed the need word in quote, because it is not fully required but that class already has some functions that we will need, so it is just better this way 😉

Implement configure() function

This one is really required. In this function we need to at least setName() and setDescription(). The first one is basically the name of our command – we will use it when we will execute it from the console. Description is self-explanatory. In the configure() function we can also define arguments addArgument() and options addOption(). As you can see on the example I have added one argument that is required (product id) and one option (optional attribue code) that its value is required. For more options and configurations you can go to symfony documentation.

Implement execute() function

The last part is to implement the execute() function. This is the function that will be launched after executing the command from the console. As you can see on the example, it has two arguments – $input and $output. With the first one we have access to everything that was typed in the command line, that includes options and arguments. With the second we can provide some output, for example display some informations.

Module functionality

To test the functionality you have to enable your module first.

cd magento_root_dir
php bin/magento setup:upgrade

Then you can check if you did everything right

php bin/magento

if yes, your command should appear on the list.
Go ahead, test it 😉

php bin/magento ruchlewicz:product-info 1 -a name
php bin/magento ruchlewicz:product-info 1

The module functionality (getting product information) has nothing to do with the console commands – it is just an example. You can treat it as a base, so modify it and experiment with it. As it is not part of the post I will not describe what I did there and why. If you are curious and have any questions feel free to contact me directly 🙂
Thanks!

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.