chore: remove the gridelements migration and the address check table

The grids are migrated in production, and the production database is the
master that is never replaced by a TYPO3 10 dump, so ep:migrate-gridelements
has nothing left to do; its tx_gridelements_* columns are dropped. The
address check forms are gone (pages removed), and so is the table that
stored their submissions: personal data that has not been processed since
2019.

The tables and columns themselves are dropped by hand: extension:setup only
adds and changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-23 18:29:29 +02:00
1 parent d86efd72e3
commit dfed542af1
3 files changed
+1 -260

No files matched your search

@@ -1,218 +0,0 @@
<?php
declare(strict_types=1);
namespace EP\EpTheme\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
/**
* Turns gridelements grids into b13/container elements (see .ddev/plans/GRIDELEMENTS-TO-CONTAINER.md).
*
* Plain SQL instead of DataHandler: no hooks, no history, deterministic. Idempotent: every migrated row
* loses its tx_gridelements_* values, so a second run finds nothing to do.
*/
#[AsCommand(name: 'ep:migrate-gridelements', description: 'Migrates gridelements grids and their children to container elements')]
class MigrateGridelementsCommand extends Command
{
/**
* Gridelements layout => [container CType, first container colPos - 1, valid gridelements columns]
*/
private const LAYOUTS = [
'halves' => ['ep_container_halves', 200, [21, 22]],
'thirds' => ['ep_container_thirds', 210, [31, 32, 33]],
'quarters' => ['ep_container_columns_quarters', 220, [41, 42, 43, 44]],
'halfAndTwoQuarters' => ['ep_container_half_and_two_quarters', 230, [51, 52, 53]],
'twoQuartersAndHalf' => ['ep_container_two_quarters_and_half', 240, [61, 62, 63]],
];
/**
* Content types whose template renders the header with settings.defaultHeaderType for layout 0
*/
private const DEFAULT_HEADER_TYPES = ['text', 'textmedia', 'textpic'];
private const RESET = [
'tx_gridelements_container' => 0,
'tx_gridelements_columns' => 0,
'tx_gridelements_children' => 0,
'tx_gridelements_backend_layout' => '',
];
public function __construct(
private readonly ConnectionPool $connectionPool,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Only show what would change')
->addOption('report', null, InputOption::VALUE_REQUIRED, 'Write every row that is not migrated 1:1 to this CSV file');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = $this->connectionPool->getConnectionForTable('tt_content');
if (!$this->hasGridelementsColumns($connection)) {
$output->writeln('No tx_gridelements_* columns in tt_content, nothing to do.');
return Command::SUCCESS;
}
// Plain SQL on purpose: Connection::select() would apply the frontend restrictions (deleted, hidden, …)
$grids = [];
foreach ($connection->executeQuery('SELECT * FROM tt_content WHERE CType = ?', ['gridelements_pi1'])->iterateAssociative() as $row) {
$grids[(int)$row['uid']] = $row;
}
$children = $connection->executeQuery(
'SELECT * FROM tt_content WHERE tx_gridelements_container > 0 AND CType <> ? ORDER BY uid',
['gridelements_pi1']
)->fetchAllAssociative();
if ((int)$connection->executeQuery('SELECT COUNT(*) FROM tt_content WHERE CType = ? AND sys_language_uid > 0', ['gridelements_pi1'])->fetchOne() > 0) {
$output->writeln('<error>Translated grids found; translations are not handled by this command.</error>');
return Command::FAILURE;
}
$updates = [];
$counts = [];
$report = [];
$count = static function (string $key) use (&$counts): void {
$counts[$key] = ($counts[$key] ?? 0) + 1;
};
foreach ($grids as $uid => $grid) {
$layout = (string)$grid['tx_gridelements_backend_layout'];
if (isset(self::LAYOUTS[$layout])) {
$updates[$uid] = ['CType' => self::LAYOUTS[$layout][0], 'pi_flexform' => ''] + self::RESET;
$count('grid ' . $layout . ((int)$grid['deleted'] ? ' (deleted)' : ''));
continue;
}
// A grid without layout renders nothing; keep it restorable as an empty container
$updates[$uid] = ['CType' => 'ep_container_halves', 'pi_flexform' => '', 'deleted' => 1] + self::RESET;
$count('grid without layout → deleted');
if (!(int)$grid['deleted']) {
$report[] = [$grid, 'grid', 'grid without layout', 'deleted'];
}
}
foreach ($children as $child) {
$uid = (int)$child['uid'];
$parent = $grids[(int)$child['tx_gridelements_container']] ?? null;
$column = (int)$child['tx_gridelements_columns'];
if ((int)$child['colPos'] >= 0) {
// Rendered in its page column, not by the grid (gridelements only queries colPos -1)
$updates[$uid] = self::RESET;
$count('child outside the grid (colPos ' . $child['colPos'] . '), stays');
if (!(int)$child['deleted']) {
$report[] = [$child, 'page column', 'colPos ' . $child['colPos'] . ' instead of -1', 'unchanged, grid fields cleared'];
}
continue;
}
if ($parent !== null && (int)$child['pid'] !== (int)$parent['pid']) {
// A container only renders children of its own page
$parent = null;
}
$config = self::LAYOUTS[(string)($parent['tx_gridelements_backend_layout'] ?? '')] ?? null;
if ($parent !== null && $config !== null && (int)$child['colPos'] === -1 && in_array($column, $config[2], true)) {
$updates[$uid] = [
'tx_container_parent' => (int)$parent['uid'],
'colPos' => $config[1] + $column % 10,
] + self::RESET;
$count('A: child migrated' . ((int)$child['deleted'] ? ' (deleted)' : ''));
// Inside a grid, TYPO3 10 did not apply settings.defaultHeaderType, so the header of these
// types stayed invisible with layout "default". Keep it invisible, and visible as such to editors.
if ((int)$child['header_layout'] === 0 && trim((string)$child['header']) !== '' && in_array($child['CType'], self::DEFAULT_HEADER_TYPES, true)) {
$updates[$uid]['header_layout'] = 100;
$count('A: header layout default → hidden');
}
continue;
}
// B (column not in the layout) and C (parent is no grid): invisible in TYPO3 10, soft deleted
$group = $parent === null ? 'C' : 'B';
$reason = $parent === null
? 'parent ' . $child['tx_gridelements_container'] . ' is not a grid on the same page'
: 'column ' . $column . ' (colPos ' . $child['colPos'] . ') not in layout ' . $parent['tx_gridelements_backend_layout'];
$updates[$uid] = ['deleted' => 1] + self::RESET;
$count($group . ': child deleted' . ((int)$child['deleted'] ? ' (was deleted already)' : ''));
if (!(int)$child['deleted']) {
$report[] = [$child, $group, $reason, 'deleted'];
}
}
// Other rows still carrying grid values (e.g. a former grid whose CType was changed by hand)
$leftovers = (int)$connection->executeQuery(
'SELECT COUNT(*) FROM tt_content WHERE (tx_gridelements_children > 0 OR tx_gridelements_backend_layout NOT IN (\'\', \'0\')) AND CType <> ? AND tx_gridelements_container = 0',
['gridelements_pi1']
)->fetchOne();
ksort($counts);
foreach ($counts as $label => $number) {
$output->writeln(sprintf('%6d %s', $number, $label));
}
if ($leftovers > 0) {
$output->writeln(sprintf('%6d other rows with grid values, cleared', $leftovers));
}
if ($input->getOption('report')) {
if (!$this->writeReport((string)$input->getOption('report'), $report)) {
$output->writeln(sprintf('<error>Cannot write the report to %s, nothing migrated.</error>', $input->getOption('report')));
return Command::FAILURE;
}
$output->writeln(sprintf('Report: %d rows in %s', count($report), $input->getOption('report')));
}
if ($updates === [] && $leftovers === 0) {
$output->writeln('Nothing to migrate.');
return Command::SUCCESS;
}
if ($input->getOption('dry-run')) {
$output->writeln('<comment>Dry run, nothing written.</comment>');
return Command::SUCCESS;
}
$connection->transactional(function (Connection $connection) use ($updates): void {
foreach ($updates as $uid => $values) {
$connection->update('tt_content', $values, ['uid' => $uid]);
}
$connection->executeStatement(
'UPDATE tt_content SET tx_gridelements_children = 0, tx_gridelements_backend_layout = \'\' WHERE tx_gridelements_children > 0 OR tx_gridelements_backend_layout NOT IN (\'\', \'0\')'
);
});
$output->writeln(sprintf('<info>%d rows migrated.</info> Next: referenceindex:update, container:integrity, cache:flush', count($updates)));
return Command::SUCCESS;
}
private function hasGridelementsColumns(Connection $connection): bool
{
return array_key_exists('tx_gridelements_container', $connection->createSchemaManager()->listTableColumns('tt_content'));
}
/**
* @param list<array{0: array<string, mixed>, 1: string, 2: string, 3: string}> $rows
*/
private function writeReport(string $file, array $rows): bool
{
$handle = @fopen($file, 'wb');
if ($handle === false) {
return false;
}
$titles = [];
foreach ($this->connectionPool->getConnectionForTable('pages')->executeQuery('SELECT uid, title FROM pages WHERE sys_language_uid = 0')->iterateAssociative() as $page) {
$titles[(int)$page['uid']] = $page['title'];
}
fputcsv($handle, ['uid', 'pid', 'page', 'CType', 'header', 'hidden', 'group', 'reason', 'action'], escape: '');
foreach ($rows as [$row, $group, $reason, $action]) {
fputcsv($handle, [
$row['uid'], $row['pid'], $titles[(int)$row['pid']] ?? '', $row['CType'], $row['header'], $row['hidden'], $group, $reason, $action,
], escape: '');
}
return fclose($handle);
}
}
@@ -31,7 +31,7 @@ call_user_func(function () {
)
);
// Column layouts, migrated from gridelements (ep:migrate-gridelements). colPos = 200 + 10 × layout + column
// Column layouts, migrated from gridelements in 2026 (commit 4172ed9). colPos = 200 + 10 × layout + column
$menus = 'menu_abstract,menu_categorized_content,menu_categorized_pages,menu_recently_updated,menu_related_pages,menu_section_pages,menu_sitemap,menu_sitemap_pages';
$columnContainers = [
'ep_container_halves' => ['Spalten 50/50', 'Zwei gleich breite Spalten', 'halves', [[201], [202]]],
-41
View File
@@ -182,44 +182,3 @@ CREATE TABLE tx_eptheme_ttcontent_tx_eptheme_disrupter_mm
KEY uid_local (uid_local),
KEY uid_foreign (uid_foreign)
);
CREATE TABLE tx_eptheme_domain_model_addresscheck
(
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
cruser_id int(11) DEFAULT '0' NOT NULL,
deleted tinyint(3) DEFAULT '0',
hidden tinyint(4) DEFAULT '0' NOT NULL,
name tinytext,
first_name tinytext,
last_name tinytext,
customer_id varchar(255) DEFAULT '' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
email varchar(255) DEFAULT '' NOT NULL,
address tinytext,
city varchar(255) DEFAULT '' NOT NULL,
zip varchar(20) DEFAULT '' NOT NULL,
country varchar(128) DEFAULT '' NOT NULL,
no_postal tinyint(4) DEFAULT '0' NOT NULL,
no_email tinyint(4) DEFAULT '0' NOT NULL,
address_current tinyint(4) DEFAULT '0' NOT NULL,
t3ver_oid int(11) DEFAULT '0' NOT NULL,
t3ver_id int(11) DEFAULT '0' NOT NULL,
t3ver_wsid int(11) DEFAULT '0' NOT NULL,
t3ver_label varchar(30) DEFAULT '' NOT NULL,
t3ver_state tinyint(4) DEFAULT '0' NOT NULL,
t3ver_stage tinyint(4) DEFAULT '0' NOT NULL,
t3ver_count int(11) DEFAULT '0' NOT NULL,
t3ver_tstamp int(11) DEFAULT '0' NOT NULL,
t3ver_move_id int(11) DEFAULT '0' NOT NULL,
t3_origuid int(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY pid (pid, email)
);