Console commands
A module can add its own commands to bin/magento. They are handy for running things not tied to a page: an import, a recalculation, a data check. A command is a plain class based on the Symfony Console component, which other frameworks use as well.
Where a command lives
Command classes go into the module's Console/Command folder and are registered in etc/di.xml. There is no generator — the file is created by hand.
app/code/Acme/Hello/
├── Console/
│ └── Command/
│ └── CountReviews.php new file
└── etc/
└── di.xml changedThe command class
// app/code/Acme/Hello/Console/Command/CountReviews.php
namespace Acme\Hello\Console\Command;
use Acme\Hello\Model\ResourceModel\Review\CollectionFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CountReviews extends Command
{
public function __construct(private CollectionFactory $collectionFactory)
{
parent::__construct();
}
protected function configure(): void
{
$this->setName('hello:reviews:count')
->setDescription('Shows the number of reviews');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$count = $this->collectionFactory->create()->getSize();
$output->writeln("<info>Reviews: $count</info>");
return Command::SUCCESS;
}
}The configure() method sets the name and the description; the execute() method does the work and returns an exit code: Command::SUCCESS or Command::FAILURE. By convention the name starts with the module name. The constructor must call parent::__construct().
Registration
A command is added to the command list through an array argument in di.xml:
<!-- app/code/Acme/Hello/etc/di.xml -->
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="hello_reviews_count" xsi:type="object">Acme\Hello\Console\Command\CountReviews</item>
</argument>
</arguments>
</type>This is the technique from the arguments lesson: a module appends its item to an array of a core class.
Running
bin/magento cache:clean
bin/magento hello:reviews:countThe command prints Reviews: 2. It also shows up in bin/magento list in the hello group.
Arguments and options
An argument is a value after the command name; an option is a named key with two dashes. They are declared in configure():
// app/code/Acme/Hello/Console/Command/CountReviews.php
use Symfony\Component\Console\Input\InputOption;
protected function configure(): void
{
$this->setName('hello:reviews:count')
->setDescription('Shows the number of reviews')
->addOption('min-rating', null, InputOption::VALUE_REQUIRED, 'Minimum rating', 1);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$minRating = (int) $input->getOption('min-rating');
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('rating', ['gteq' => $minRating]);
$output->writeln('<info>Reviews: ' . $collection->getSize() . '</info>');
return Command::SUCCESS;
}bin/magento hello:reviews:count --min-rating=5The command counts only reviews rated five. A required unnamed value is declared with addArgument('file', InputArgument::REQUIRED, 'Description') and read with $input->getArgument('file').
Styling the output
| Tag | What it looks like |
|---|---|
info | Green text |
comment | Yellow text |
error | White text on a red background |
Tags are written right in the string, as in the example above.
Common mistake
Magento creates the objects of all commands on every bin/magento run, even when another command is called. A heavy dependency in the constructor — a session, the product repository — slows down every command and sometimes breaks setup:install while the database is still empty. Substitute such dependencies with a proxy in di.xml, as shown in the arguments lesson: the object is created only on first use.