Loading...
Loading...
Expert guidance on manipulating TYPO3 records via the DataHandler, ensuring transactional safety, PSR-14 event handling, and reference index integrity. Use when working with database, datahandler, tcemain, records, content, pages.
npx skill4agent add dirnbauer/webconsulting-skills typo3-datahandlerCompatibility: TYPO3 v13.x and v14.x (v14 preferred) All code examples in this skill are designed to work on both TYPO3 v13 and v14.
TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in your target TYPO3 version (v13 or v14) by checking the official TYPO3 documentation.
INSERTUPDATEDELETEpagestt_contentDataHandlersys_refindexsys_history$data[tableName][uid][fieldName] = valueNEW<?php
declare(strict_types=1);
$data = [
'tt_content' => [
'NEW_1' => [
'pid' => 1,
'CType' => 'text',
'header' => 'My New Content Element',
'bodytext' => '<p>Content goes here</p>',
'sys_language_uid' => 0,
],
],
];<?php
declare(strict_types=1);
$data = [
'tt_content' => [
123 => [
'header' => 'Updated Header',
'hidden' => 0,
],
],
];<?php
declare(strict_types=1);
$data = [
'pages' => [
'NEW_page' => [
'pid' => 1,
'title' => 'New Page',
],
],
'tt_content' => [
'NEW_content' => [
'pid' => 'NEW_page', // References the new page
'CType' => 'text',
'header' => 'Content on new page',
],
],
];$cmd[tableName][uid][command] = value<?php
declare(strict_types=1);
$cmd = [
'tt_content' => [
123 => ['delete' => 1],
],
];<?php
declare(strict_types=1);
$cmd = [
'tt_content' => [
123 => ['move' => 456], // Target page UID; use negative UID to place after record
],
];<?php
declare(strict_types=1);
$cmd = [
'tt_content' => [
123 => ['copy' => 1], // Target page UID
],
];<?php
declare(strict_types=1);
$cmd = [
'tt_content' => [
123 => [
'localize' => 1, // Target language UID
],
],
];<?php
declare(strict_types=1);
namespace Vendor\Extension\Service;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
final class ContentService
{
public function __construct(
private readonly ConnectionPool $connectionPool,
) {}
public function createContentWithTransaction(array $data, array $cmd = []): array
{
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
// 1. Prepare
$dataHandler->start($data, $cmd);
// 2. Get Connection & Start Transaction
$connection = $this->connectionPool->getConnectionForTable('tt_content');
$connection->beginTransaction();
try {
// 3. Process DataMap
if (!empty($data)) {
$dataHandler->process_datamap();
}
// 4. Process CmdMap
if (!empty($cmd)) {
$dataHandler->process_cmdmap();
}
// 5. Validate
if (!empty($dataHandler->errorLog)) {
throw new \RuntimeException(
'DataHandler Error: ' . implode(', ', $dataHandler->errorLog),
1700000001
);
}
// 6. Commit
$connection->commit();
// 7. Return substituted UIDs for NEW records
return $dataHandler->substNEWwithIDs;
} catch (\Throwable $e) {
// 8. Rollback on Failure
$connection->rollBack();
// Log and re-throw
throw $e;
}
}
}<?php
declare(strict_types=1);
// Set admin context for DataHandler operations
$GLOBALS['BE_USER']->user['admin'] = 1;
$GLOBALS['BE_USER']->workspace = 0; // Live workspace
// Alternative: Use the BackendUserAuthentication properly
$backendUser = $GLOBALS['BE_USER'];
$backendUser->setWorkspace(0);<?php
declare(strict_types=1);
namespace Vendor\Extension\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: 'myext:import',
description: 'Import data using DataHandler',
)]
final class ImportCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Initialize backend for DataHandler operations
Bootstrap::initializeBackendAuthentication();
// Your DataHandler logic here...
return Command::SUCCESS;
}
}<?php
declare(strict_types=1);
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Utility\GeneralUtility;
$referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
// Update for a specific record
$referenceIndex->updateRefIndexTable('tt_content', 123);
// Or update all (for migrations)
// Run via CLI: vendor/bin/typo3 referenceindex:update// ❌ WRONG - Missing pid
$data = ['tt_content' => ['NEW_1' => ['header' => 'Test']]];
// ✅ CORRECT - Always include pid
$data = ['tt_content' => ['NEW_1' => ['pid' => 1, 'header' => 'Test']]];// ❌ WRONG - String UID for existing record
$data = ['tt_content' => ['123' => ['header' => 'Test']]];
// ✅ CORRECT - Integer UID for existing record
$data = ['tt_content' => [123 => ['header' => 'Test']]];// ❌ WRONG - Nothing happens
$dataHandler->start($data, $cmd);
// ✅ CORRECT - Actually process the data
$dataHandler->start($data, $cmd);
$dataHandler->process_datamap();
$dataHandler->process_cmdmap();// ❌ WRONG - Silently ignoring errors
$dataHandler->process_datamap();
// ✅ CORRECT - Check for errors
$dataHandler->process_datamap();
if (!empty($dataHandler->errorLog)) {
// Handle errors appropriately
throw new \RuntimeException(implode(', ', $dataHandler->errorLog));
}<?php
declare(strict_types=1);
$dataHandler->process_datamap();
// Get the real UID for 'NEW_1'
$newContentUid = $dataHandler->substNEWwithIDs['NEW_1'] ?? null;
if ($newContentUid === null) {
throw new \RuntimeException('Failed to create content element');
}<?php
declare(strict_types=1);
// Check if we're in a workspace
$workspaceId = $GLOBALS['BE_USER']->workspace;
if ($workspaceId > 0) {
// In workspace - DataHandler will create versioned records
// Use the wsol (workspace overlay) for reading
}
// Force live workspace for specific operations
$previousWorkspace = $GLOBALS['BE_USER']->workspace;
$GLOBALS['BE_USER']->setWorkspace(0);
// ... perform operations ...
$GLOBALS['BE_USER']->setWorkspace($previousWorkspace);Important: PSR-14 events are the preferred way to react to DataHandler operations in TYPO3 v13/v14. Legacy hooks still work in v13 but should be migrated to events.
| Event | Triggered When |
|---|---|
| Before any record operation |
| After any record operation |
| After database operations complete |
| Before a new record is inserted |
# Configuration/Services.yaml
services:
Vendor\Extension\EventListener\ContentCreatedListener:
tags:
- name: event.listener
identifier: 'vendor-extension/content-created'<?php
declare(strict_types=1);
namespace Vendor\Extension\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\Event\AfterDatabaseOperationsEvent;
#[AsEventListener(identifier: 'vendor-extension/content-created')]
final class ContentCreatedListener
{
public function __invoke(AfterDatabaseOperationsEvent $event): void
{
$table = $event->getTable();
$status = $event->getStatus();
$recordUid = $event->getRecordUid();
$fields = $event->getFields();
$dataHandler = $event->getDataHandler();
if ($table !== 'tt_content') {
return;
}
if ($status === 'new') {
// Handle new record creation
$newUid = $dataHandler->substNEWwithIDs[$recordUid] ?? $recordUid;
// Your logic here...
}
if ($status === 'update') {
// Handle record update
}
}
}<?php
declare(strict_types=1);
namespace Vendor\Extension\EventListener;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\Event\ModifyRecordBeforeInsertEvent;
#[AsEventListener(identifier: 'vendor-extension/modify-before-insert')]
final class ModifyBeforeInsertListener
{
public function __invoke(ModifyRecordBeforeInsertEvent $event): void
{
if ($event->getTable() !== 'tx_myext_domain_model_item') {
return;
}
$record = $event->getRecord();
// Modify the record before it's inserted
$record['crdate'] = time();
$record['tstamp'] = time();
$event->setRecord($record);
}
}Warning: Legacy hooks are deprecated in TYPO3 v14. Use PSR-14 events instead. The following is for reference when maintaining legacy code.
ext_localconf.php<?php
declare(strict_types=1);
// ⚠️ DEPRECATED - Use PSR-14 events for new code
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= \Vendor\Extension\Hooks\DataHandlerHook::class;
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][]
= \Vendor\Extension\Hooks\DataHandlerHook::class;<?php
declare(strict_types=1);
namespace Vendor\Extension\Hooks;
use TYPO3\CMS\Core\DataHandling\DataHandler;
/**
* @deprecated Use PSR-14 events instead. This hook works in v13 but should be migrated.
*/
final class DataHandlerHook
{
public function processDatamap_afterDatabaseOperations(
string $status,
string $table,
string|int $id,
array $fieldArray,
DataHandler $dataHandler
): void {
if ($table !== 'tt_content') {
return;
}
if ($status === 'new') {
// Handle new record creation
$newUid = $dataHandler->substNEWwithIDs[$id] ?? $id;
}
if ($status === 'update') {
// Handle record update
}
}
}<?php
// ext_emconf.php
$EM_CONF[$_EXTKEY] = [
'title' => 'My Extension',
'version' => '1.0.0',
'state' => 'stable',
'constraints' => [
'depends' => [
'typo3' => '13.0.0-14.99.99',
'php' => '8.2.0-8.4.99',
],
],
];