Write your own CLI Tools for TYPO3

TYPO3 Developer Days 2023

Frank Berger

A bit about me

  • Frank Berger
  • Head of Engineering at sudhaus7.de, a label of the B-Factor GmbH, member of the code711.de network
  • Started as an Unix Systemadministrator who also develops in 1996
  • Does TYPO3 since 2005

What are CLI tools?

CLI = Command-line Interface

A tool to be used in the context of a shell or commandline

Is often used to manipulate files or data streams (STDIN/STDOUT)

often have parameters to fine-tune behaviour or output

are essential in automating things, for example in CI/CD

CLI in context of Symfony and TYPO3

TYPO3 uses the Symfony Console Component

Usually there is a 'console' script in which 'commands' can be registered to be executed (vendor/bin/typo3 in TYPO3)

the first parameter in that case is the command class to be called

Usually to be used to interact with the PHP application from the command line

(some) existing Commands

(in TYPO3 v11/v12)
  • typo3 list - lists all registered commands
  • typo3 help - displays a general help page, or the help page for a certain command
  • typo3 cache:flush - flushes the caches
  • typo3 cache:warmup - warms up (many) caches
  • typo3 database:updateschema - check the database definitions and update them if needed
  • typo3 install:extensionsetupifpossible - checks for new extensions and set them up

Advantages to write CLI tools

  • Automate tedious tasks
  • Bulk updates
  • Migration in code
  • Offload tasks
  • Performance intensive tasks

Creating your own command

Classes/Command/MytoolCommand.php

declare(strict_types=1);

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MytoolCommand extends Command
{
	protected function configure(): void
	{
		$this->setDescription('that one task I need done regulary')
			 ->setHelp('the help someone might need when using my command');
	}
	protected function execute(InputInterface $input, OutputInterface $output): int
	{
		$output->writeln('Hello World');
		return 0;
	}
}
					

Enable it in the System

Configuration/Services.yaml

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: true

  Talk\Mytool\:
    resource: '../Classes/*'
  Talk\Mytool\Command\MytoolCommand:
    tags:
      - name: 'console.command'
        command: 'my:tool'
        description: 'My first CLI tool'
        schedulable: false
							
Configuration/Services.php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator,
	ContainerBuilder $containerBuilder): void
{
    $services = $containerConfigurator->services();
    $services->defaults()->public()
             ->autowire()->autoconfigure();

    $services->load('Talk\\Mytool\\', __DIR__ . '/../Classes/');
    $services->set(\Talk\Mytool\Command\MytoolCommand::class)
			->tag('console.command', [
        'priority'=>10,
        'command'=>'my:tool',
        'description'=>'My first CLI tool',
        'schedulable'=>false,
    ]);
};
							

Lets try this

$input and $output

$input

						$input->getArgument('theargument');
						$input->getOption('theoption');
					
$output

						$output->writeln($message);
						$output->write($message,OutputInterface::VERBOSITY_VERBOSE);
					

$message can be a string or an iterable

Setting up arguments


protected function configure(): void
{
	$this->addArgument(
		'message',
		InputArgument::OPTIONAL,
		'our message',
		'default value'
	);
}
protected function execute(InputInterface $input, OutputInterface $output):int
{
	$msg = $input->getArgument('message');
}
					

when you use multiple arguments, the order they are defined is important!!

Setting up options


protected function configure(): void {
	$this->addOption(
		'twitterhandle',
		'x',
		InputOption::VALUE_REQUIRED,
		'the twitter handle to use'
	);
}
protected function execute(InputInterface $input, OutputInterface $output):int {
	if($x = $input->getOption('twitterhandle')) {
		$output->writeln($x);
	}
}
					

be careful with InputOption::VALUE_OPTIONAL!!

Output Formatters / Helpers

There are some helpers which can attach to OutputInterface to format data or to enhance the output, for example Table or ProgressBar

some good (best?) practices and pitfalls when working with TYPO3

Encapsulate Business logic in Services, and use LoggerAwareInterface and LoggerAwareTrait in those to communicate (free mapping to -v -vv -vvv)


class MyService implements LoggerAwareInterface {
	use LoggerAwareTrait;
	public function doSomething() {
        $this->logger->error( 'there was an error');
        $this->logger->warning('there was a warning');
        $this->logger->info('this is some information -vv');
        $this->logger->debug('this is debugging -vvv');
	}
}
					

// function execute
$logger = new ConsoleLogger($output);
$service = GeneralUtility::makeInstance(MyService::class);
$service->setLogger($logger);
$service->doSomething();
					

The Site object and the SiteFinder service is your friend

There is no $TSFE and no $REQUEST available. The Site object's router will create you frontend-links even in CLI mode

There is no PageTS or Frontend TS available.
If you need configurable values in CLI, consider putting them in your site/config.yaml
or $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']

Example using Site and SiteFinder


$site = GeneralUtility::makeInstance(SiteFinder::class)
		->getSiteByPageId($pageID);
$uri  = $site->getRouter()->generateUri($pageID, [
	'tx_myext'=>[
		'controller'=>'MyController',
		'action'=>'detail',
		'uid'=>$contentID,
		'_language'=>0
	]
], 'c123');
					

It will create a fully qualified URL including routeEnhancers

you can use some Extbase features

Generally speaking you can use Repositories and Models, but you should test it very well.

If you create or modify Models, persist them with
PersistenceManager directly!

Do small incremental updates,
otherwise the console seems 'frozen'

Other than that -> no $TSFE means no Extbase Context.

What about Fluid?

You can use Fluid (StandaloneView), but some ViewHelpers need a Controller context or TSFE
(for example the whole f:link and f:uri family)

Solution: write your own ViewHelper using SiteFinder and Site

Some ViewHelper might resolve a File to the full path instead of an URL

TEST!!

other things that work

  • BackendUtility
  • TYPO3\CMS\Core\Core\Environment
  • FAL
  • ImageRendering
  • .. a lot more

Shameless plug

some CLI tools I wrote and maintain:

  • EXT:logformatter - formats and searches TYPO3 Logfiles
  • EXT:solr_tools - some tools to initialize large EXT:solr instances
  • EXT:updatetrustedhostfromsites - syncs TRUSTED_HOSTS from site/config.yaml

QUESTIONS AND DISCUSSIONS

https://code711.de/talks/write-your-own-cli-tools-for-typo3

Thank you, I am here all week

Twitter: @FoppelFB

Mastodon: @foppel@mastodon.cloud

fberger@sudhaus7.de

https://sudhaus7.de/

fberger@code711.de

https://code711.de/