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.

EXT:my_extension/Classes/Command/DoSomethingCommand.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:dosomething',
  description: 'A command that does nothing and always succeeds.',
  aliases: ['myextension:dosomethingalias'],
)]
class DoSomethingCommand extends Command
{
  protected function configure(): void
  {
    $this->setHelp('This command does nothing. It always succeeds.');
  }

  protected function execute(InputInterface $input, OutputInterface $output): int
  {
    $io = new SymfonyStyle($input, $output);
    $io->info('Command needs to be implemented. ');
    return Command::SUCCESS;
  }
}
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 .
aliases
A list of further names under which the command is available.

To keep a command out of the scheduler, add the attribute #[AsNonSchedulableCommand] as well. See Making a command non-schedulable.

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 (excerpt)
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'myextension:dosomething'
        description: 'A command that does nothing and always succeeds.'
        # To hide it from the scheduler:
        schedulable: false
      # Also an alias for the command can be configured
      - name: console.command
        command: 'myextension:dosomethingalias'
        alias: true
Copied!

Making a command non-schedulable 

New in version 14.0

The PHP attribute #[AsNonSchedulableCommand] has been introduced to mark a command as non-schedulable.

To provide support for both TYPO3 v14 and v13 continue to register your command in Services.yaml (see Tag console.command in the Services.yaml).

A command can be set as disabled for the scheduler by using the #[AsNonSchedulableCommand] attribute:

EXT:my_extension/Classes/Commands/MyImportCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Commands;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand('myextension:import', 'Import data from external source')]
#[AsNonSchedulableCommand]
final class MyImportCommand extends Command
{
  // ...
}
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. On the command line, this is the administrator _cli_. TYPO3 creates this user when it does not exist yet. The following command prints the user it runs as:

packages/my_extension/Classes/Command/ShowBackendUserCommand.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:showbackenduser',
  description: 'Shows the backend user the command runs as.',
)]
final class ShowBackendUserCommand extends Command
{
  protected function execute(InputInterface $input, OutputInterface $output): int
  {
    Bootstrap::initializeBackendAuthentication();
    $backendUser = $GLOBALS['BE_USER'];

    $output->writeln(sprintf(
      'Running as backend user "%s"%s.',
      $backendUser->getUserName(),
      $backendUser->isAdmin() ? ', an administrator' : '',
    ));
    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/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\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
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,
    private readonly TemplatedEmailFactory $templatedEmailFactory,
  ) {
    parent::__construct();
  }

  protected function execute(InputInterface $input, OutputInterface $output): int
  {
    // The site has to have a fully qualified domain name
    $site = $this->siteFinder->getSiteByPageId(1);
    // Simulate a frontend request to the site, so that links and resources
    // in the email point to the site
    $request = (new ServerRequest($site->getBase()))
      ->withAttribute(
        'applicationType',
        SystemEnvironmentBuilder::REQUESTTYPE_FE,
      )
      ->withAttribute('site', $site);
    // FluidEmail needs normalizedParams on the request, see
    // https://forge.typo3.org/issues/111023
    $request = $request->withAttribute(
      'normalizedParams',
      NormalizedParams::createFromRequest($request),
    );
    // Commands use create(), which reads the global mail configuration
    $email = $this->templatedEmailFactory->create($request);
    // Set receiver etc
    $this->mailer->send($email);
    return Command::SUCCESS;
  }
}
Copied!

Links that the template builds with f:link.page, f:uri.page, or f:link.typolink and the argument absolute="1" point to the site. Rich text is different: the ViewHelper f:format.html needs the TypoScript lib.parseFunc_RTE, which the simulated request does not load, and throws an exception. Without f:format.html, the rich text stays unprocessed, and t3:// links in it stay unresolved. See parseFunc for what the processing does. The missing API is tracked in Forge issue #111022, absolute links in rich text of emails in Forge issue #102362.

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/CreateWizardCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use MyVendor\MyExtension\Exception\InvalidWizardException;
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 TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand(
  name: 'myextension:createwizard',
)]
#[AsNonSchedulableCommand]
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.

The following command gets two services through its constructor: the requester from HTTP requests to external sources, which fetches a fact about cats, and a logger. When the request fails, the command logs the error and returns Command::FAILURE :

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use MyVendor\MyExtension\Http\MeowInformationRequester;
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;

#[AsCommand(
  name: 'myextension:meowinformation',
  description: 'Prints a random fact about cats.',
)]
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
  {
    try {
      $fact = $this->requester->request();
    } catch (\RuntimeException|\JsonException $e) {
      $this->logger->error('No cat fact: ' . $e->getMessage());
      return Command::FAILURE;
    }
    $output->writeln($fact);
    return Command::SUCCESS;
  }
}
Copied!

More about Symfony console commands