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.
See also
- For a step-by-step guide, see: Tutorial: Create a console command.
Table of contents
The custom command class
To implement a console command in TYPO3 extend the
\Symfony\ class.
See also
Console commands in TYPO3 are based on the same technology as commands in Symfony. Find more information about Commands in the Symfony documentation.
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.:
PHP attribute AsCommand
CLI commands can be registered by setting the attribute
\Symfony\ in the command class.
When using this attribute there is no need to register the command in
Services..
<?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;
}
}
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
hiddentotrue. aliases- A list of further names under which the command is available.
To keep a command out of the scheduler, add the attribute
# 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.:
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
Note
Despite using autoconfigure: true the commands
have to be explicitly defined in Configuration/Services.yaml or
use the PHP attribute
#.
Making a command non-schedulable
New in version 14.0
The PHP attribute
# 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. (see
Tag console.command in the Services.yaml).
A command can be set as disabled for the scheduler by using the
# attribute:
<?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
{
// ...
}
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
Attention
It is not recommended to use Extbase repositories in a 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:
<?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;
}
}
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.
See also
See chapter Simulating a frontend request.
A minimal request configuration may be sufficient for generating simple links or using FluidEmail:
<?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;
}
}
Links that the template builds with f:, f:, or
f: and the argument absolute="1" point to the site. Rich
text is different: the ViewHelper
f:format.html
needs the TypoScript lib., which the simulated request does
not load, and throws an exception. Without f:, 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.
Note
Simulating a frontend request in CLI is possible but requires all bootstrapping steps to be manually carried out. While basic functionality, such as link generation, can work with a minimal setup, complex TypoScript-based link modifications, access restrictions, and context-aware rendering require additional configuration and may still behave differently from a real web request.
Create a command with arguments and interaction
Passing arguments
Since a command extends
\Symfony\,
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 add and
add to register them.
This argument can be retrieved with
$input->get, the options with
$input->get, for example:
<?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
}
}
User interaction on the console
You can create a
Symfony console user interface using the
$input
and
$output
parameters of the
execute function:
<?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;
}
}
}
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:::
<?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;
}
}
More about Symfony console commands
- See implementation of existing command controllers in the Core:
typo3/sysext/*/ Classes/ Command - Symfony Command Documentation
- Symfony Commands: Console Input (Arguments & Options)