Using data handler in upgrade wizards in TYPO3 CMS
I work a lot with TYPO3 Upgrades and often have to write upgrade wizards to migrate certain data. I recently discovered, that it is possible to use the data handler, which is the core component of handling data database actions in the backend, e.g. when a record is saved, to actually do the data updates. This has the advantage, that it is possible to see changes in the records' history and all defaults are automatically applied. Furthermore the creation and updated timestamps are written automatically. It is possible to use both data updates, e.g. changing field and commands such as copy & paste, move and delete.
Following you find an example of an upgrade wizard, written for TYPO3 CMS 12.
<?php
declare(strict_types=1);
namespace MyVendor\MyExt\Install;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\ServerRequestFactory;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Attribute\UpgradeWizard;
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\RepeatableInterface;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
#[UpgradeWizard('myext_myUpgradeWizard')]
class MyUpgradeWizard implements UpgradeWizardInterface, RepeatableInterface
{
public function getTitle(): string
{
return 'My upgrade wizard';
}
public function getDescription(): string
{
return 'Migrates some data, removes old elements and creates new records.';
}
public function executeUpdate(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryBuilder
->select('c.uid', 'c.pid', 'c.pi_flexform')
->from('tt_content', 'c')
->join('c', 'pages', 'p', 'c.pid = p.uid')
->where(
$queryBuilder->expr()->eq('c.CType', $queryBuilder->createNamedParameter('dce_somecontentelement')),
$queryBuilder->expr()->neq('c.pi_flexform', $queryBuilder->createNamedParameter(''))
);
/** @var FlexFormService $flexFormService */
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$cmd = [];
$data = [];
foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $contentRecord) {
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentRecord['pi_flexform']);
// Delete the processed content element
$cmd['tt_content'][$contentRecord['uid']] = ['delete' => true];
// Set some page setting
$data['pages'][$contentRecord['pid']]['some_page_setting'] = 1;
if (empty($flexFormData)) {
continue;
}
if (empty($flexFormData['settings']['comments'])) {
continue;
}
// Parse flex form data and create data structure for the data handler
foreach ($flexFormData['settings']['comments'] as $comment) {
$data['tx_myext_domain_model_comment']['NEW_' . bin2hex(random_bytes(8))] = [
'pid' => $contentRecord['pid'],
// Set the necessary data for the new record
];
}
}
if (Environment::isCli()) {
// When on CLI, we need to initialize the special CLI user
Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
// Set user ID to avoid PHP warnings, and make the user an admin to avoid permission issues
$GLOBALS['BE_USER']->user = [
'uid' => 0,
'admin' => 1,
];
} else {
// When using the install tool, we need to initialize the backend user with the request, to make the data handler work correctly
Bootstrap::initializeBackendUser(BackendUserAuthentication::class, ServerRequestFactory::fromGlobals());
}
Bootstrap::initializeLanguageObject();
/** @var DataHandler $dataHandler */
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
// Make sure to bypass workspace restrictions
$dataHandler->bypassWorkspaceRestrictions = true;
$dataHandler->start($data, $cmd);
$dataHandler->process_datamap();
$dataHandler->process_cmdmap();
return true;
}
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryBuilder
->count('c.uid')
->from('tt_content', 'c')
->join('c', 'pages', 'p', 'c.pid = p.uid')
->where(
$queryBuilder->expr()->eq('c.CType', $queryBuilder->createNamedParameter('dce_somecontentelement')),
$queryBuilder->expr()->neq('c.pi_flexform', $queryBuilder->createNamedParameter(''))
);
return $queryBuilder->executeQuery()->fetchOne() > 0;
}
public function getPrerequisites(): array
{
return [DatabaseUpdatedPrerequisite::class];
}
}
I hope, you find it useful. Leave a comment and let me know, what you think.
This website uses Disqus for displaying comments. Due to privacy regulations you need to explicitly consent to loading comments from Disqus.
Show comments