Writing custom commands

TYPO3 uses the Symfony Console component to define and execute command-line interface (CLI) commands. Custom commands allow extension developers to provide their own functionality for use on the command line or in the TYPO3 scheduler.

The custom command class

To implement a console command in TYPO3 extend the \Symfony\Component\Console\Command\Command class.

Console command registration

There are two ways that a console command can be registered: you can use the PHP Attribute AsCommand or register the command in your Services.yaml:

PHP attribute AsCommand

CLI commands can be registered by setting the attribute \Symfony\Component\Console\Attribute\AsCommand in the command class. When using this attribute there is no need to register the command in Services.yaml.

#[AsCommand(
    name: 'examples:dosomething',
    description: 'A command that does nothing and always succeeds.',
    aliases: ['examples:dosomethingalias'],
)]
Copied!

The following parameters are available:

name
The name under which the command is available.
description
Gives a short description. It will be displayed in the list of commands and the help information for the command.
hidden
Hide the command from the command list by setting hidden to true.
alias
A command can be made available under a different name. Set to true if your command name is an alias.

If you want to set a command as non-schedulable it has to be registered via tag not attribute.

Tag console.command in the Services.yaml

You can register the command in Configuration/Services.yaml by adding the service definition of your class as a tag console.command:

packages/my_extension/Configuration/Services.yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and always succeeds.'
      # Also an alias for the command can be configured
      - name: console.command
        command: 'examples:dosomethingalias'
        alias: true
Copied!

Making a command non-schedulable

A command can be set as disabled for the scheduler by setting schedulable to false. This can only be done when registering the command via tag and not via attribute:

packages/my_extension/Configuration/Services.yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and cannot be scheduled.'
        schedulable: false
Copied!

Context of a command: No request, no site, no user

Commands are called from the console / command line and not through a web request. Therefore, when the code of your custom command is run by default there is no ServerRequest available, no backend or frontend user logged in and a request is called without context of a site or page.

For that reason Site Settings, TypoScript and TSconfig are not loaded by default, Extbase repositories cannot be used without taking precautions and there are many more limitations.

Extbase limitations in CLI context

Extbase relies on frontend TypoScript, and features such as request-based TypoScript conditions may not behave as expected.

Instead, use the Query Builder or DataHandler when implementing custom commands.

Using the DataHandler in CLI commands

When using the DataHandler in a CLI command, backend user authentication is required. For more information see: Using the DataHandler in a Symfony command.

Initialize backend user

A backend user can be initialized inside the execute() method as follows:

packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class DoBackendRelatedThingsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();
        // Do backend related stuff

        return Command::SUCCESS;
    }
}
Copied!

This is necessary when using the DataHandler or other backend permission-handling-related tasks.

Simulating a frontend request in TYPO3 Commands

Executing a TYPO3 command in the CLI does not trigger a frontend (web) request. This means that several request attributes required for link generation via Fluid or TypoScript are missing by default. While setting the site attribute in the request is a first step, it does not fully replicate the frontend behavior.

A minimal request configuration may be sufficient for generating simple links or using FluidEmail:

packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.php
<?php

declare(strict_types=1);

namespace T3docs\Examples\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Mail\FluidEmail;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Site\SiteFinder;

#[AsCommand(
    name: 'myextension:sendmail',
)]
class SendFluidMailCommand extends Command
{
    public function __construct(
        private readonly SiteFinder $siteFinder,
        private readonly MailerInterface $mailer,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();

        // The site has to have a fully qualified domain name
        $site = $this->siteFinder->getSiteByPageId(1);
        $request = (new ServerRequest())
            ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
            ->withAttribute('site', $site);
        $GLOBALS['TYPO3_REQUEST'] = $request;
        // Send some mails with FluidEmail
        $email = new FluidEmail();
        $email->setRequest($request);
        // Set receiver etc
        $this->mailer->send($email);
        return Command::SUCCESS;
    }
}
Copied!

Create a command with arguments and interaction

Passing arguments

Since a command extends \Symfony\Component\Console\Command\Command, it is possible to define arguments (ordered) and options (unordered) using the Symfony command API. This is explained in depth on the following Symfony Documentation page:

Both arguments and properties can be registered in a command implementation by overriding the configure() method. You can call methods addArgument() and addOption() to register them.

This argument can be retrieved with $input->getArgument(), the options with $input->getOption(), for example:

packages/my_extension/Classes/Command/SendFluidMailCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use T3docs\Examples\Exception\InvalidWizardException;

#[AsCommand(
    name: 'myextension:createwizard',
)]
final class CreateWizardCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->setHelp('This command accepts arguments')
            ->addArgument(
                'wizardName',
                InputArgument::OPTIONAL,
                'The wizard\'s name',
            )
            ->addOption(
                'brute-force',
                'b',
                InputOption::VALUE_NONE,
                'Allow the "Wizard of Oz". You can use --brute-force or -b when running command',
            );
    }
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $wizardName = $input->getArgument('wizardName');
        $bruteForce = (bool)$input->getOption('brute-force');
        try {
            $this->doMagic($io, $wizardName, $bruteForce);
        } catch (InvalidWizardException) {
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }

    private function doMagic(SymfonyStyle $io, mixed $wizardName, bool $bruteForce): void
    {
        // do your magic here
    }
}
Copied!

User interaction on the console

You can create a SymfonyStyle console user interface using the $input and $output parameters of the execute() function:

packages/my_extension/Classes/Command/CrazyCalculatorCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'myextension:crazycalculator',
)]
final class CrazyCalculatorCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $io->title('Welcome to our awesome extension');

        $io->text([
            'We will ask some questions.',
            'Please take your time to answer them.',
        ]);
        do {
            $number = (int)$io->ask(
                'Please enter a number greater 0',
                '42',
            );
        } while ($number <= 0);
        $operation = (string)$io->choice(
            'Chose the desired operation',
            ['squared', 'divided by 0'],
            'squared',
        );
        switch ($operation) {
            case 'squared':
                $io->success(sprintf('%d squared is %d', $number, $number * $number));
                return Command::SUCCESS;
            default:
                $io->error('Operation ' . $operation . 'is not supported. ');
                return Command::FAILURE;
        }
    }
}
Copied!

The $io variable can be used to generate output and prompt for input.

Dependency injection in console commands

You can use dependency injection (DI) in console commands via constructor injection or method injection.

packages/my_extension/Classes/Command/MeowInformationCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use T3docs\Examples\Http\MeowInformationRequester;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class MeowInformationCommand extends Command
{
    public function __construct(
        private readonly MeowInformationRequester $requester,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        if (!$this->requester->isReady()) {
            $this->logger->error('MeowInformationRequester was not ready! ');
            return Command::SUCCESS;
        }
        // Do awesome stuff
        return Command::SUCCESS;
    }
}
Copied!

More about Symfony console commands