Use DataHandler in symfony command

with TYPO3 and CLI

  • 8 LTS
  • 9 LTS
  • 10-dev

Example command

Let´s begin with an example class. Later you´ll see why it won´t work without an extra step.

 

<?php
declare(strict_types=1);
namespace Kronovanet\Tutorial\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ExampleCommand extends Command
{
    protected function configure(): void
    {
        $this->setDescription('...');
        $this->setHelp('...');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $data = [];
        $data['tx_tutorial_example']['NEW1234'] = [
            'pid' => 2,
            'title' => 'Example record',
            'description' => 'Some unnecessary example content...'
        ];
        
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start($data, []);
        $dataHandler->process_datamap();
    }
}

 

So this is an easy one. We just want to add one new record to our example table using the DataHandler as our processor.

The code itself is correct and would work for example in scheduler tasks or extbase commands, but not in our symfony command. But what the hell is the difference here? Let´s debug...

Debugging what´s wrong

The command can be executed and it doesn´t show you any error, but take a look into your log! You´ll see something like:

 

Attempt to modify table 'tx_tutorial_example' without permission (msg#1.2.1)	

 

But why? We created our wonderful _cli_ user with admin rights and we know that the DataHandler checks $this->admin and allows modifications if it´s true. And it does work with a scheduler task too!

Yeah the problem will be more clear if I tell you that $this->admin is null. Null? Yes! Null!

Let´s take a look why $this->admin === null. The method start gives us a clue.

 

/**
 * Initializing.
 * For details, see 'TYPO3 Core API' document.
 * This function does not start the processing of data, but merely initializes the object
 *
 * @param array $data Data to be modified or inserted in the database
 * @param array $cmd Commands to copy, move, delete, localize, versionize records.
 * @param BackendUserAuthentication|null $altUserObject An alternative userobject you can set instead of the default, which is $GLOBALS['BE_USER']
 */
public function start($data, $cmd, $altUserObject = null)
{
    // Initializing BE_USER
    $this->BE_USER = is_object($altUserObject) ? $altUserObject : $GLOBALS['BE_USER'];
    $this->userid = $this->BE_USER->user['uid'];
    $this->username = $this->BE_USER->user['username'];
    $this->admin = $this->BE_USER->user['admin'];
    ...
}

 

This method set´s the BE_USER and the admin property using $GLOBALS['BE_USER'] if we don´t pass our own $altUserObject. That´s great and exactly what we want and usually it should work.

Let´s dig a bit deeper. $GLOBALS['BE_USER'] contains our BackendUser I mean our _cli_ user. And this user is an admin so why does $this->BE_USER->user['admin'] end in null?
The reason is the way how $GLOBALS['BE_USER'] will be initialized using the CLI. I´ll show you:

 

# file: typo3/sysext/core/Classes/Console/CommandRequestHandler.php

/**
 * Handles any commandline request
 *
 * @param InputInterface $input
 */
public function handleRequest(InputInterface $input)
{
    $output = new ConsoleOutput();

    Bootstrap::loadExtTables();
    // create the BE_USER object (not logged in yet)
    Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
    Bootstrap::initializeLanguageObject();
    // Make sure output is not buffered, so command-line output and interaction can take place
    ob_clean();

    $this->populateAvailableCommands();

    $exitCode = $this->application->run($input, $output);
    exit($exitCode);
}

 

It will be initialized using the initializeBackendUser method and the result is a CommandLineUserAuthentication and not a BackendUserAuthentication. 
We need to get in a little bit deeper :P

 

# file typo3/sysext/core/Classes/Core/Bootstrap.php

/**
 * Initialize backend user object in globals
 *
 * @param string $className usually \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class but can be used for CLI
 * @return Bootstrap|null
 * @internal This is not a public API method, do not use in own extensions
 */
public static function initializeBackendUser($className = \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class)
{
    /** @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication $backendUser */
    $backendUser = GeneralUtility::makeInstance($className);
    // The global must be available very early, because methods below
    // might trigger code which relies on it. See: #45625
    $GLOBALS['BE_USER'] = $backendUser;
    $backendUser->start();
    return static::$instance;
}

 

So our CommandLineUserAuthentication will be instantiated then assigned to $GLOBALS['BE_USER'] and then the method start() will be called.
The start method inside the BackendUserAuthentication (which comes from AbstractUserAuthentication) is a big one. But it´ll be overriden with the following code:

 

/**
 * Replacement for AbstactUserAuthentication::start()
 *
 * We do not need support for sessions, cookies, $_GET-modes, the postUserLookup hook or
 * a database connectiona during CLI Bootstrap
 */
public function start()
{
    $this->logger->debug('## Beginning of auth logging.');
    // svConfig is unused, but we set it, as the property is public and might be used by extensions
    $this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ?? [];
}

 

And now we´ll see that $GLOBALS['BE_USER']->user has never been set. So how can we use the DataHandler in our symfony command?

The solution

I found this solution inside the ImportCommand of the sysext impexp. They´re using a symfony command and the DataHandler too and ran into the same problem.

Their solution:

 

// Ensure the _cli_ user is authenticated
Bootstrap::initializeBackendAuthentication();

 

Sounds great. The easiest way would be copy paste but be careful! There is a command that forbids us the usage of this method:

 

/**
 * Initializes and ensures authenticated access
 *
 * @internal This is not a public API method, do not use in own extensions
 * @param bool $proceedIfNoUserIsLoggedIn if set to TRUE, no forced redirect to the login page will be done
 * @return Bootstrap|null
 */
public static function initializeBackendAuthentication($proceedIfNoUserIsLoggedIn = false)
{
    $GLOBALS['BE_USER']->backendCheckLogin($proceedIfNoUserIsLoggedIn);
    return static::$instance;
}

 

So let´s copy paste the first line of that method and we´re safe :)

For your interest: The backendCheckLogin() method calls the authenticate() method which initializes the user object. Take a look into the class if you´re interested:

Show me the code

Working example

<?php
declare(strict_types=1);
namespace Kronovanet\Tutorial\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ExampleCommand extends Command
{
    protected function configure(): void
    {
        $this->setDescription('...');
        $this->setHelp('...');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // authenticate CommandLineUserAuthentication user for DataHandler usage
        $GLOBALS['BE_USER']->backendCheckLogin();
        $data = [];
        $data['tx_tutorial_example']['NEW1234'] = [
            'pid' => 2,
            'title' => 'Example record',
            'description' => 'Some unnecessary example content...'
        ];
        
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start($data, []);
        $dataHandler->process_datamap();
    }
}