typo3-datahandler

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TYPO3 DataHandler Operations

TYPO3 DataHandler 操作指南

Compatibility: 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.
兼容性: TYPO3 v13.x 和 v14.x(推荐使用v14) 本技能中的所有代码示例均兼容TYPO3 v13和v14版本。
TYPO3 API优先原则: 在创建自定义实现之前,务必优先使用TYPO3的内置API、核心功能和既定规范。不要重复造轮子。使用前请务必通过TYPO3官方文档验证你所使用的API和方法在目标TYPO3版本(v13或v14)中是否存在且未被弃用。

1. The Prime Directive

1. 核心准则

NEVER use raw SQL (
INSERT
,
UPDATE
,
DELETE
) for
pages
,
tt_content
, or any TCA-configured table.
You MUST use the
DataHandler
to ensure:
  • Reference Index updates (
    sys_refindex
    )
  • Cache clearing
  • Version history (
    sys_history
    )
  • Workspace compatibility
  • PSR-14 event dispatching
  • FlexForm handling
  • MM relation management
绝对不要
pages
tt_content
或任何TCA配置的表使用原生SQL(
INSERT
UPDATE
DELETE
)。
必须使用
DataHandler
来确保:
  • 引用索引更新(
    sys_refindex
  • 缓存清理
  • 版本历史(
    sys_history
  • 工作区兼容性
  • PSR-14事件分发
  • FlexForm处理
  • MM关系管理

Exceptions (Raw SQL Allowed)

例外情况(允许使用原生SQL)

  • Custom logging tables without TCA
  • Bulk analytics/reporting queries (read-only)
  • Migration scripts with explicit reference index rebuild
  • 无TCA配置的自定义日志表
  • 批量分析/报表查询(只读)
  • 明确包含引用索引重建的迁移脚本

2. Structure of Arrays

2. 数组结构

The DataMap ($data)

数据映射($data)

Used for creating or updating records.
Syntax:
$data[tableName][uid][fieldName] = value
Creating a New Record:
Use a unique string starting with
NEW
as the UID:
php
<?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,
        ],
    ],
];
Updating an Existing Record:
Use the numeric UID:
php
<?php
declare(strict_types=1);

$data = [
    'tt_content' => [
        123 => [
            'header' => 'Updated Header',
            'hidden' => 0,
        ],
    ],
];
Referencing NEW Records:
php
<?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',
        ],
    ],
];
用于创建更新记录。
语法:
$data[tableName][uid][fieldName] = value
创建新记录:
使用以
NEW
开头的唯一字符串作为UID:
php
<?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,
        ],
    ],
];
更新现有记录:
使用数值型UID:
php
<?php
declare(strict_types=1);

$data = [
    'tt_content' => [
        123 => [
            'header' => 'Updated Header',
            'hidden' => 0,
        ],
    ],
];
引用新创建的记录:
php
<?php
declare(strict_types=1);

$data = [
    'pages' => [
        'NEW_page' => [
            'pid' => 1,
            'title' => 'New Page',
        ],
    ],
    'tt_content' => [
        'NEW_content' => [
            'pid' => 'NEW_page', // 引用新创建的页面
            'CType' => 'text',
            'header' => 'Content on new page',
        ],
    ],
];

The CmdMap ($cmd)

命令映射($cmd)

Used for moving, copying, deleting, or undeleting records.
Syntax:
$cmd[tableName][uid][command] = value
Delete a Record:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['delete' => 1],
    ],
];
Move a Record:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['move' => 456], // Target page UID; use negative UID to place after record
    ],
];
Copy a Record:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['copy' => 1], // Target page UID
    ],
];
Localize a Record:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => [
            'localize' => 1, // Target language UID
        ],
    ],
];
用于移动复制删除恢复记录。
语法:
$cmd[tableName][uid][command] = value
删除记录:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['delete' => 1],
    ],
];
移动记录:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['move' => 456], // 目标页面UID;使用负UID表示将记录放在指定记录之后
    ],
];
复制记录:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => ['copy' => 1], // 目标页面UID
    ],
];
本地化记录:
php
<?php
declare(strict_types=1);

$cmd = [
    'tt_content' => [
        123 => [
            'localize' => 1, // 目标语言UID
        ],
    ],
];

3. Transactional Execution Pattern (Mandatory)

3. 事务执行模式(强制要求)

