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); } }