setHelp( 'Transfers form definitions from one storage backend to another.' . LF . LF . 'Available storage types depend on the installed adapters. Core provides:' . LF . ' - database: Database storage (default target)' . LF . ' - extension: Extension paths (EXT:...)' . LF . ' - filemount: File mount storage (deprecated since v14.2)' . LF . LF . 'Target location (--target-location / -l) per storage type:' . LF . ' - database: Always "0" (fixed; forms are stored at the root level).' . LF . ' - extension: An EXT: path configured in "persistenceManager.allowedExtensionPaths",' . LF . ' with "persistenceManager.allowSaveToExtensionPaths: true" set.' . LF . ' e.g. --target-location="EXT:my_extension/Resources/Private/Forms/"' . LF . LF . 'Use --dry-run to preview which forms would be transferred.' . LF . 'Use --move to delete the source form after successful transfer.' . LF . LF . 'After a successful transfer, content element references in "tt_content" are automatically' . LF . 'updated to point to the new storage location. No other tables are updated.' ) ->addOption( 'source', null, InputOption::VALUE_REQUIRED, 'Source storage type identifier (e.g., "extension", "filemount", "database").', ) ->addOption( 'target', null, InputOption::VALUE_REQUIRED, 'Target storage type identifier (e.g., "database", "extension").', ) ->addOption( 'target-location', 'l', InputOption::VALUE_REQUIRED, 'Target storage location. For "database": always "0". For "extension": EXT: path from allowedExtensionPaths.', '0', ) ->addOption( 'form-identifier', 'f', InputOption::VALUE_REQUIRED, 'Transfer only the form with this identifier. If omitted, all forms from the source are transferred.', ) ->addOption( 'move', 'm', InputOption::VALUE_NONE, 'Delete the source form after successful transfer (move operation).', ) ->addOption( 'dry-run', null, InputOption::VALUE_NONE, 'Only list forms that would be transferred without making changes.', ); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); // @todo: ConfigurationManager triggered by PersistenceConfigurationService needs a Request $request = (new ServerRequest('https://localhost/', 'GET')); $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE) ->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $GLOBALS['TYPO3_REQUEST'] = $request; $io = new SymfonyStyle($input, $output); $sourceType = $input->getOption('source'); $targetType = $input->getOption('target'); $targetLocation = $input->getOption('target-location'); $formIdentifier = $input->getOption('form-identifier'); $isMove = (bool)$input->getOption('move'); $isDryRun = (bool)$input->getOption('dry-run'); if ($sourceType === null || $targetType === null) { $io->error('Both --source and --target options are required.'); $io->note(sprintf( 'Available storage types: %s', implode(', ', $this->formTransferService->getAvailableStorageTypes()), )); return Command::FAILURE; } if (!$this->formTransferService->hasStorageType($sourceType)) { $io->error(sprintf( 'Unknown source storage type "%s". Available types: %s', $sourceType, implode(', ', $this->formTransferService->getAvailableStorageTypes()), )); return Command::FAILURE; } if (!$this->formTransferService->hasStorageType($targetType)) { $io->error(sprintf( 'Unknown target storage type "%s". Available types: %s', $targetType, implode(', ', $this->formTransferService->getAvailableStorageTypes()), )); return Command::FAILURE; } if ($sourceType === $targetType && $formIdentifier === null) { $io->error('Source and target storage types are identical. Use --form-identifier to transfer a specific form, or choose different storage types.'); return Command::FAILURE; } $targetAdapter = $this->formTransferService->getAdapter($targetType); if (!$targetAdapter->isAllowedStorageLocation($targetLocation)) { $hint = match ($targetType) { 'database' => 'For database storage the only valid location is "0" (default). The option can be omitted.', 'extension' => 'For extension storage, provide an EXT: path that is registered in "persistenceManager.allowedExtensionPaths"' . LF . 'and ensure "persistenceManager.allowSaveToExtensionPaths: true" is set in your form YAML setup.' . LF . 'Example: --target-location="EXT:my_extension/Resources/Private/Forms/"', default => 'Check the storage adapter documentation for valid location formats.', }; $io->error(sprintf( 'The target location "%s" is not valid for the "%s" storage adapter.' . LF . '%s', $targetLocation, $targetType, $hint, )); return Command::FAILURE; } $sourceForms = $this->formTransferService->listSourceForms($sourceType, $formIdentifier); if ($sourceForms === []) { $message = $formIdentifier !== null ? sprintf('No form with identifier "%s" found in "%s" storage.', $formIdentifier, $sourceType) : sprintf('No forms found in "%s" storage.', $sourceType); $io->warning($message); return Command::SUCCESS; } $operation = $isMove ? 'move' : 'transfer'; $io->section(sprintf( 'Found %d form(s) to %s from "%s" to "%s"', count($sourceForms), $operation, $sourceType, $targetType, )); if ($isDryRun) { $rows = []; foreach ($sourceForms as $form) { $rows[] = [ $form->identifier, $form->name, $form->persistenceIdentifier ?? '-', 'would ' . $operation . '', ]; } $io->table(['Identifier', 'Name', 'Source', 'Status'], $rows); $io->note('Dry-run mode: no forms were transferred.'); return Command::SUCCESS; } $transferred = 0; $failed = 0; $results = []; $migrationMap = []; foreach ($sourceForms as $form) { try { $result = $this->formTransferService->transferForm( $form, $sourceType, $targetType, $targetLocation, $isMove, ); $status = 'success'; if ($isMove && $result->sourceDeleted) { $status = 'moved'; } elseif ($isMove && $result->deletionError !== null) { $status = 'transferred, source deletion failed: ' . $result->deletionError . ''; } $results[] = [ $result->formIdentifier, $result->formName, $result->sourceIdentifier, $result->targetIdentifier, $status, ]; $migrationMap[$result->sourceIdentifier] = $result->targetIdentifier; $transferred++; } catch (\Exception $e) { $results[] = [ $form->identifier, $form->name, $form->persistenceIdentifier ?? '-', '-', '' . $e->getMessage() . '', ]; $failed++; if ($output->isVerbose()) { $io->error(sprintf('Failed to transfer "%s": %s', $form->identifier, $e->getMessage())); } } } $io->table(['Identifier', 'Name', 'Source', 'Target', 'Status'], $results); if ($migrationMap !== []) { $referencesUpdated = $this->formTransferService->updateContentElementReferences($migrationMap); if ($referencesUpdated > 0) { $io->note(sprintf('Updated %d content element reference(s).', $referencesUpdated)); } } if ($transferred > 0) { $verb = $isMove ? 'moved' : 'transferred'; $io->success(sprintf('Successfully %s %d form(s) from "%s" to "%s".', $verb, $transferred, $sourceType, $targetType)); } if ($failed > 0) { $io->warning(sprintf('Failed to transfer %d form(s).', $failed)); } return $failed > 0 ? Command::FAILURE : Command::SUCCESS; } }