DataHandler already wraps its own database writes in transactions. Only wrap the call yourself when you have to bundle multiple DataHandler runs or adjacent custom writes on the same connection. Do not mix different connections inside one transaction.
php
<?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;
        }
    }
}
DataHandler已将自身的数据库写入操作包裹在事务中。仅当需要将多个DataHandler运行或相邻的自定义写入操作绑定到同一连接时,才需要自行包裹事务。不要在一个事务中混合不同的连接。
php
<?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. 准备
        $dataHandler->start($data, $cmd);

        // 2. 获取连接并启动事务
        $connection = $this->connectionPool->getConnectionForTable('tt_content');
        $connection->beginTransaction();

        try {
            // 3. 处理数据映射
            if (!empty($data)) {
                $dataHandler->process_datamap();
            }

            // 4. 处理命令映射
            if (!empty($cmd)) {
                $dataHandler->process_cmdmap();
            }

            // 5. 验证
            if (!empty($dataHandler->errorLog)) {
                throw new \RuntimeException(
                    'DataHandler Error: ' . implode(', ', $dataHandler->errorLog),
                    1700000001
                );
            }

            // 6. 提交事务
            $connection->commit();

            // 7. 返回新记录替换后的UID
            return $dataHandler->substNEWwithIDs;

        } catch (\Throwable $e) {
            // 8. 失败时回滚
            $connection->rollBack();
            
            // 记录日志并重新抛出异常
            throw $e;
        }
    }
}

4. Admin Context

4. 管理员上下文

When running from CLI (Symfony Command) or scheduler tasks, you MUST ensure the backend user has admin privileges:
php
<?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);
当从CLI(Symfony命令)或调度任务运行时,必须确保后端用户拥有管理员权限:
php
<?php
declare(strict_types=1);

// 为DataHandler操作设置管理员上下文
$GLOBALS['BE_USER']->user['admin'] = 1;
$GLOBALS['BE_USER']->workspace = 0; // 实时工作区

// 替代方案:正确使用BackendUserAuthentication
$backendUser = $GLOBALS['BE_USER'];
$backendUser->setWorkspace(0);

CLI Command Setup (v13/v14 Compatible)

CLI命令设置(兼容v13/v14)

php
<?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
<?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
    {
        // 初始化后端以支持DataHandler操作
        Bootstrap::initializeBackendAuthentication();
        
        // 你的DataHandler逻辑代码...
        
        return Command::SUCCESS;
    }
}

5. Reference Index Handling

5. 引用索引处理

After bulk operations, always update the reference index:
php
<?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
批量操作完成后,务必更新引用索引:
php
<?php
declare(strict_types=1);

use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);

// 更新特定记录的引用索引
$referenceIndex->updateRefIndexTable('tt_content', 123);

// 或更新所有索引(适用于迁移场景)
// 通过CLI运行:vendor/bin/typo3 referenceindex:update

6. Common Pitfalls

6. 常见陷阱

Pitfall 1: Missing PID for New Records

陷阱1:新记录缺少PID

php
// ❌ WRONG - Missing pid
$data = ['tt_content' => ['NEW_1' => ['header' => 'Test']]];

// ✅ CORRECT - Always include pid
$data = ['tt_content' => ['NEW_1' => ['pid' => 1, 'header' => 'Test']]];
php
// ❌ 错误 - 缺少pid
$data = ['tt_content' => ['NEW_1' => ['header' => 'Test']]];

// ✅ 正确 - 始终包含pid
$data = ['tt_content' => ['NEW_1' => ['pid' => 1, 'header' => 'Test']]];

Pitfall 2: Using String UIDs for Existing Records

陷阱2:为现有记录使用字符串类型UID

php
// ❌ WRONG - String UID for existing record
$data = ['tt_content' => ['123' => ['header' => 'Test']]];

// ✅ CORRECT - Integer UID for existing record
$data = ['tt_content' => [123 => ['header' => 'Test']]];
php
// ❌ 错误 - 现有记录使用字符串UID
$data = ['tt_content' => ['123' => ['header' => 'Test']]];

// ✅ 正确 - 现有记录使用整数UID
$data = ['tt_content' => [123 => ['header' => 'Test']]];

Pitfall 3: Forgetting process_datamap() / process_cmdmap()

陷阱3:忘记调用process_datamap() / process_cmdmap()

php
// ❌ WRONG - Nothing happens
$dataHandler->start($data, $cmd);

// ✅ CORRECT - Actually process the data
$dataHandler->start($data, $cmd);
$dataHandler->process_datamap();
$dataHandler->process_cmdmap();
php
// ❌ 错误 - 不会执行任何操作
$dataHandler->start($data, $cmd);

// ✅ 正确 - 实际处理数据
$dataHandler->start($data, $cmd);
$dataHandler->process_datamap();
$dataHandler->process_cmdmap();

Pitfall 4: Ignoring Error Log

陷阱4:忽略错误日志

php
// ❌ 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
// ❌ 错误 - 静默忽略错误
$dataHandler->process_datamap();

