TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Scheduler\Migration;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Serializer\DenyListDeserializer;
|
||||
use TYPO3\CMS\Core\Upgrades\DatabaseUpdatedPrerequisite;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
||||
|
||||
/**
|
||||
* @since 14.0
|
||||
* @internal This class is only meant to be used within EXT:scheduler and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('schedulerDatabaseStorageMigration')]
|
||||
class SchedulerDatabaseStorageMigration implements UpgradeWizardInterface
|
||||
{
|
||||
protected const TABLE_NAME = 'tx_scheduler_task';
|
||||
|
||||
public function __construct(private readonly DenyListDeserializer $deserializer) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate the contents of the tx_scheduler_task database table into a more structured form.';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Each Scheduler Task was previously stored in a serialized object format in the database. This update wizard migrates records of this type to a JSON-formatted storage in the database. If this wizard does not disappear, it means there are tasks that failed to be migrated and may need manual inspection or re-creation. When this happens, inspect all tasks of tx_scheduler_task where the "tasktype" column is empty.';
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return $this->hasRecordsToUpdate();
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
|
||||
$table = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->introspectSchema()->getTable(self::TABLE_NAME);
|
||||
$taskSerializer = GeneralUtility::makeInstance(TaskSerializer::class);
|
||||
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
||||
$hasFailures = false;
|
||||
foreach ($this->getRecordsToUpdate() as $record) {
|
||||
try {
|
||||
// Base migration was already done, but not the migration to additional fields, so we'll do this now
|
||||
if (!empty($record['tasktype'])) {
|
||||
$taskObject = $taskSerializer->deserialize($record);
|
||||
} else {
|
||||
// unserialize() will only give a E_NOTICE and false result, not throw an error. Silence this
|
||||
// (for tests) and operate on the "false". If future PHP promotes this to an exception, the Throwable
|
||||
// catch will kick in.
|
||||
$taskObject = $this->deserializer->deserialize($record['serialized_task_object']);
|
||||
}
|
||||
if ($taskObject instanceof AbstractTask) {
|
||||
$fieldsToUpdate = [
|
||||
'tasktype' => $taskObject->getTaskType(),
|
||||
'execution_details' => $taskObject->getExecution()?->toArray(),
|
||||
];
|
||||
$taskDetails = $taskService->getTaskDetailsFromTask($taskObject);
|
||||
$taskParameters = $taskObject->getTaskParameters();
|
||||
if (($taskDetails['isNativeTask'] ?? false) && $taskDetails['className'] !== ExecuteSchedulableCommandTask::class) {
|
||||
// map native types to real fields, and do not use the parameters' value. Only
|
||||
// exception to this are console commands, which are native types but use the
|
||||
// parameters as well, because they have dynamic configuration (arguments, options).
|
||||
if (is_array($taskDetails['additionalFields'] ?? false) && $taskDetails['additionalFields'] !== []) {
|
||||
foreach ($taskDetails['additionalFields'] as $additionalFieldName) {
|
||||
$fieldsToUpdate[$additionalFieldName] = $taskParameters[$additionalFieldName] ?? null;
|
||||
}
|
||||
}
|
||||
$fieldsToUpdate['parameters'] = null;
|
||||
} else {
|
||||
$fieldsToUpdate['parameters'] = $taskParameters;
|
||||
}
|
||||
$connection->update(
|
||||
self::TABLE_NAME,
|
||||
array_filter($fieldsToUpdate, static fn($column) => $table->hasColumn($column), ARRAY_FILTER_USE_KEY),
|
||||
['uid' => (int)$record['uid']]
|
||||
);
|
||||
} elseif ($taskObject instanceof \__PHP_Incomplete_Class) {
|
||||
$objectVars = get_mangled_object_vars($taskObject);
|
||||
$properties = [];
|
||||
$executionDetails = null;
|
||||
$taskType = null;
|
||||
foreach ($objectVars as $key => $value) {
|
||||
$key = trim($key);
|
||||
$key = trim($key, "*\0");
|
||||
$key = trim($key);
|
||||
if ($key === '__PHP_Incomplete_Class_Name') {
|
||||
$taskType = $value;
|
||||
} else {
|
||||
switch ($key) {
|
||||
case '__PHP_Incomplete_Class_Name':
|
||||
$taskType = $value;
|
||||
break;
|
||||
case 'execution':
|
||||
$executionDetails = $value;
|
||||
break;
|
||||
case 'progress':
|
||||
case 'scheduler':
|
||||
case 'taskUid':
|
||||
case 'disabled':
|
||||
case 'runOnNextCronJob': // mapped to "task_group" in the database
|
||||
case 'executionTime': // mapped to "next_execution" in the database
|
||||
case 'taskGroup': // mapped to "task_group" in the database
|
||||
case 'description':
|
||||
break;
|
||||
default:
|
||||
if (is_scalar($value) || is_null($value)) {
|
||||
$properties[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$connection->update(
|
||||
self::TABLE_NAME,
|
||||
[
|
||||
'tasktype' => $taskType,
|
||||
'parameters' => $properties,
|
||||
'execution_details' => $executionDetails?->toArray(),
|
||||
],
|
||||
['uid' => (int)$record['uid']]
|
||||
);
|
||||
} else {
|
||||
// This happens if unserialize() failed (gracefully).
|
||||
// Wizard shall not be marked as completed and show up again to let people know.
|
||||
$hasFailures = true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Mark wizard as failed so the upgrade wizard will show up again, and people know there is a problem.
|
||||
$hasFailures = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !$hasFailures;
|
||||
}
|
||||
|
||||
protected function hasRecordsToUpdate(): bool
|
||||
{
|
||||
// Check if table exists
|
||||
if (!$this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->tableExists(self::TABLE_NAME)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$this->getPreparedQueryBuilder()->count('uid')->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
protected function getRecordsToUpdate(): array
|
||||
{
|
||||
return $this->getPreparedQueryBuilder()->select('*')->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
protected function getPreparedQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$nativeTaskTypesWithAdditionalFields = $this->getAllNativeTaskTypesWithAdditionalFields();
|
||||
|
||||
$queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
// This is done by intention, so the upgrade wizard continues to work even if we introduce further TCA details for tx_scheduler_task
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->or(
|
||||
// Find all where the task type is empty (legacy serialized storage)
|
||||
// OR where we have a native task type, that contains additional fields we can migrate
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq(
|
||||
'tasktype',
|
||||
$queryBuilder->createNamedParameter('')
|
||||
),
|
||||
$queryBuilder->expr()->isNull('tasktype')
|
||||
),
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->in(
|
||||
'tasktype',
|
||||
$queryBuilder->createNamedParameter(array_keys($nativeTaskTypesWithAdditionalFields), ArrayParameterType::STRING)
|
||||
),
|
||||
$queryBuilder->expr()->isNotNull('parameters'),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
protected function getAllNativeTaskTypesWithAdditionalFields(): array
|
||||
{
|
||||
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
||||
$allTaskInformation = $taskService->getAllTaskTypes();
|
||||
$nativeTaskTypesWithAdditionalFields = [];
|
||||
foreach ($allTaskInformation as $taskType => $taskInformation) {
|
||||
if (($taskInformation['isNativeTask'] ?? false) && $taskInformation['className'] !== ExecuteSchedulableCommandTask::class) {
|
||||
// Native tasks can define "additionalFields". However, console commands, which are
|
||||
// native tasks as well, do not define real fields but use the "parameters" feature.
|
||||
$nativeTaskTypesWithAdditionalFields[$taskType] = $taskInformation['additionalFields'] ?? [];
|
||||
}
|
||||
}
|
||||
return $nativeTaskTypesWithAdditionalFields;
|
||||
}
|
||||
|
||||
protected function getConnectionPool(): ConnectionPool
|
||||
{
|
||||
return GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user