Creating shell script for Magento2

Shell scripts are a good way to quickly run maintenance commands in Magento. In Magento2 shell scripts are executed through the command

php bin/magento <command name> <command parameters>

Background

The Magento CLI is defined in class Magento\Framework\Console\Cli which is a Symphony Application. The application starts running from the run command defined in Symphony application that performs the initial configuration, like setting the Input to console input and the output to the console. Once the initialisation is complete the application doRun() method is called. This method performs the following steps:

  • Check if the version argument was passed to display the version information.
  • If the version information was not requested, the script retrieves the command name.
  • Checks if the help argument was passed and if so it sets the command name to ‘help’.
  • If the help information was not requested and no command was passed, the command name is set to the application default command, which in Magento2 is to list the commands available.
  • Next it looks up the command within the list of registered commands.
  • Finally it executes the command and returns it exit code.

Let’s get coding

When writing extensions that interact with categories, it is always a good idea to run the Catalog tests supplied by the Magento test framework to ensure that no Magento functionality was broken. The drawback of running these tests is that it will create a lot of test categories and products which hinder the performance of the system, making it sometimes impossible to continue developing and testing.
In this article we will build a command line tool that cleans up the test categories that are generated by the testing framework. So it is easier to re-run the tests during development without the need to restore the database. For the purpose of this article the command for calling the script will be catalog:category:clean, that is our command will be called when the following console command is executed

php bin/magento catalog:category:clean

1. Creating Class for Command

The command is handled through a class that extends the Symphony Command class. Once we create the class, the class needs to be configured and bound to command catalog:category:clean. This is done by setting the class name property to our command and call the Symphony Command configure() method.

class CatalogCleanCommand extends Command
{
    protected function configure()
    {
        $this->setName('catalog:category:clean')
            ->setDescription('Cleans the catalog from extra categories (ending with more than 4 numbers');
        parent::configure();
    }
}

In this article we place the class under the namespace Clounce\Commands\Console\Command.

2. Register Class with the CLI tool

When we call the command to clean the categories is called the doRun of the CLI application will fetch the class that needs to execute the command. This is handled by registering our class within the di.xml file as follows:

// di.xml
<type name="Magento\Framework\Console\CommandList">
    <arguments>
        <argument name="commands" xsi:type="array">
            <item name="catalogCategoryCleanCommand" xsi:type="object">Clounce\Commands\Console\Command\CatalogCleanCommand</item>
 
        </argument>
    </arguments>
</type>

Note that the item name is an arbitrary name, however it is a Magento best practice to name the command item the same way as the command name without the colon separators and appending the work ‘Command’ at the end.

2.1 Check that the Class was properly registered

Now that we have created our command class and we have registered it with the di.xml, we can check that the command was registered. To do this from the command line run the command

php bin/magento

This will load the list of commands that are available and our command should show in the list if all was properly set.

Listing of CLI commands

Figure 1: Listing of CLI commands

Figure 1, shows our new command in the list with the description that was specified. Note that the word before the first colon was for grouping. This is done automatically by the Symphony Application class.

3. Handling the command

Running the command right now, returns the error in Figure 2.

Missing execute() function

Figure 2: Missing execute() function

3.1 Initialising the Object Manager

Before we can do anything useful with our command, we need to initialise the object manager. This is the base class that is used in Magento to initialise classes. For the purpose of the article the object manager will be initialised with the Admin scope.

/**
 * Constructor
 *
 * @param ObjectManagerFactory $objectManagerFactory
 */
public function __construct(
    ObjectManagerFactory $objectManagerFactory
){
    $params = $_SERVER;
    $params[StoreManager::PARAM_RUN_CODE] = 'admin';
    $params[StoreManager::PARAM_RUN_TYPE] = 'store';
    $this->objectManager = $objectManagerFactory->create($params);
    parent::__construct();
}

3.2 Defining the command execution

Now that we have the object manager available the command can be implemented. First override the execute() method, and output some information to the console so users know that the command was found and started executing.

/**
 * @param InputInterface $input
 * @param OutputInterface $output
 * @throws \Magento\Framework\Exception\LocalizedException
 */
protected function execute(InputInterface $input, OutputInterface $output)
{
    $output->writeln('<info>Starting Category Cleanup</info>');
}

It is interesting to note that the writeln() command can interpret some tags to style the output. According to the site Symfony2 Console component, by example Symfony2 Console component, by example | Output the writeln() command provides 4 tags:

  • info – changes the enclosed text to green colour
  • comment – changes the enclosed text to yellow
  • question – shows the enclosed text in black colour on a cyan background
  • error – the text is displayed on a red background with white text colour
