TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
<?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\Form\Upgrades;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Upgrades\ConfirmableInterface;
|
||||
use TYPO3\CMS\Core\Upgrades\Confirmation;
|
||||
use TYPO3\CMS\Core\Upgrades\DatabaseUpdatedPrerequisite;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\PersistenceConfigurationService;
|
||||
use TYPO3\CMS\Form\Domain\DTO\FormData;
|
||||
use TYPO3\CMS\Form\Domain\DTO\FormMetadata;
|
||||
use TYPO3\CMS\Form\Domain\Repository\FormDefinitionRepository;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\YamlSource;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||
use TYPO3\CMS\Form\Service\FormTransferService;
|
||||
|
||||
/**
|
||||
* Migrate file-based form definitions (YAML) to database storage.
|
||||
*
|
||||
* This wizard reads all form definitions from the paths configured in
|
||||
* `persistenceManager.allowedFileMounts` (deprecated since v14.2),
|
||||
* imports them into the form_definition database table,
|
||||
* and updates all tt_content references (FlexForm persistenceIdentifier) to
|
||||
* point to the new database records.
|
||||
*
|
||||
* After successful migration, the original YAML files are deleted from
|
||||
* file storage to avoid duplicates.
|
||||
*
|
||||
* When removing this class in v16, also remove all classes and methods tagged with
|
||||
* `@deprecated Remove in v16 along with the FileFormsToDatabaseUpgradeWizard`.
|
||||
*
|
||||
* @since 14.2
|
||||
* @internal
|
||||
*/
|
||||
#[UpgradeWizard('formFileFormsToDatabaseMigration')]
|
||||
final readonly class FileFormsToDatabaseUpgradeWizard implements UpgradeWizardInterface, ConfirmableInterface
|
||||
{
|
||||
public function __construct(
|
||||
private FormDefinitionRepository $formDefinitionRepository,
|
||||
private FormTransferService $formTransferService,
|
||||
private LoggerInterface $logger,
|
||||
private YamlSource $yamlSource,
|
||||
private ResourceFactory $resourceFactory,
|
||||
private PersistenceConfigurationService $storageConfiguration,
|
||||
private StorageRepository $storageRepository,
|
||||
) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate file-based forms to database storage';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
$forms = $this->getFileBasedForms();
|
||||
$count = count($forms);
|
||||
|
||||
if ($count === 0) {
|
||||
return 'No file-based form definitions found.';
|
||||
}
|
||||
|
||||
$description = sprintf(
|
||||
'Found %d file-based form definition(s) that will be migrated to database storage:',
|
||||
$count
|
||||
);
|
||||
|
||||
foreach ($forms as $form) {
|
||||
$description .= LF . sprintf(
|
||||
' • %s (%s)',
|
||||
$form->name,
|
||||
$form->persistenceIdentifier ?? $form->identifier
|
||||
);
|
||||
}
|
||||
|
||||
$description .= LF . LF . 'After migration, all tt_content references will be updated automatically.';
|
||||
$description .= LF . LF . 'Note: Only references in tt_content (CType form_formframework) are updated '
|
||||
. 'automatically. If your installation uses form persistence identifiers in custom '
|
||||
. 'database tables or FlexForm fields outside tt_content, these references must be '
|
||||
. 'updated manually.';
|
||||
$description .= LF . LF . 'The original YAML files WILL BE DELETED after successful migration. '
|
||||
. 'Please ensure backups exist. Failures will be logged and can be found in their '
|
||||
. 'configured log locations after execution, and should be reviewed.';
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
public function getConfirmation(): Confirmation
|
||||
{
|
||||
return new Confirmation(
|
||||
'Migrate forms to database?',
|
||||
'This will move all filemount-based form definitions into the database '
|
||||
. 'and update content element references in tt_content. '
|
||||
. 'References in custom database tables or FlexForm fields outside tt_content '
|
||||
. 'are NOT updated automatically and must be migrated manually. '
|
||||
. 'The original YAML files will be deleted after successful migration. '
|
||||
. 'YAML files in extension directories are not affected. '
|
||||
. 'Please make sure you have a backup before proceeding.',
|
||||
false,
|
||||
'Yes, migrate forms to database',
|
||||
'No, keep file-based storage'
|
||||
);
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return $this->getFileBasedForms() !== [];
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$forms = $this->getFileBasedForms();
|
||||
|
||||
if ($forms === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$success = true;
|
||||
$migrationMap = [];
|
||||
$migratedFiles = [];
|
||||
|
||||
foreach ($forms as $form) {
|
||||
$persistenceIdentifier = $form->persistenceIdentifier ?? $form->identifier;
|
||||
|
||||
try {
|
||||
$formData = $this->readForm($persistenceIdentifier);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to load form definition from "{identifier}": {message}', [
|
||||
'identifier' => $persistenceIdentifier,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
$success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this form was already migrated (by identifier)
|
||||
$existingUid = $this->formDefinitionRepository->findUidByFormIdentifier($formData->identifier);
|
||||
if ($existingUid !== null) {
|
||||
$this->logger->info('Form "{identifier}" already exists in database, skipping.', [
|
||||
'identifier' => $formData->identifier,
|
||||
]);
|
||||
// Still track mapping for reference updates
|
||||
$migrationMap[$persistenceIdentifier] = $existingUid;
|
||||
$migratedFiles[] = $persistenceIdentifier;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write to database using raw insert (no DataHandler required,
|
||||
// so this works in Install Tool context without a backend user)
|
||||
try {
|
||||
$newUid = $this->formDefinitionRepository->addRaw(0, $formData);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Database insert failed for form "{identifier}": {message}', [
|
||||
'identifier' => $formData->identifier,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
$newUid = null;
|
||||
}
|
||||
|
||||
if ($newUid === null) {
|
||||
$this->logger->error('Failed to insert form "{identifier}" into database.', [
|
||||
'identifier' => $formData->identifier,
|
||||
'persistenceIdentifier' => $persistenceIdentifier,
|
||||
]);
|
||||
$success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrationMap[$persistenceIdentifier] = $newUid;
|
||||
$migratedFiles[] = $persistenceIdentifier;
|
||||
|
||||
$this->logger->info('Migrated form "{identifier}" from "{file}" to database UID {uid}.', [
|
||||
'identifier' => $formData->identifier,
|
||||
'file' => $persistenceIdentifier,
|
||||
'uid' => $newUid,
|
||||
]);
|
||||
}
|
||||
|
||||
// Update tt_content FlexForm references
|
||||
if ($migrationMap !== []) {
|
||||
// Convert int UIDs to string for the service (persistenceIdentifier values are always strings)
|
||||
$stringMap = array_map(strval(...), $migrationMap);
|
||||
$referencesUpdated = $this->formTransferService->updateContentElementReferences($stringMap);
|
||||
$this->logger->info('Updated {count} content element reference(s).', [
|
||||
'count' => $referencesUpdated,
|
||||
]);
|
||||
}
|
||||
|
||||
// Delete original YAML files only when all forms were migrated successfully.
|
||||
// If any migration failed, keep all files to allow re-running the wizard.
|
||||
if ($success && $migratedFiles !== []) {
|
||||
$deletedCount = $this->deleteOriginalFiles($migratedFiles);
|
||||
$this->logger->info('Deleted {count} of {total} original YAML file(s).', [
|
||||
'count' => $deletedCount,
|
||||
'total' => count($migratedFiles),
|
||||
]);
|
||||
} elseif (!$success && $migratedFiles !== []) {
|
||||
$this->logger->warning(
|
||||
'Some forms could not be migrated. Original YAML files were kept to allow re-running the wizard.'
|
||||
);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all YAML form definitions in configured file mounts.
|
||||
*
|
||||
* @return list<FormMetadata>
|
||||
*/
|
||||
private function getFileBasedForms(): array
|
||||
{
|
||||
try {
|
||||
$results = [];
|
||||
foreach ($this->retrieveYamlFilesFromStorageFolders() as $file) {
|
||||
$formMetadata = $this->loadMetaData($file);
|
||||
|
||||
if (!$this->looksLikeAFormDefinition($formMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->hasValidFileExtension($file->getCombinedIdentifier())) {
|
||||
continue;
|
||||
}
|
||||
$results[] = $formMetadata;
|
||||
}
|
||||
return $results;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Could not list file-based forms: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete original YAML files from file storage after successful migration.
|
||||
*
|
||||
* Files that cannot be deleted (e.g. due to permissions) are logged but
|
||||
* do not cause the overall migration to fail — the database records are
|
||||
* already the authoritative source at this point.
|
||||
*
|
||||
* @param list<string> $persistenceIdentifiers Combined identifiers (e.g. "1:/form_definitions/contact.form.yaml")
|
||||
* @return int Number of successfully deleted files
|
||||
*/
|
||||
private function deleteOriginalFiles(array $persistenceIdentifiers): int
|
||||
{
|
||||
$deletedCount = 0;
|
||||
|
||||
foreach ($persistenceIdentifiers as $persistenceIdentifier) {
|
||||
try {
|
||||
$this->deleteForm($persistenceIdentifier);
|
||||
$deletedCount++;
|
||||
|
||||
$this->logger->info('Deleted original YAML file "{identifier}".', [
|
||||
'identifier' => $persistenceIdentifier,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Could not delete original YAML file "{identifier}": {message}', [
|
||||
'identifier' => $persistenceIdentifier,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $deletedCount;
|
||||
}
|
||||
|
||||
private function readForm(string $identifier): FormData
|
||||
{
|
||||
$file = $this->retrieveFileByPersistenceIdentifier($identifier);
|
||||
$formDefinition = $this->yamlSource->load([$file]);
|
||||
$this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($formDefinition, $identifier);
|
||||
return FormData::fromArray($formDefinition);
|
||||
}
|
||||
|
||||
private function deleteForm(string $identifier): void
|
||||
{
|
||||
if (!$this->hasValidFileExtension($identifier)) {
|
||||
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $identifier), 1472239534);
|
||||
}
|
||||
if (!$this->exists($identifier)) {
|
||||
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $identifier), 1764879545);
|
||||
}
|
||||
[$storageUid, $fileIdentifier] = explode(':', $identifier, 2);
|
||||
$storage = $this->getStorageByUid((int)$storageUid);
|
||||
$file = $storage->getFile($fileIdentifier);
|
||||
if (!$storage->checkFileActionPermission('delete', $file)) {
|
||||
throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $identifier), 1472239516);
|
||||
}
|
||||
$storage->deleteFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PersistenceManagerException
|
||||
* @throws NoSuchFileException
|
||||
*/
|
||||
private function retrieveFileByPersistenceIdentifier(string $identifier): File
|
||||
{
|
||||
$this->ensureValidPersistenceIdentifier($identifier);
|
||||
try {
|
||||
$file = $this->resourceFactory->retrieveFileOrFolderObject($identifier);
|
||||
} catch (\Exception) {
|
||||
// Top level catch to ensure useful following exception handling, because FAL throws top level exceptions.
|
||||
$file = null;
|
||||
}
|
||||
if ($file === null) {
|
||||
throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $identifier), 1524684442);
|
||||
}
|
||||
if (!$file->getStorage()->checkFileActionPermission('read', $file)) {
|
||||
throw new PersistenceManagerException(sprintf('No read access to file "%s".', $identifier), 1471630578);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
private function ensureValidPersistenceIdentifier(string $identifier): void
|
||||
{
|
||||
if (pathinfo($identifier, PATHINFO_EXTENSION) !== 'yaml') {
|
||||
throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $identifier), 1477679819);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
private function generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension(array $formDefinition, string $identifier): void
|
||||
{
|
||||
if ($this->looksLikeAFormDefinitionArray($formDefinition) && !$this->hasValidFileExtension($identifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Form definition "%s" does not end with ".form.yaml".', $identifier), 1780660703);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if array looks like a form definition
|
||||
*/
|
||||
private function looksLikeAFormDefinitionArray(array $data): bool
|
||||
{
|
||||
return !empty($data['identifier']) && trim($data['type'] ?? '') === 'Form';
|
||||
}
|
||||
|
||||
private function hasValidFileExtension(string $identifier): bool
|
||||
{
|
||||
return str_ends_with($identifier, FormPersistenceManagerInterface::FORM_DEFINITION_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
private function exists(string $identifier): bool
|
||||
{
|
||||
$exists = false;
|
||||
if ($this->hasValidFileExtension($identifier) && $this->pathIsIntendedAsFileMountPath($identifier)) {
|
||||
[$storageUid, $fileIdentifier] = explode(':', $identifier, 2);
|
||||
$storage = $this->getStorageByUid((int)$storageUid);
|
||||
$exists = $storage->hasFile($fileIdentifier);
|
||||
}
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ResourceStorage for a given uid
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
private function getStorageByUid(int $storageUid): ResourceStorage
|
||||
{
|
||||
$storage = $this->storageRepository->findByUid($storageUid);
|
||||
if (!$storage?->isBrowsable()) {
|
||||
throw new PersistenceManagerException(sprintf('Could not access storage with uid "%d".', $storageUid), 1471630581);
|
||||
}
|
||||
return $storage;
|
||||
}
|
||||
|
||||
private function pathIsIntendedAsFileMountPath(string $path): bool
|
||||
{
|
||||
if (empty($path)) {
|
||||
return false;
|
||||
}
|
||||
[$storageUid, $pathIdentifier] = explode(':', $path, 2);
|
||||
if (empty($storageUid) || empty($pathIdentifier)) {
|
||||
return false;
|
||||
}
|
||||
return MathUtility::canBeInterpretedAsInteger($storageUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves yaml files from storage folders for further processing.
|
||||
* At this time it's not determined yet, whether these files contain form data.
|
||||
*
|
||||
* @return File[]
|
||||
*/
|
||||
private function retrieveYamlFilesFromStorageFolders(): array
|
||||
{
|
||||
$filesFromStorageFolders = [];
|
||||
$fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
|
||||
$fileExtensionFilter->setAllowedFileExtensions(['yaml']);
|
||||
foreach ($this->getAccessibleFormStorageFolders() as $folder) {
|
||||
$storage = $folder->getStorage();
|
||||
$storage->setFileAndFolderNameFilters([
|
||||
[$fileExtensionFilter, 'filterFileList'],
|
||||
]);
|
||||
$files = $folder->getFiles(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, true);
|
||||
array_push($filesFromStorageFolders, ...array_values($files));
|
||||
$storage->resetFileAndFolderNameFiltersToDefault();
|
||||
}
|
||||
return $filesFromStorageFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all accessible file mountpoints for the
|
||||
* current backend user.
|
||||
*
|
||||
* Only registered mount points from
|
||||
* persistenceManager.allowedFileMounts
|
||||
* are listed.
|
||||
*
|
||||
* @return Folder[]
|
||||
*/
|
||||
private function getAccessibleFormStorageFolders(): array
|
||||
{
|
||||
$storageFolders = [];
|
||||
$allowedFileMounts = $this->storageConfiguration->getPersistenceManagerConfiguration()->allowedFileMounts;
|
||||
|
||||
if (empty($allowedFileMounts)) {
|
||||
return $storageFolders;
|
||||
}
|
||||
|
||||
foreach ($allowedFileMounts as $allowedFileMount) {
|
||||
$allowedFileMount = rtrim($allowedFileMount, '/') . '/';
|
||||
[$storageUid, $fileMountPath] = explode(':', $allowedFileMount, 2);
|
||||
try {
|
||||
$storage = $this->getStorageByUid((int)$storageUid);
|
||||
} catch (PersistenceManagerException) {
|
||||
continue;
|
||||
}
|
||||
$isStorageFileMount = false;
|
||||
$parentFolder = $storage->getRootLevelFolder(false);
|
||||
foreach ($storage->getFileMounts() as $storageFileMount) {
|
||||
$storageFileMountFolder = $storageFileMount['folder'];
|
||||
// Normally should use ResourceStorage::isWithinFolder() to check if the configured file mount path is within
|
||||
// a storage file mount but this requires a valid Folder object and thus a directory which already exists.
|
||||
// And the folder could simply not exist yet.
|
||||
if (str_starts_with($fileMountPath, $storageFileMountFolder->getIdentifier())) {
|
||||
$isStorageFileMount = true;
|
||||
$parentFolder = $storageFileMountFolder;
|
||||
}
|
||||
}
|
||||
// Get storage folder object, create it if missing
|
||||
try {
|
||||
$fileMountFolder = $storage->getFolder($fileMountPath);
|
||||
} catch (InsufficientFolderAccessPermissionsException) {
|
||||
continue;
|
||||
} catch (FolderDoesNotExistException) {
|
||||
if ($isStorageFileMount) {
|
||||
$fileMountPath = substr(
|
||||
$fileMountPath,
|
||||
strlen($parentFolder->getIdentifier())
|
||||
);
|
||||
}
|
||||
try {
|
||||
$fileMountFolder = $storage->createFolder($fileMountPath, $parentFolder);
|
||||
} catch (InsufficientFolderAccessPermissionsException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$storageFolders[$allowedFileMount] = $fileMountFolder;
|
||||
}
|
||||
return $storageFolders;
|
||||
}
|
||||
|
||||
private function loadMetaData(File $file): FormMetadata
|
||||
{
|
||||
$persistenceIdentifier = $file->getCombinedIdentifier();
|
||||
$rawYamlContent = $file->getContents();
|
||||
|
||||
try {
|
||||
$yaml = $this->extractMetaDataFromCouldBeFormDefinition($rawYamlContent);
|
||||
$this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
|
||||
return FormMetadata::createFromYaml(
|
||||
$yaml,
|
||||
$persistenceIdentifier,
|
||||
$file->getUid()
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return FormMetadata::createInvalid($persistenceIdentifier, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition): array
|
||||
{
|
||||
$metaDataProperties = ['identifier', 'type', 'label', 'prototypeName'];
|
||||
$metaData = [];
|
||||
foreach (explode(LF, $maybeRawFormDefinition) as $line) {
|
||||
if (empty($line) || $line[0] === ' ') {
|
||||
continue;
|
||||
}
|
||||
$parts = explode(':', $line, 2);
|
||||
$key = trim($parts[0]);
|
||||
if (!($parts[1] ?? null) || !in_array($key, $metaDataProperties, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'label') {
|
||||
try {
|
||||
$parsedLabelLine = Yaml::parse($line);
|
||||
$value = $parsedLabelLine['label'] ?? '';
|
||||
} catch (ParseException) {
|
||||
$value = '';
|
||||
}
|
||||
} else {
|
||||
$value = trim($parts[1], " '\"\r");
|
||||
}
|
||||
$metaData[$key] = $value;
|
||||
}
|
||||
return $metaData;
|
||||
}
|
||||
|
||||
private function looksLikeAFormDefinition(FormMetadata $formMetadata): bool
|
||||
{
|
||||
return !empty($formMetadata->identifier) && trim($formMetadata->type) === 'Form';
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user