diff --git a/.github/workflows/diagnostics.yml b/.github/workflows/diagnostics.yml index 965020297..7f7bca80e 100644 --- a/.github/workflows/diagnostics.yml +++ b/.github/workflows/diagnostics.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Pull source - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 @@ -29,7 +29,7 @@ jobs: # setup caches - name: Cache composer cache directory - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: composer-cache-dir with: @@ -37,7 +37,7 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-build-${{ env.cache-name }} - name: Cache vendor directory - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: vendor with: @@ -47,7 +47,7 @@ jobs: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.contao }}-build-${{ env.cache-name }}- - name: Cache phpcq directory - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: phpcq with: @@ -69,7 +69,7 @@ jobs: run: ./vendor/bin/phpcq run -v ${{ matrix.output }} - name: Upload build directory to artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: success() || failure() with: name: phpcq-builds-php-${{ matrix.php }}-${{ matrix.contao }} diff --git a/psalm.xml b/psalm.xml index c38e88615..683d39edc 100644 --- a/psalm.xml +++ b/psalm.xml @@ -23,7 +23,6 @@ - @@ -271,7 +270,6 @@ - diff --git a/runonce/runonce.php b/runonce/runonce.php deleted file mode 100644 index b7c7a5429..000000000 --- a/runonce/runonce.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @author Christopher Boelter - * @author Sven Baumann - * @copyright 2012-2019 The MetaModels team. - * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later - * @filesource - */ - -// Let our handler handle the necessary steps. -MetaModels\Helper\UpgradeHandler::perform(); diff --git a/src/CoreBundle/Migration/DcaSettingPublishedMigration.php b/src/CoreBundle/Migration/DcaSettingPublishedMigration.php new file mode 100644 index 000000000..92c6bf34a --- /dev/null +++ b/src/CoreBundle/Migration/DcaSettingPublishedMigration.php @@ -0,0 +1,106 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; + +/** + * Adds the "published" column to "tl_metamodel_dcasetting" and sets all existing + * rows to published=1 to preserve the prior behaviour (everything was published). + * + * Introduced: MetaModels 1.0.1. + */ +final class DcaSettingPublishedMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Add "published" column to "tl_metamodel_dcasetting".'; + } + + #[\Override] + public function shouldRun(): bool + { + if (!$this->tablesExist(['tl_metamodel_dcasetting'])) { + return false; + } + + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dcasetting') + ); + + return !in_array('published', $columnNames, true); + } + + #[\Override] + public function run(): MigrationResult + { + $this->connection->executeStatement( + "ALTER TABLE `tl_metamodel_dcasetting` ADD COLUMN `published` char(1) NOT NULL default ''" + ); + $this->connection->executeStatement( + 'UPDATE `tl_metamodel_dcasetting` SET `published`=1' + ); + + return new MigrationResult(true, 'Added "published" column to "tl_metamodel_dcasetting".'); + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Migration/InputScreenFlagMigration.php b/src/CoreBundle/Migration/InputScreenFlagMigration.php new file mode 100644 index 000000000..9977cc82a --- /dev/null +++ b/src/CoreBundle/Migration/InputScreenFlagMigration.php @@ -0,0 +1,196 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; +use function sprintf; +use function time; + +/** + * Converts the legacy "flag" column in "tl_metamodel_dca" into proper sort-group + * entries in "tl_metamodel_dca_sortgroup" and drops the column afterwards. + * + * The flag value encodes both the grouping type and sort direction; odd values + * sort ascending, even values sort descending. + * + * Flag mapping: + * 1–2 → char grouping, length 1 + * 3–4 → char grouping, length 2 + * 5–6 → day grouping + * 7–8 → month grouping + * 9–10 → year grouping + * 11–12 → digit grouping + * other → no grouping + */ +final class InputScreenFlagMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Migrate "flag" column to sort-group entries in "tl_metamodel_dca_sortgroup".'; + } + + #[\Override] + public function shouldRun(): bool + { + if (!$this->tablesExist(['tl_metamodel_dca'])) { + return false; + } + + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dca') + ); + + return in_array('flag', $columnNames, true); + } + + #[\Override] + public function run(): MigrationResult + { + $this->ensureSortGroupTableExists(); + + $dcaRows = $this->connection->fetchAllAssociative('SELECT * FROM `tl_metamodel_dca`'); + $count = 0; + foreach ($dcaRows as $dca) { + [$renderGroupType, $renderGroupLen] = $this->resolveGroupType((int) $dca['flag']); + + $this->connection->insert( + 'tl_metamodel_dca_sortgroup', + [ + 'pid' => (int) $dca['id'], + 'sorting' => 128, + 'tstamp' => time(), + 'name' => null, + 'isdefault' => '1', + 'ismanualsort' => '1', + 'rendergrouptype' => $renderGroupType, + 'rendergrouplen' => $renderGroupLen, + 'rendergroupattr' => 0, + 'rendersort' => in_array((int) $dca['flag'], [2, 4, 6, 8, 10, 12], true) ? 'desc' : 'asc', + 'rendersortattr' => 0, + ] + ); + $count++; + } + + $this->connection->executeStatement( + 'ALTER TABLE `tl_metamodel_dca` DROP COLUMN `flag`' + ); + + return new MigrationResult( + true, + sprintf('Created %d sort-group row(s) and dropped "flag" column from "tl_metamodel_dca".', $count) + ); + } + + /** + * Returns [renderGroupType, renderGroupLen] for the given flag value. + * + * @return array{string, int} + */ + private function resolveGroupType(int $flag): array + { + if (in_array($flag, [1, 2, 3, 4], true)) { + return ['char', in_array($flag, [1, 2], true) ? 1 : 2]; + } + if (in_array($flag, [5, 6], true)) { + return ['day', 0]; + } + if (in_array($flag, [7, 8], true)) { + return ['month', 0]; + } + if (in_array($flag, [9, 10], true)) { + return ['year', 0]; + } + if (in_array($flag, [11, 12], true)) { + return ['digit', 0]; + } + + return ['none', 0]; + } + + private function ensureSortGroupTableExists(): void + { + if ($this->tablesExist(['tl_metamodel_dca_sortgroup'])) { + return; + } + + $this->connection->executeStatement( + 'CREATE TABLE `tl_metamodel_dca_sortgroup` ( + `id` int(10) unsigned NOT NULL auto_increment, + `pid` int(10) unsigned NOT NULL default \'0\', + `sorting` int(10) unsigned NOT NULL default \'0\', + `tstamp` int(10) unsigned NOT NULL default \'0\', + `name` text NULL, + `isdefault` char(1) NOT NULL default \'\', + `ismanualsort` char(1) NOT NULL default \'\', + `rendergrouptype` varchar(10) NOT NULL default \'none\', + `rendergrouplen` int(10) unsigned NOT NULL default \'1\', + `rendergroupattr` int(10) unsigned NOT NULL default \'0\', + `rendersort` varchar(10) NOT NULL default \'asc\', + `rendersortattr` int(10) unsigned NOT NULL default \'0\', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' + ); + $this->existsCache = []; + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Migration/InputScreenModeMigration.php b/src/CoreBundle/Migration/InputScreenModeMigration.php new file mode 100644 index 000000000..29b770c3d --- /dev/null +++ b/src/CoreBundle/Migration/InputScreenModeMigration.php @@ -0,0 +1,124 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; + +/** + * Replaces the legacy numeric "mode" column in "tl_metamodel_dca" with the + * string-based "rendermode" column ("flat", "parented", "hierarchical"). + */ +final class InputScreenModeMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Replace numeric "mode" with string "rendermode" in "tl_metamodel_dca".'; + } + + #[\Override] + public function shouldRun(): bool + { + if (!$this->tablesExist(['tl_metamodel_dca'])) { + return false; + } + + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dca') + ); + + return in_array('mode', $columnNames, true); + } + + #[\Override] + public function run(): MigrationResult + { + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dca') + ); + + if (!in_array('rendermode', $columnNames, true)) { + $this->connection->executeStatement( + "ALTER TABLE `tl_metamodel_dca` ADD COLUMN `rendermode` varchar(12) NOT NULL default ''" + ); + } + + $this->connection->executeStatement( + "UPDATE `tl_metamodel_dca` SET `rendermode`='flat' WHERE `mode` IN (0, 1, 2, 3)" + ); + $this->connection->executeStatement( + "UPDATE `tl_metamodel_dca` SET `rendermode`='parented' WHERE `mode` IN (4)" + ); + $this->connection->executeStatement( + "UPDATE `tl_metamodel_dca` SET `rendermode`='hierarchical' WHERE `mode` IN (5, 6)" + ); + + $this->connection->executeStatement( + 'ALTER TABLE `tl_metamodel_dca` DROP COLUMN `mode`' + ); + + return new MigrationResult( + true, + 'Replaced numeric "mode" with string "rendermode" in "tl_metamodel_dca".' + ); + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Migration/IsClosedMigration.php b/src/CoreBundle/Migration/IsClosedMigration.php new file mode 100644 index 000000000..3aec7cd91 --- /dev/null +++ b/src/CoreBundle/Migration/IsClosedMigration.php @@ -0,0 +1,122 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; + +/** + * Replaces the legacy "isclosed" column in "tl_metamodel_dca" with the three + * separate flags "iseditable", "iscreatable", and "isdeleteable", deriving their + * values via bitwise XOR (isclosed^1 = the inverse). + */ +final class IsClosedMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Replace "isclosed" with "iseditable", "iscreatable", "isdeleteable" in "tl_metamodel_dca".'; + } + + #[\Override] + public function shouldRun(): bool + { + if (!$this->tablesExist(['tl_metamodel_dca'])) { + return false; + } + + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dca') + ); + + return in_array('isclosed', $columnNames, true); + } + + #[\Override] + public function run(): MigrationResult + { + $schemaManager = $this->connection->createSchemaManager(); + $columnNames = array_keys($schemaManager->listTableColumns('tl_metamodel_dca')); + + foreach (['iseditable', 'iscreatable', 'isdeleteable'] as $col) { + if (!in_array($col, $columnNames, true)) { + $this->connection->executeStatement( + 'ALTER TABLE `tl_metamodel_dca`' + . " ADD COLUMN `{$col}` char(1) NOT NULL default ''" + ); + } + } + + $this->connection->executeStatement( + 'UPDATE `tl_metamodel_dca` + SET `iseditable`=`isclosed`^1, `iscreatable`=`isclosed`^1, `isdeleteable`=`isclosed`^1' + ); + + $this->connection->executeStatement( + 'ALTER TABLE `tl_metamodel_dca` DROP COLUMN `isclosed`' + ); + + return new MigrationResult( + true, + 'Replaced "isclosed" with "iseditable", "iscreatable", "isdeleteable" in "tl_metamodel_dca".' + ); + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Migration/JumpToMigration.php b/src/CoreBundle/Migration/JumpToMigration.php new file mode 100644 index 000000000..6634196d7 --- /dev/null +++ b/src/CoreBundle/Migration/JumpToMigration.php @@ -0,0 +1,131 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function implode; +use function strtolower; + +/** + * Adds the "metamodel_jumpTo" column to "tl_content" and "tl_module" and copies + * any existing "jumpTo" values over. + * + * Introduced: MetaModels pre-release 1.0. + */ +final class JumpToMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Add "metamodel_jumpTo" column to "tl_content" and "tl_module".'; + } + + #[\Override] + public function shouldRun(): bool + { + return !empty($this->findTablesNeedingMigration()); + } + + #[\Override] + public function run(): MigrationResult + { + $messages = []; + foreach ($this->findTablesNeedingMigration() as $tableName => $hasJumpTo) { + $this->connection->executeStatement( + 'ALTER TABLE `' . $tableName . '`' + . " ADD COLUMN `metamodel_jumpTo` int(10) unsigned NOT NULL default '0'" + ); + if ($hasJumpTo) { + $this->connection->executeStatement( + 'UPDATE `' . $tableName . '` SET `metamodel_jumpTo`=`jumpTo`' + ); + } + $messages[] = $tableName; + } + + return new MigrationResult(true, 'Added "metamodel_jumpTo" to: ' . implode(', ', $messages)); + } + + /** + * Returns a map of table name → whether a "jumpTo" source column exists. + * + * @return array + */ + private function findTablesNeedingMigration(): array + { + $result = []; + foreach (['tl_content', 'tl_module'] as $tableName) { + if (!$this->tablesExist([$tableName])) { + continue; + } + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns($tableName) + ); + if (\in_array('metamodel_jumpto', $columnNames, true)) { + continue; + } + $result[$tableName] = \in_array('jumpto', $columnNames, true); + } + + return $result; + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Migration/SubPalettesToConditionsMigration.php b/src/CoreBundle/Migration/SubPalettesToConditionsMigration.php new file mode 100644 index 000000000..0908efb52 --- /dev/null +++ b/src/CoreBundle/Migration/SubPalettesToConditionsMigration.php @@ -0,0 +1,205 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +declare(strict_types=1); + +namespace MetaModels\CoreBundle\Migration; + +use Contao\CoreBundle\Migration\AbstractMigration; +use Contao\CoreBundle\Migration\MigrationResult; +use Doctrine\DBAL\Connection; + +use function array_intersect; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; +use function sprintf; +use function time; + +/** + * Converts old sub-palette settings in "tl_metamodel_dcasetting" into proper + * property-value conditions in "tl_metamodel_dcasetting_condition" and drops + * the legacy "subpalette" column afterwards. + */ +final class SubPalettesToConditionsMigration extends AbstractMigration +{ + /** + * The database connection. + * + * @var Connection + */ + private Connection $connection; + + /** @var list */ + private array $existsCache = []; + + /** + * Create a new instance. + * + * @param Connection $connection The database connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + #[\Override] + public function getName(): string + { + return 'MetaModels: Migrate sub-palettes to input field conditions in "tl_metamodel_dcasetting".'; + } + + #[\Override] + public function shouldRun(): bool + { + if (!$this->tablesExist(['tl_metamodel_dcasetting'])) { + return false; + } + + $columnNames = array_keys( + $this->connection->createSchemaManager()->listTableColumns('tl_metamodel_dcasetting') + ); + + return in_array('subpalette', $columnNames, true); + } + + #[\Override] + public function run(): MigrationResult + { + $this->ensureConditionTableExists(); + $count = $this->migrateSubPaletteRows(); + $this->connection->executeStatement( + 'ALTER TABLE `tl_metamodel_dcasetting` DROP COLUMN `subpalette`' + ); + + return new MigrationResult( + true, + sprintf( + 'Migrated %d sub-palette row(s) to conditions and dropped "subpalette" column.', + $count + ) + ); + } + + private function ensureConditionTableExists(): void + { + if ($this->tablesExist(['tl_metamodel_dcasetting_condition'])) { + return; + } + + $this->connection->executeStatement( + 'CREATE TABLE `tl_metamodel_dcasetting_condition` ( + `id` int(10) unsigned NOT NULL auto_increment, + `pid` int(10) unsigned NOT NULL default \'0\', + `settingId` int(10) unsigned NOT NULL default \'0\', + `sorting` int(10) unsigned NOT NULL default \'0\', + `tstamp` int(10) unsigned NOT NULL default \'0\', + `enabled` char(1) NOT NULL default \'\', + `type` varchar(255) NOT NULL default \'\', + `attr_id` int(10) unsigned NOT NULL default \'0\', + `comment` varchar(255) NOT NULL default \'\', + `value` blob NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' + ); + $this->existsCache = []; + } + + private function migrateSubPaletteRows(): int + { + $subpalettes = $this->connection->fetchAllAssociative( + 'SELECT * FROM `tl_metamodel_dcasetting` WHERE `subpalette` != 0' + ); + + if ([] === $subpalettes) { + return 0; + } + + // Build attr_id lookup: dcasetting.id → dcasetting.attr_id (for non-subpalette attribute settings). + $checkboxRows = $this->connection->fetchAllAssociative( + "SELECT `id`, `attr_id` FROM `tl_metamodel_dcasetting` WHERE `subpalette`=0 AND `dcatype`='attribute'" + ); + $checkboxAttrById = []; + foreach ($checkboxRows as $row) { + $checkboxAttrById[(int) $row['id']] = (int) $row['attr_id']; + } + + // Build colName lookup: attr_id → colName. + $attributeRows = $this->connection->fetchAllAssociative( + 'SELECT `attribute`.`id`, `attribute`.`colName` + FROM `tl_metamodel_dcasetting` AS `setting` + LEFT JOIN `tl_metamodel_attribute` AS `attribute` ON (`setting`.`attr_id` = `attribute`.`id`) + WHERE `setting`.`dcatype` = \'attribute\'' + ); + $colNameByAttrId = []; + foreach ($attributeRows as $row) { + $colNameByAttrId[(int) $row['id']] = $row['colName']; + } + + $count = 0; + foreach ($subpalettes as $subpalette) { + $parentSettingId = (int) $subpalette['subpalette']; + $parentAttrId = $checkboxAttrById[$parentSettingId] ?? 0; + $parentColName = $colNameByAttrId[$parentAttrId] ?? ''; + + $this->connection->insert( + 'tl_metamodel_dcasetting_condition', + [ + 'pid' => 0, + 'settingId' => (int) $subpalette['id'], + 'sorting' => 128, + 'tstamp' => time(), + 'enabled' => '1', + 'type' => 'conditionpropertyvalueis', + 'attr_id' => $parentAttrId, + 'comment' => sprintf('Only show when checkbox "%s" is checked', $parentColName), + 'value' => '1', + ] + ); + + $this->connection->update( + 'tl_metamodel_dcasetting', + ['subpalette' => 0], + ['id' => (int) $subpalette['id']] + ); + $this->connection->update( + 'tl_metamodel_dcasetting', + ['submitOnChange' => 1], + ['id' => $parentSettingId] + ); + + $count++; + } + + return $count; + } + + private function tablesExist(array $tableNames): bool + { + if ([] === $this->existsCache) { + $this->existsCache = array_values($this->connection->createSchemaManager()->listTableNames()); + } + + return count($tableNames) === count( + array_intersect($tableNames, array_map('strtolower', $this->existsCache)) + ); + } +} diff --git a/src/CoreBundle/Resources/config/services.yml b/src/CoreBundle/Resources/config/services.yml index fbc728799..5205668d6 100644 --- a/src/CoreBundle/Resources/config/services.yml +++ b/src/CoreBundle/Resources/config/services.yml @@ -296,6 +296,42 @@ services: tags: - name: contao.migration + MetaModels\CoreBundle\Migration\JumpToMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + + MetaModels\CoreBundle\Migration\DcaSettingPublishedMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + + MetaModels\CoreBundle\Migration\SubPalettesToConditionsMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + + MetaModels\CoreBundle\Migration\IsClosedMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + + MetaModels\CoreBundle\Migration\InputScreenModeMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + + MetaModels\CoreBundle\Migration\InputScreenFlagMigration: + arguments: + $connection: '@database_connection' + tags: + - name: contao.migration + MetaModels\CoreBundle\Formatter\SelectAttributeOptionLabelFormatter: public: false diff --git a/src/Helper/UpgradeHandler.php b/src/Helper/UpgradeHandler.php deleted file mode 100644 index b2a12790c..000000000 --- a/src/Helper/UpgradeHandler.php +++ /dev/null @@ -1,394 +0,0 @@ - - * @author Sven Baumann - * @author Ingolf Steinhardt - * @copyright 2012-2023 The MetaModels team. - * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later - * @filesource - */ - -namespace MetaModels\Helper; - -use Contao\Database; - -/** - * Upgrade handler class that changes structural changes in the database. - * This should rarely be necessary, but sometimes we need it. - */ -class UpgradeHandler -{ - /** - * Retrieve the database instance from Contao. - * - * @return Database - * - * @SuppressWarnings(PHPMD.ShortMethodName) - * @SuppressWarnings(PHPMD.CamelCaseMethodName) - */ - protected static function DB() - { - return Database::getInstance(); - } - - /** - * Handle database upgrade for the jumpTo field. - * - * Introduced: pre-release 1.0. - * - * If the field 'metamodel_jumpTo' does exist in tl_module or tl_content, - * it will get created and the content from jumpTo will get copied over. - * - * @return void - */ - protected static function upgradeJumpTo() - { - $objDB = self::DB(); - if ( - $objDB->tableExists('tl_content', null, true) - && !$objDB->fieldExists('metamodel_jumpTo', 'tl_content', true) - ) { - // Create the column in the database and copy the data over. - TableManipulation::createColumn( - 'tl_content', - 'metamodel_jumpTo', - 'int(10) unsigned NOT NULL default \'0\'' - ); - if ($objDB->fieldExists('jumpTo', 'tl_content', true)) { - $objDB->execute('UPDATE tl_content SET metamodel_jumpTo=jumpTo;'); - } - } - - if ( - $objDB->tableExists('tl_module', null, true) - && !$objDB->fieldExists('metamodel_jumpTo', 'tl_module', true) - ) { - // Create the column in the database and copy the data over. - TableManipulation::createColumn( - 'tl_module', - 'metamodel_jumpTo', - 'int(10) unsigned NOT NULL default \'0\'' - ); - if ($objDB->fieldExists('jumpTo', 'tl_module', true)) { - $objDB->execute('UPDATE tl_module SET metamodel_jumpTo=jumpTo;'); - } - } - } - - /** - * Handle database upgrade for the published field in tl_metamodel_dcasetting. - * - * Introduced: version 1.0.1 - * - * If the field 'published' does not exist in tl_metamodel_dcasetting, - * it will get created and all rows within that table will get initialized to 1 - * to have the prior behaviour back (everything was being published before then). - * - * @return void - */ - protected static function upgradeDcaSettingsPublished() - { - $objDB = self::DB(); - if ( - $objDB->tableExists('tl_metamodel_dcasetting', null, true) - && !$objDB->fieldExists('published', 'tl_metamodel_dcasetting', true) - ) { - // Create the column in the database and copy the data over. - TableManipulation::createColumn( - 'tl_metamodel_dcasetting', - 'published', - 'char(1) NOT NULL default \'\'' - ); - // Publish everything we had so far. - $objDB->execute('UPDATE tl_metamodel_dcasetting SET published=1;'); - } - } - - /** - * Handle database upgrade for changing sub palettes to input field conditions. - * - * @return void - */ - protected static function changeSubPalettesToConditions() - { - $objDB = self::DB(); - - // Create the table. - if (!$objDB->tableExists('tl_metamodel_dcasetting_condition')) { - $objDB->execute( - 'CREATE TABLE `tl_metamodel_dcasetting_condition` ( - `id` int(10) unsigned NOT NULL auto_increment, - `pid` int(10) unsigned NOT NULL default \'0\', - `settingId` int(10) unsigned NOT NULL default \'0\', - `sorting` int(10) unsigned NOT NULL default \'0\', - `tstamp` int(10) unsigned NOT NULL default \'0\', - `enabled` char(1) NOT NULL default \'\', - `type` varchar(255) NOT NULL default \'\', - `attr_id` int(10) unsigned NOT NULL default \'0\', - `comment` varchar(255) NOT NULL default \'\', - `value` blob NULL, - PRIMARY KEY (`id`) - )ENGINE=MyISAM DEFAULT CHARSET=utf8;' - ); - } - - if ( - $objDB->tableExists('tl_metamodel_dcasetting', null, true) - && $objDB->fieldExists('subpalette', 'tl_metamodel_dcasetting', true) - ) { - $subpalettes = $objDB->execute('SELECT * FROM tl_metamodel_dcasetting WHERE subpalette!=0'); - - if ($subpalettes->numRows) { - // Get all attribute names and setting ids. - $attributes = $objDB - ->execute(' - SELECT attr_id, colName - FROM tl_metamodel_dcasetting AS setting - LEFT JOIN tl_metamodel_attribute AS attribute - ON (setting.attr_id=attribute.id) - WHERE dcatype=\'attribute\' - '); - - $attr = array(); - while ($attributes->next()) { - /** @psalm-suppress UndefinedMagicPropertyFetch */ - $attr[$attributes->attr_id] = $attributes->colName; - } - - $checkboxes = $objDB->execute(' - SELECT * - FROM tl_metamodel_dcasetting - WHERE - subpalette=0 - AND dcatype=\'attribute\' - '); - - $check = array(); - while ($checkboxes->next()) { - /** @psalm-suppress UndefinedMagicPropertyFetch */ - $check[$checkboxes->id] = $checkboxes->attr_id; - } - - while ($subpalettes->next()) { - // Add property value condition for parent property dependency. - /** @psalm-suppress UndefinedMagicPropertyFetch */ - $data = [ - 'pid' => 0, - 'settingId' => $subpalettes->id, - 'sorting' => '128', - 'tstamp' => \time(), - 'enabled' => '1', - 'type' => 'conditionpropertyvalueis', - 'attr_id' => $check[$subpalettes->subpalette], - 'comment' => \sprintf( - 'Only show when checkbox "%s" is checked', - $attr[$check[$subpalettes->subpalette]] - ), - 'value' => '1', - ]; - - $objDB - ->prepare('INSERT INTO tl_metamodel_dcasetting_condition %s') - ->set($data) - ->execute(); - - $objDB - ->prepare('UPDATE tl_metamodel_dcasetting SET subpalette=0 WHERE id=?') - ->execute($subpalettes->id); - - $objDB - ->prepare('UPDATE tl_metamodel_dcasetting SET submitOnChange=1 WHERE id=?') - ->execute($subpalettes->subpalette); - } - } - - TableManipulation::dropColumn('tl_metamodel_dcasetting', 'subpalette', true); - } - } - - /** - * Upgrade the database to change from closed dca to editable, creatable and deletable. - * - * @return void - */ - protected static function upgradeClosed() - { - $objDB = self::DB(); - - // Change isclosed to iseditable, iscreatable and isdeleteable. - if ( - $objDB->tableExists('tl_metamodel_dca', null, true) - && !$objDB->fieldExists('iseditable', 'tl_metamodel_dca') - ) { - // Create the column in the database and copy the data over. - TableManipulation::createColumn( - 'tl_metamodel_dca', - 'iseditable', - 'char(1) NOT NULL default \'\'' - ); - TableManipulation::createColumn( - 'tl_metamodel_dca', - 'iscreatable', - 'char(1) NOT NULL default \'\'' - ); - TableManipulation::createColumn( - 'tl_metamodel_dca', - 'isdeleteable', - 'char(1) NOT NULL default \'\'' - ); - - $objDB->execute(' - UPDATE tl_metamodel_dca - SET - iseditable=isclosed^1, - iscreatable=isclosed^1, - isdeleteable=isclosed^1 - '); - - TableManipulation::dropColumn('tl_metamodel_dca', 'isclosed', true); - } - } - - /** - * Upgrade the input screens. - * - * @return void - */ - protected static function upgradeInputScreenMode() - { - $objDB = self::DB(); - if (!$objDB->tableExists('tl_metamodel_dca', null, true)) { - return; - } - - if (!$objDB->fieldExists('mode', 'tl_metamodel_dca')) { - return; - } - - // Create the fields for grouping and sorting and migrate. - if (!$objDB->fieldExists('rendermode', 'tl_metamodel_dca')) { - TableManipulation::createColumn( - 'tl_metamodel_dca', - 'rendermode', - 'varchar(12) NOT NULL default \'\'' - ); - } - - $objDB->execute('UPDATE tl_metamodel_dca SET rendermode="flat" WHERE mode IN (0,1,2,3)'); - $objDB->execute('UPDATE tl_metamodel_dca SET rendermode="parented" WHERE mode IN (4)'); - $objDB->execute('UPDATE tl_metamodel_dca SET rendermode="hierarchical" WHERE mode IN (5,6)'); - - TableManipulation::dropColumn('tl_metamodel_dca', 'mode', true); - } - - /** - * Upgrade the input screens. - * - * @return void - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected static function upgradeInputScreenFlag() - { - $objDB = self::DB(); - if (!$objDB->tableExists('tl_metamodel_dca', null, true)) { - return; - } - if (!$objDB->fieldExists('flag', 'tl_metamodel_dca')) { - return; - } - - if (!$objDB->tableExists('tl_metamodel_dca_sortgroup', null, true)) { - $objDB->execute(' - CREATE TABLE `tl_metamodel_dca_sortgroup` ( - `id` int(10) unsigned NOT NULL auto_increment, - `pid` int(10) unsigned NOT NULL default \'0\', - `sorting` int(10) unsigned NOT NULL default \'0\', - `tstamp` int(10) unsigned NOT NULL default \'0\', - `name` text NULL, - `isdefault` char(1) NOT NULL default \'\', - `ismanualsort` char(1) NOT NULL default \'\', - `rendergrouptype` varchar(10) NOT NULL default \'none\', - `rendergrouplen` int(10) unsigned NOT NULL default \'1\', - `rendergroupattr` int(10) unsigned NOT NULL default \'0\', - `rendersort` varchar(10) NOT NULL default \'asc\', - `rendersortattr` int(10) unsigned NOT NULL default \'0\', - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - '); - } - - $dca = $objDB->execute('SELECT * FROM tl_metamodel_dca'); - - while ($dca->next()) { - $renderGroupLen = 0; - /** @psalm-suppress UndefinedMagicPropertyFetch */ - if (\in_array($dca->flag, [1, 2, 3, 4])) { - $renderGroupType = 'char'; - if (\in_array($dca->flag, [1, 2])) { - $renderGroupLen = 1; - } else { - $renderGroupLen = 2; - } - } elseif (\in_array($dca->flag, [5, 6])) { - $renderGroupType = 'day'; - } elseif (\in_array($dca->flag, [7, 8])) { - $renderGroupType = 'month'; - } elseif (\in_array($dca->flag, [9, 10])) { - $renderGroupType = 'year'; - } elseif (\in_array($dca->flag, [11, 12])) { - $renderGroupType = 'digit'; - } else { - $renderGroupType = 'none'; - } - - /** @psalm-suppress UndefinedMagicPropertyFetch */ - $data = [ - 'pid' => $dca->id, - 'sorting' => 128, - 'tstamp' => time(), - 'name' => null, - 'isdefault' => '1', - 'ismanualsort' => '1', - 'rendergrouptype' => $renderGroupType, - 'rendergrouplen' => $renderGroupLen, - 'rendergroupattr' => 0, - 'rendersort' => \in_array($dca->flag, [2, 4, 6, 8, 10, 12]) ? 'desc' : 'asc', - 'rendersortattr' => 0, - ]; - - $objDB - ->prepare('INSERT INTO tl_metamodel_dca_sortgroup %s') - ->set($data) - ->execute(); - } - - TableManipulation::dropColumn('tl_metamodel_dca', 'flag', true); - } - - /** - * Perform all upgrade steps. - * - * @return void - */ - public static function perform() - { - self::upgradeJumpTo(); - self::upgradeDcaSettingsPublished(); - self::changeSubPalettesToConditions(); - self::upgradeClosed(); - self::upgradeInputScreenMode(); - self::upgradeInputScreenFlag(); - } -}