Console output tags

Figure 3: Console output tags

3.2.1 Obtaining the Category Collection

The example in this article, will clean categories that are created by the test framework. To obtain the categories, the category collection needs to be created. Using the object manager we obtain the category collection as follows.

/** @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory */
$categoryCollectionFactory = $this->objectManager->get('Magento\Catalog\Model\ResourceModel\Category\CollectionFactory');
 
/** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */
$categoryCollection = $categoryCollectionFactory->create();
$categoryCollection->addAttributeToSelect('name');

Note that we have added the ‘name’ attribute to the collection. In Magento 2 EAV collections do not automatically add custom attributes to the collection.

3.2.2 Deleting test Categories

Once the category collection is obtained the test categories can be identified and deleted. From a quick analysis of the test data created, the categories created by the test framework all end up with a sequence of numbers which is normally 8 digits or more long. For simplicity, our command will delete any categories that have at least 4 characters at the end.

/** @var \Magento\Catalog\Model\Category $category */
foreach ($categoryCollection as $category) {
    if (preg_match('/\\d{4,}$/', $category->getName()) == 1) {
        $output->writeln('<comment>Deleting Cateogry with Name: "'. $category->getName() . '"</comment>');
        $category->delete();
    }
}

Note that although our collection has only the attribute name defined, it still returns a category object. However the category object will have only the information in the catalog_category_entity table and the name attribute. This is enough to allow the category to be deleted as the category ID is defined.

When we try to run the code, an error will show up that the Delete operation is not permitted. This error occurs as the category model requires that delete operations are only performed from secure areas.

Operation forbidden

Figure 4: Operation forbidden

3.2.3 Setting Secure Area

Looking at the RemoveAction functionality, one notices that a check is performed to ensure that the action was called from a secure area. As it is our intention to delete the categories from the command line, we need to set the script to run in a secure area. This is done by setting the isSecureArea in the registry.

/**
 * @var \Magento\Framework\Registry
 */
$registry = $this->objectManager->get('\Magento\Framework\Registry');
$registry->register('isSecureArea', true);

Now that the script has been tagged as secure, the command can be executed and the categories get deleted.

Code execution

Figure 5: Code execution

Full class code

<?php
/**
 * Copyright © 2015 Clounce. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Clounce\Commands\Console\Command;
 
use Magento\Framework\App\ObjectManager\ConfigLoader;
use Magento\Framework\App\ObjectManagerFactory;
use Magento\Framework\App\State;
use Magento\Store\Model\StoreManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
 
class CatalogCleanCommand extends Command
{
    /**
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $objectManager;
 
    /**
     * Constructor
     *
     * @param ObjectManagerFactory $objectManagerFactory
     */
    public function __construct(
        ObjectManagerFactory $objectManagerFactory
    ){
        $params = $_SERVER;
        $params[StoreManager::PARAM_RUN_CODE] = 'admin';
        $params[StoreManager::PARAM_RUN_TYPE] = 'store';
        $this->objectManager = $objectManagerFactory->create($params);
        parent::__construct();
    }
 
    protected function configure()
    {
        $this->setName('catalog:category:clean')
            ->setDescription('Cleans the catalog from extra categories (ending with more than 4 numbers');
        parent::configure();
    }
 
    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @throws \Magento\Framework\Exception\LocalizedException
     * @return null|int null or 0 if everything went fine, or an error code
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('<info>Starting Category Cleanup</info>');
 
        /**
         * @var \Magento\Framework\Registry
         */
        $registry = $this->objectManager->get('\Magento\Framework\Registry');
        $registry->register('isSecureArea', true);
 
        /** @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory */
        $categoryCollectionFactory = $this->objectManager->get('Magento\Catalog\Model\ResourceModel\Category\CollectionFactory');
 
        /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */
        $categoryCollection = $categoryCollectionFactory->create();
        $categoryCollection->addAttributeToSelect('name');
 
        /** @var \Magento\Catalog\Model\Category $category */
        foreach ($categoryCollection as $category) {
            if (preg_match('/\\d{4,}$/', $category->getName()) == 1) {
                $output->writeln('<comment>Deleting Category with Name: "'. $category->getName() . '"</comment>');
                $category->delete();
            }
        }
 
        $output->writeln('<info>Categories Cleaned</info>');
 
        return 0;
    }
}

References