Migration, flux:field.file to inline FAL

Upgrade wizard class to convert instances of flux:field.file which is now deprecated on TYPO3 9.5 and removed on TYPO3 10.4, to instances of inline FAL relations.

Description TYPO3 EXT:flux field.file -> field.inline.fal migration script
Author TrueType
Creation date 2020-09-24T15:53:47.000Z
Extensions
  1. FluidTYPO3.Flux
Tags
  1. Content
  2. Pages
  3. Forms
Files
  1. FluxFalUpdateWizard.php
<?php
namespace MyVendor\Sitepackage\Updates;
/**
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\DBALException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Upgrade wizard which goes through all files referenced in fe_users::image
* and creates sys_file records as well as sys_file_reference records for each hit.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FluxFalUpdateWizard implements UpgradeWizardInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Number of records fetched per database query
* Used to prevent memory overflows for huge databases
*/
const RECORDS_PER_QUERY = 5000;
/**
* @var ResourceStorage
*/
protected $storage;
/**
* Table to migrate records from
*
* @var string
*/
protected $table = 'tt_content';
/**
* Table field holding the migration to be
*
* @var string
*/
protected $fieldToMigrate = 'pi_flexform';
/**
* all fields to migrate in pi_flexform
*/
protected $piFlexformFields = [];
/**
* namespace for FLUX CEs
*/
protected $nameSpaceFluxElements = '';
/**
* the source file resides here
*
* @var string
*/
protected $sourcePath = '/';
/**
* target folder after migration
* Relative to fileadmin
*
* @var string
*/
protected $targetPath = '/';
/**
* @var Registry
*/
protected $registry;
/**
* @var string
*/
protected $registryNamespace = 'FluxFalUpdateWizard';
/**
* @var array
*/
protected $recordOffset = [];
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'fluxFalUpdateWizard';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate file relations from Flux CEs to FAL';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'This update wizard goes through all files that are referenced in the fe_users.image'
. ' field and adds the files to the FAL File Index. It also moves the files from'
. ' uploads/ to the fileadmin/_migrated/ path.';
}
/**
* Checks if an update is needed
*
* @return bool TRUE if an update is needed, FALSE otherwise
*/
public function updateNecessary(): bool
{
$this->registry = GeneralUtility::makeInstance(Registry::class);
return $this->registry->get($this->registryNamespace, 'recordOffset') === null;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update.
*
* @return bool TRUE on success, FALSE on error
*/
public function executeUpdate(): bool
{
try {
$this->init();
if (!isset($this->recordOffset[$this->table])) {
$this->recordOffset[$this->table] = 0;
}
do {
$limit = $this->recordOffset[$this->table] . ',' . self::RECORDS_PER_QUERY;
$records = $this->getRecordsFromTable($limit);
foreach ($records as $record) {
$this->migrateField($record);
}
$this->registry->set($this->registryNamespace, 'recordOffset', $this->recordOffset);
} while (count($records) === self::RECORDS_PER_QUERY);
$this->registry->remove($this->registryNamespace, 'recordOffset');
} catch (\Exception $e) {
// Silently catch db errors
echo $e->getMessage();
return false;
}
return true;
}
/**
* Initialize the storage repository.
*/
protected function init()
{
$storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
$this->storage = $storages[0];
$this->registry = GeneralUtility::makeInstance(Registry::class);
$this->recordOffset = $this->registry->get($this->registryNamespace, 'recordOffset', []);
$this->nameSpaceFluxElements = 'MyVendor.Sitepackage';
$this->piFlexformFields = [
'MyVendor.Sitepackage:HeadWithArrow.html' => [
'header-top-file',
'header-bottom-bg-file',
'header-image'
],
'MyVendor.Sitepackage:HeaderBoxThreeColumns.html' => [
'file'
],
'MyVendor.Sitepackage:HeaderBoxFourColumns.html' => [
'file'
],
'MyVendor.Sitepackage:ImageCompare.html' => [
'file-left',
'file-right'
],
];
}
/**
* Get records from table where the field to migrate is not empty (NOT NULL and != '')
* and also not numeric (which means that it is migrated)
*
* @param int $limit Maximum number records to select
* @return array
* @throws \RuntimeException
*/
protected function getRecordsFromTable($limit)
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
/** @var \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder */
$queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
try {
$queryBuilder
->select('uid', 'pid', 'tx_fed_fcefile', $this->fieldToMigrate)
->from($this->table)
->where(
$queryBuilder->expr()->like('tx_fed_fcefile', $queryBuilder->createNamedParameter($this->nameSpaceFluxElements . '%', \PDO::PARAM_STR))
)
->orWhere(
$queryBuilder->expr()->neq('tx_flux_migrated_version', $queryBuilder->createNamedParameter('fal', \PDO::PARAM_STR)),
$queryBuilder->expr()->isNull('tx_flux_migrated_version')
)
->orderBy('uid')
->setFirstResult($limit);
return $queryBuilder->execute()->fetchAll();
} catch (DBALException $e) {
throw new \RuntimeException(
'Database query failed. Error was: ' . $e->getPrevious()->getMessage(),
1476050084
);
}
}
/**
* Migrates a single field.
*
* @param array $row
*/
protected function migrateField($row)
{
// sanity checks
if (empty($row[$this->fieldToMigrate])) {
return;
}
if (empty($this->piFlexformFields[$row['tx_fed_fcefile']])) {
return;
}
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$xml = simplexml_load_string(($row[$this->fieldToMigrate]));
foreach ($this->piFlexformFields[$row['tx_fed_fcefile']] as $piFlexformField)
{
$fileNodes = $xml->xpath('//data/sheet/language/field[@index="' . $piFlexformField . '"]/value');
if (empty($fileNodes)) {
continue;
}
$fileNameWithPath = (string)$fileNodes[0];
if (substr($fileNameWithPath, 0, 10) !== 'fileadmin/') {
$this->logger->notice(
'File name does not start with /fileadmin - Reference was not migrated.',
[
'table' => $this->table,
'record' => $row,
'field' => $this->fieldToMigrate,
]
);
continue;
}
$fileNameWithPath = substr($fileNameWithPath,10);
if (!file_exists(Environment::getPublicPath() . '/fileadmin/' . $fileNameWithPath)) {
$this->logger->notice(
'File does not exist - file_exists(). Reference was not migrated.',
[
'table' => $this->table,
'record' => $row,
'field' => $this->fieldToMigrate,
]
);
continue;
}
/** @var File $file */
$file = $this->storage->getFile($fileNameWithPath);
if (empty($file)) {
$this->logger->notice(
'File does not exist - getFile(). Reference was not migrated.',
[
'table' => $this->table,
'record' => $row,
'field' => $this->fieldToMigrate,
]
);
continue;
}
$fields = [
'fieldname' => $piFlexformField,
'table_local' => 'sys_file',
'pid' => $row['pid'],
'uid_foreign' => $row['uid'],
'uid_local' => $file->getUid(),
'tablenames' => $this->table,
'crdate' => time(),
'tstamp' => time()
];
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_reference');
$queryBuilder->insert('sys_file_reference')->values($fields)->execute();
/**
* this marks the CE as processed
*/
$queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
$queryBuilder->update($this->table)
->set('tx_flux_migrated_version', 'fal')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT))
)
->execute();
//todo: update of the xml and replace of values with simple "1" counter. But data leftover should be of no harm and will be overwritten on next save of record.
}
}
}