From 8bd15ac380e4201ec57a9b649f9e7c8897cc589a Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 9 Jan 2023 11:49:51 +0000 Subject: [PATCH] Provide location update wizard --- Classes/Updates/MigrateOldLocations.php | 259 ++++++++++++++++++++++++ Documentation/Changelog/3.1.0.rst | 10 + ext_localconf.php | 2 + phpstan-baseline.neon | 37 ++++ 4 files changed, 308 insertions(+) create mode 100644 Classes/Updates/MigrateOldLocations.php diff --git a/Classes/Updates/MigrateOldLocations.php b/Classes/Updates/MigrateOldLocations.php new file mode 100644 index 0000000..6fe072c --- /dev/null +++ b/Classes/Updates/MigrateOldLocations.php @@ -0,0 +1,259 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Wrm\Events\Updates; + +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\DataHandling\DataHandler; +use TYPO3\CMS\Core\Log\Logger; +use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite; +use TYPO3\CMS\Install\Updates\UpgradeWizardInterface; + +class MigrateOldLocations implements UpgradeWizardInterface +{ + /** + * @var ConnectionPool + */ + private $connectionPool; + + /** + * @var DataHandler + */ + private $dataHandler; + + /** + * @var Logger + */ + private $logger; + + /** + * @var array + */ + private $uidsForTranslation = []; + + public function __construct( + ConnectionPool $connectionPool, + DataHandler $dataHandler, + LogManager $logManager + ) { + $this->connectionPool = $connectionPool; + $this->dataHandler = $dataHandler; + $this->logger = $logManager->getLogger(self::class); + } + + public function getTitle(): string + { + return 'Migrate EXT:event location data.'; + } + + public function getDescription(): string + { + return 'Checks for legacy location data stored within events and will create dedicated location records and relations.'; + } + + public function updateNecessary(): bool + { + return $this->getQueryBuilder() + ->count('*') + ->execute() + ->fetchOne() > 0 + ; + } + + public function executeUpdate(): bool + { + $result = $this->getQueryBuilder()->execute(); + foreach ($result as $eventRecord) { + $this->logger->info('Updating event record.', ['record' => $eventRecord]); + $eventRecord['location'] = $this->getLocationUid($eventRecord); + $this->uidsForTranslation[$eventRecord['uid'] . '-' . $eventRecord['sys_language_uid']] = $eventRecord['location']; + $this->updateEvent($eventRecord); + } + return true; + } + + private function getLocationUid(array $event): int + { + $existingUid = $this->getExitingLocationUid($event); + if ($existingUid > 0) { + $this->logger->info('Location already exists', ['uid' => $existingUid, 'event' => $event]); + return $existingUid; + } + + return $this->createLocation($event); + } + + private function getExitingLocationUid(array $event): int + { + $oldColumns = [ + 'sys_language_uid' => 'sys_language_uid', + 'zzz_deleted_name' => 'name', + 'zzz_deleted_street' => 'street', + 'zzz_deleted_district' => 'district', + 'zzz_deleted_city' => 'city', + 'zzz_deleted_zip' => 'zip', + 'zzz_deleted_country' => 'country', + 'zzz_deleted_phone' => 'phone', + 'zzz_deleted_latitude' => 'latitude', + 'zzz_deleted_longitude' => 'longitude', + ]; + $qb = $this->connectionPool->getQueryBuilderForTable('tx_events_domain_model_location'); + $qb->select('uid', 'l10n_parent'); + $qb->from('tx_events_domain_model_location'); + foreach ($oldColumns as $oldName => $newName) { + $qb->andWhere($qb->expr()->eq($newName, $qb->createNamedParameter($event[$oldName]))); + } + + $uids = $qb->execute()->fetchAssociative(); + if (is_bool($uids)) { + return 0; + } + + return $uids['l10n_parent'] ?: $uids['uid']; + } + + private function createLocation(array $event): int + { + $this->logger->info('Location will be created.', ['event' => $event]); + + $record = [ + 'pid' => $event['pid'], + 'sys_language_uid' => $event['sys_language_uid'], + 'name' => $event['zzz_deleted_name'], + 'street' => $event['zzz_deleted_street'], + 'district' => $event['zzz_deleted_district'], + 'city' => $event['zzz_deleted_city'], + 'zip' => $event['zzz_deleted_zip'], + 'country' => $event['zzz_deleted_country'], + 'phone' => $event['zzz_deleted_phone'], + 'latitude' => $event['zzz_deleted_latitude'], + 'longitude' => $event['zzz_deleted_longitude'], + ]; + $recordUid = 'NEW12121'; + $l10nParentUid = $this->uidsForTranslation[$event['l10n_parent'] . '-0'] ?? 0; + $dataHandler = clone $this->dataHandler; + + if ($event['sys_language_uid'] > 0 && $l10nParentUid > 0) { + $this->logger->info('Foreign language, create translation.', [ + 'l10nParentUid' => $l10nParentUid, + 'event' => $event, + ]); + + $dataHandler->start([], [ + 'tx_events_domain_model_location' => [ + $l10nParentUid => [ + 'localize' => $event['sys_language_uid'], + ], + ], + ]); + $dataHandler->process_cmdmap(); + $recordUid = $dataHandler->copyMappingArray_merged['tx_events_domain_model_location'][$l10nParentUid] ?? 0; + } + + $this->logger->info('Create or update loation.', [ + 'recordUid' => $recordUid, + 'l10nParentUid' => $l10nParentUid, + 'event' => $event, + 'record' => $record, + ]); + + $dataHandler->start([ + 'tx_events_domain_model_location' => [ + $recordUid => $record, + ], + ], []); + $dataHandler->process_datamap(); + + $uid = $dataHandler->substNEWwithIDs[$recordUid] ?? 0; + $this->logger->info('Created or updated location.', [ + 'uid' => $uid, + ]); + if ($uid > 0) { + return $uid; + } + if ($l10nParentUid > 0) { + return $l10nParentUid; + } + + throw new \Exception('Could not create location: ' . implode(', ', $dataHandler->errorLog), 1672916613); + } + + private function updateEvent(array $event): void + { + $this->connectionPool + ->getConnectionForTable('tx_events_domain_model_event') + ->update( + 'tx_events_domain_model_event', + ['location' => $event['location']], + ['uid' => $event['uid']] + ) + ; + } + + private function getQueryBuilder(): QueryBuilder + { + $oldColumns = [ + 'zzz_deleted_name', + 'zzz_deleted_street', + 'zzz_deleted_district', + 'zzz_deleted_city', + 'zzz_deleted_zip', + 'zzz_deleted_country', + 'zzz_deleted_phone', + 'zzz_deleted_latitude', + 'zzz_deleted_longitude', + ]; + $qb = $this->connectionPool->getQueryBuilderForTable('tx_events_domain_model_event'); + $qb->getRestrictions()->removeAll(); + $qb->select(...$oldColumns); + $qb->addSelect('uid', 'pid', 'sys_language_uid', 'l10n_parent'); + $qb->from('tx_events_domain_model_event'); + foreach ($oldColumns as $columnName) { + $qb->orWhere($qb->expr()->neq($columnName, $qb->createNamedParameter(''))); + } + $qb->orderBy('sys_language_uid', 'ASC'); + $qb->addOrderBy('l10n_parent', 'ASC'); + $qb->addOrderBy('uid', 'ASC'); + + return $qb; + } + + public function getIdentifier(): string + { + return self::class; + } + + public function getPrerequisites(): array + { + return [ + DatabaseUpdatedPrerequisite::class, + ]; + } + + public static function register(): void + { + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][self::class] = self::class; + } +} diff --git a/Documentation/Changelog/3.1.0.rst b/Documentation/Changelog/3.1.0.rst index ea74971..4866a0d 100644 --- a/Documentation/Changelog/3.1.0.rst +++ b/Documentation/Changelog/3.1.0.rst @@ -16,6 +16,16 @@ Features * Pages can now define they store event records. A new entry was added to select and the icon within page tree will be replaced. +* Provided update wizard to migrate old location data to new one. + It is important to execute the database compare once, so old columns are prefixed but not removed. + Execute wizard and you are done. + Locations will be extracted from old events and placed on same pid. + You might wanna move all locations to a dedicated new pid in the end, e.g. with an sql query (Replace 2 by your page uid): + + .. code-block:: sql + + UPDATE tx_events_domain_model_location SET pid = 2 + Fixes ----- diff --git a/ext_localconf.php b/ext_localconf.php index 4db4316..8ac73bb 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -50,4 +50,6 @@ call_user_func(function () { \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, ['source' => 'EXT:events/Resources/Public/Icons/Folder.svg'] ); + + \Wrm\Events\Updates\MigrateOldLocations::register(); }); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ce19eb3..4ecec40 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4,10 +4,12 @@ parameters: message: "#^Cannot call method typoLink_URL\\(\\) on TYPO3\\\\CMS\\\\Frontend\\\\ContentObject\\\\ContentObjectRenderer\\|null\\.$#" count: 1 path: Classes/Controller/DateController.php + - message: "#^Parameter \\#1 \\$categories of method Wrm\\\\Events\\\\Domain\\\\Model\\\\Event\\:\\:setCategories\\(\\) expects TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\ObjectStorage\\, TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\ObjectStorage\\ given\\.$#" count: 1 path: Classes/Service/DestinationDataImportService.php + - message: "#^Parameter \\#2 \\$array of function array_map expects array, mixed given\\.$#" count: 1 @@ -38,3 +40,38 @@ parameters: count: 2 path: Classes/Service/DestinationDataImportService/CategoriesAssignment.php + - + message: "#^Argument of an invalid type Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int supplied for foreach, only iterables are supported\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Argument of an invalid type Doctrine\\\\DBAL\\\\Result\\|int supplied for foreach, only iterables are supported\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Method Wrm\\\\Events\\\\Updates\\\\MigrateOldLocations\\:\\:getExitingLocationUid\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Offset 'sys_language_uid' does not exist on array\\{location\\: int\\}\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php + + - + message: "#^Offset 'uid' does not exist on array\\{location\\: int\\}\\.$#" + count: 1 + path: Classes/Updates/MigrateOldLocations.php +