// ✅ 正确 - 检查错误
$dataHandler->process_datamap();
if (!empty($dataHandler->errorLog)) {
    // 适当处理错误
    throw new \RuntimeException(implode(', ', $dataHandler->errorLog));
}

7. Retrieving New Record UIDs

7. 获取新记录的UID

After processing, get the real UIDs for NEW records:
php
<?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');
}
处理完成后,获取新记录的真实UID:
php
<?php
declare(strict_types=1);

$dataHandler->process_datamap();

// 获取'NEW_1'对应的真实UID
$newContentUid = $dataHandler->substNEWwithIDs['NEW_1'] ?? null;

if ($newContentUid === null) {
    throw new \RuntimeException('Failed to create content element');
}

8. Workspace-Aware Operations

8. 工作区感知操作

When working with workspaces:
php
<?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);
在工作区环境中操作时:
php
<?php
declare(strict_types=1);

// 检查是否处于工作区中
$workspaceId = $GLOBALS['BE_USER']->workspace;

if ($workspaceId > 0) {
    // 处于工作区 - DataHandler将创建版本化记录
    // 读取时使用wsol(工作区覆盖)
}

// 强制使用实时工作区执行特定操作
$previousWorkspace = $GLOBALS['BE_USER']->workspace;
$GLOBALS['BE_USER']->setWorkspace(0);

// ... 执行操作 ...

$GLOBALS['BE_USER']->setWorkspace($previousWorkspace);

9. PSR-14 Events (Preferred Pattern)

9. PSR-14事件(推荐模式)

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.
重要提示: 在TYPO3 v13/v14中,PSR-14事件是响应DataHandler操作的推荐方式。旧版钩子在v13中仍可使用,但应迁移到事件机制。

Available DataHandler Events

可用的DataHandler事件

EventTriggered When
BeforeRecordOperationEvent
Before any record operation
AfterRecordOperationEvent
After any record operation
AfterDatabaseOperationsEvent
After database operations complete
ModifyRecordBeforeInsertEvent
Before a new record is inserted
事件触发时机
BeforeRecordOperationEvent
任何记录操作之前
AfterRecordOperationEvent
任何记录操作之后
AfterDatabaseOperationsEvent
数据库操作完成之后
ModifyRecordBeforeInsertEvent
新记录插入之前

Event Listener Registration (Services.yaml)

事件监听器注册(Services.yaml)

yaml
undefined
yaml
undefined

Configuration/Services.yaml

Configuration/Services.yaml

services: Vendor\Extension\EventListener\ContentCreatedListener: tags: - name: event.listener identifier: 'vendor-extension/content-created'
undefined
services: Vendor\Extension\EventListener\ContentCreatedListener: tags: - name: event.listener identifier: 'vendor-extension/content-created'
undefined

Event Listener Implementation (v13/v14 Compatible)

事件监听器实现(兼容v13/v14)

php
<?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
<?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') {
            // 处理新记录创建逻辑
            $newUid = $dataHandler->substNEWwithIDs[$recordUid] ?? $recordUid;
            // 你的业务逻辑...
        }

        if ($status === 'update') {
            // 处理记录更新逻辑
        }
    }
}

Modifying Records Before Insert

插入前修改记录

php
<?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);
    }
}
php
<?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();
        
        // 在记录插入前修改数据
        $record['crdate'] = time();
        $record['tstamp'] = time();
        
        $event->setRecord($record);
    }
}

10. Legacy Hooks (Deprecated, v13 Only)

10. 旧版钩子(已弃用,仅v13支持)

Warning: Legacy hooks are deprecated in TYPO3 v14. Use PSR-14 events instead. The following is for reference when maintaining legacy code.
Register hooks in
ext_localconf.php
:
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;
Hook implementation:
php
<?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
        }
    }
}
警告: 旧版钩子在TYPO3 v14中已被弃用。请使用PSR-14事件替代。以下内容仅用于维护旧代码时参考。
ext_localconf.php
中注册钩子:
php
<?php
declare(strict_types=1);

// ⚠️ 已弃用 - 新代码请使用PSR-14事件
$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
<?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') {
            // 处理新记录创建逻辑
            $newUid = $dataHandler->substNEWwithIDs[$id] ?? $id;
        }

        if ($status === 'update') {
            // 处理记录更新逻辑
        }
    }
}

11. Version Constraints for Extensions

11. 扩展的版本约束

When creating extensions that use DataHandler, ensure proper version constraints:
php
<?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',
        ],
    ],
];

创建使用DataHandler的扩展时,确保设置正确的版本约束:
php
<?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',
        ],
    ],
];

Credits & Attribution

致谢与归属

Thanks to Netresearch DTT GmbH for their contributions to the TYPO3 community.
感谢Netresearch DTT GmbH对TYPO3社区的贡献。