TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
<?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\Mvc\Persistence;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Form\Domain\DTO\FormData;
|
||||
use TYPO3\CMS\Form\Domain\DTO\FormMetadata;
|
||||
use TYPO3\CMS\Form\Domain\DTO\PersistenceManagerConfiguration;
|
||||
use TYPO3\CMS\Form\Domain\DTO\SearchCriteria;
|
||||
use TYPO3\CMS\Form\Domain\DTO\StorageContext;
|
||||
use TYPO3\CMS\Form\Domain\ValueObject\FormIdentifier;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\TypoScriptService;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Event\AfterFormDefinitionLoadedEvent;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||
use TYPO3\CMS\Form\Service\DatabaseService;
|
||||
use TYPO3\CMS\Form\Storage\StorageAdapterFactory;
|
||||
|
||||
/**
|
||||
* Form Persistence Manager - Entry point for FormManagerController
|
||||
*
|
||||
* This manager acts as a facade that delegates storage operations to appropriate
|
||||
* storage adapters via the StorageAdapterFactory.
|
||||
*
|
||||
* Scope: frontend / backend
|
||||
* @internal
|
||||
*/
|
||||
#[AsAlias(FormPersistenceManagerInterface::class)]
|
||||
readonly class FormPersistenceManager implements FormPersistenceManagerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StorageAdapterFactory $storageAdapterFactory,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $runtimeCache,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private TypoScriptService $typoScriptService,
|
||||
private DatabaseService $databaseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load form definition and apply event listeners and TypoScript overrides
|
||||
*/
|
||||
public function load(string $persistenceIdentifier, ?array $typoScriptSettings = null, ?ServerRequestInterface $request = null): array
|
||||
{
|
||||
$cacheKey = 'ext-form-load-' . hash('xxh3', $persistenceIdentifier);
|
||||
|
||||
if ($this->runtimeCache->has($cacheKey)) {
|
||||
$formDefinition = $this->runtimeCache->get($cacheKey);
|
||||
} else {
|
||||
$formDefinition = $this->loadFromStorage($persistenceIdentifier, $request);
|
||||
$this->runtimeCache->set($cacheKey, $formDefinition);
|
||||
}
|
||||
|
||||
$formDefinition = $this->eventDispatcher
|
||||
->dispatch(new AfterFormDefinitionLoadedEvent($formDefinition, $persistenceIdentifier, $cacheKey))
|
||||
->getFormDefinition();
|
||||
|
||||
if ($request !== null && !empty($typoScriptSettings['formDefinitionOverrides'][$formDefinition['identifier']] ?? null)) {
|
||||
$formDefinitionOverrides = $this->typoScriptService->resolvePossibleTypoScriptConfiguration(
|
||||
$typoScriptSettings['formDefinitionOverrides'][$formDefinition['identifier']],
|
||||
$request
|
||||
);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($formDefinition, $formDefinitionOverrides);
|
||||
}
|
||||
|
||||
return $formDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save form definition to appropriate storage
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
public function save(string $persistenceIdentifier, array $formDefinition, array $formSettings, ?string $storageLocation = null): FormIdentifier
|
||||
{
|
||||
if (!$this->isAllowedPersistenceIdentifier($persistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(
|
||||
sprintf('Save to path "%s" is not allowed.', $persistenceIdentifier),
|
||||
1477680881
|
||||
);
|
||||
}
|
||||
|
||||
$identifier = new FormIdentifier($persistenceIdentifier);
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
||||
$formData = FormData::fromArray($formDefinition);
|
||||
|
||||
$context = null;
|
||||
if (MathUtility::canBeInterpretedAsInteger($storageLocation)) {
|
||||
$context = StorageContext::create((int)$storageLocation);
|
||||
}
|
||||
|
||||
$savedIdentifier = $adapter->write($identifier, $formData, $context);
|
||||
|
||||
$this->clearFormCache($savedIdentifier->identifier);
|
||||
return $savedIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete form definition from storage
|
||||
*/
|
||||
public function delete(string $persistenceIdentifier, array $formSettings): void
|
||||
{
|
||||
$identifier = new FormIdentifier($persistenceIdentifier);
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
||||
|
||||
if (!$adapter->exists($identifier)) {
|
||||
throw new PersistenceManagerException(
|
||||
sprintf('The form "%s" does not exist.', $persistenceIdentifier),
|
||||
1472239535
|
||||
);
|
||||
}
|
||||
|
||||
$adapter->delete($identifier);
|
||||
$this->clearFormCache($persistenceIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all form definitions from all available storages
|
||||
*/
|
||||
public function listForms(array $formSettings, SearchCriteria $searchCriteria): array
|
||||
{
|
||||
$identifiers = [];
|
||||
$forms = [];
|
||||
|
||||
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
||||
try {
|
||||
$formDataList = $adapter->findAll($searchCriteria);
|
||||
|
||||
foreach ($formDataList as $formData) {
|
||||
if ($formData->storageType === null) {
|
||||
$formData = $formData->withStorageType($adapter->getTypeIdentifier());
|
||||
}
|
||||
$forms[] = $formData;
|
||||
if (!isset($identifiers[$formData->identifier])) {
|
||||
$identifiers[$formData->identifier] = 0;
|
||||
}
|
||||
$identifiers[$formData->identifier]++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($identifiers as $identifier => $count) {
|
||||
if ($count > 1) {
|
||||
foreach ($forms as $index => $formMetadata) {
|
||||
if ($formMetadata->identifier === $identifier) {
|
||||
$forms[$index] = $formMetadata->withDuplicateIdentifier(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allReferencesForFileUid = $this->databaseService->getAllReferencesForFileUid();
|
||||
$allReferencesForPersistenceIdentifier = $this->databaseService->getAllReferencesForPersistenceIdentifier();
|
||||
$allReferencesForFormDefinitionUid = $this->databaseService->getAllReferencesForFormDefinitionUid();
|
||||
|
||||
foreach ($forms as $index => $formMetadata) {
|
||||
if (isset($formMetadata->fileUid) && array_key_exists($formMetadata->fileUid, $allReferencesForFileUid)) {
|
||||
$referenceCount = $allReferencesForFileUid[$formMetadata->fileUid];
|
||||
} elseif ($formMetadata->persistenceIdentifier && array_key_exists($formMetadata->persistenceIdentifier, $allReferencesForFormDefinitionUid)) {
|
||||
$referenceCount = $allReferencesForFormDefinitionUid[$formMetadata->persistenceIdentifier];
|
||||
} elseif ($formMetadata->persistenceIdentifier && array_key_exists($formMetadata->persistenceIdentifier, $allReferencesForPersistenceIdentifier)) {
|
||||
$referenceCount = $allReferencesForPersistenceIdentifier[$formMetadata->persistenceIdentifier];
|
||||
} else {
|
||||
$referenceCount = 0;
|
||||
}
|
||||
if ($referenceCount > 0) {
|
||||
$forms[$index] = $formMetadata->withReferenceCount($referenceCount);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sortForms($forms, $formSettings, $searchCriteria->getOrderField(), $searchCriteria->getOrderDirection());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any forms are available
|
||||
*/
|
||||
public function hasForms(array $formSettings): bool
|
||||
{
|
||||
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
||||
try {
|
||||
$forms = $adapter->findAll(new SearchCriteria(limit: 1));
|
||||
|
||||
if (!empty($forms)) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique persistence identifier for a new form
|
||||
*/
|
||||
public function getUniquePersistenceIdentifier(string $storage, string $formIdentifier, ?string $savePath): string
|
||||
{
|
||||
return $this->storageAdapterFactory->getAdapterByType($storage)->getUniquePersistenceIdentifier($formIdentifier, $savePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique identifier (not persistence identifier)
|
||||
*/
|
||||
public function getUniqueIdentifier(string $identifier): string
|
||||
{
|
||||
$originalIdentifier = $identifier;
|
||||
|
||||
if ($this->checkForDuplicateIdentifier($identifier)) {
|
||||
for ($attempts = 1; $attempts < 100; $attempts++) {
|
||||
$identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
|
||||
if (!$this->checkForDuplicateIdentifier($identifier)) {
|
||||
return $identifier;
|
||||
}
|
||||
}
|
||||
|
||||
$identifier = $originalIdentifier . '_' . time();
|
||||
if ($this->checkForDuplicateIdentifier($identifier)) {
|
||||
throw new NoUniqueIdentifierException(
|
||||
sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
|
||||
1477688567
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a storage location is allowed
|
||||
*
|
||||
* For database storage: storageLocation is a PID
|
||||
* For file storage: storageLocation is a folder path (e.g., "1:/forms/")
|
||||
*/
|
||||
public function isAllowedStorageLocation(string $storageLocation): bool
|
||||
{
|
||||
try {
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($storageLocation);
|
||||
return $adapter->isAllowedStorageLocation($storageLocation);
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a persistence identifier is allowed
|
||||
*
|
||||
* For database storage: identifier is a UID or NEW*
|
||||
* For file storage: identifier is a full file path (e.g., "1:/forms/contact.form.yaml")
|
||||
*/
|
||||
public function isAllowedPersistenceIdentifier(string $persistenceIdentifier): bool
|
||||
{
|
||||
try {
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
||||
return $adapter->isAllowedPersistenceIdentifier($persistenceIdentifier);
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file has valid extension
|
||||
*/
|
||||
public function hasValidFileExtension(string $fileName): bool
|
||||
{
|
||||
return str_ends_with($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load form definition from storage
|
||||
*/
|
||||
private function loadFromStorage(string $persistenceIdentifier, ?ServerRequestInterface $request = null): array
|
||||
{
|
||||
try {
|
||||
$identifier = new FormIdentifier($persistenceIdentifier);
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
||||
|
||||
return $adapter->read($identifier, $request)->toArray();
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'type' => 'Form',
|
||||
'identifier' => $persistenceIdentifier,
|
||||
'label' => $e->getMessage(),
|
||||
'invalid' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if form with persistence identifier exists
|
||||
*/
|
||||
private function exists(string $persistenceIdentifier): bool
|
||||
{
|
||||
try {
|
||||
$identifier = new FormIdentifier($persistenceIdentifier);
|
||||
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
||||
|
||||
return $adapter->exists($identifier);
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form with given identifier already exists in any storage
|
||||
*/
|
||||
private function checkForDuplicateIdentifier(string $identifier): bool
|
||||
{
|
||||
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
||||
try {
|
||||
if ($adapter->existsByFormIdentifier($identifier)) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function sortForms(array $forms, array $formSettings, string $orderField = '', ?string $orderDirection = null): array
|
||||
{
|
||||
$persistenceConfiguration = PersistenceManagerConfiguration::fromArray($formSettings['persistenceManager'] ?? []);
|
||||
if ($orderDirection) {
|
||||
$ascending = $orderDirection === 'asc';
|
||||
} else {
|
||||
$ascending = $persistenceConfiguration->sortAscending;
|
||||
}
|
||||
$sortMultiplier = $ascending ? 1 : -1;
|
||||
$keys = $orderField ? [$orderField] : $persistenceConfiguration->sortByKeys;
|
||||
|
||||
usort($forms, static function (FormMetadata $a, FormMetadata $b) use ($keys, $sortMultiplier) {
|
||||
foreach ($keys as $key) {
|
||||
$aValue = $a->getSortableValue($key);
|
||||
$bValue = $b->getSortableValue($key);
|
||||
|
||||
if ($aValue === null || $bValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = (is_int($aValue) && is_int($bValue))
|
||||
? $aValue - $bValue
|
||||
: strcasecmp((string)$aValue, (string)$bValue);
|
||||
|
||||
if ($diff !== 0) {
|
||||
return $diff * $sortMultiplier;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return $forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for specific form
|
||||
*/
|
||||
private function clearFormCache(string $persistenceIdentifier): void
|
||||
{
|
||||
$cacheKey = 'ext-form-load-' . hash('xxh3', $persistenceIdentifier);
|
||||
$this->runtimeCache->remove($cacheKey);
|
||||
}
|
||||
|
||||
public function getAccessibleStorageAdapters(): array
|
||||
{
|
||||
$storageAdapters = [];
|
||||
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
||||
if ($adapter->isAccessible() === false) {
|
||||
continue;
|
||||
}
|
||||
$storageAdapters[] = [
|
||||
'typeIdentifier' => $adapter->getTypeIdentifier(),
|
||||
'label' => $adapter->getLabel(),
|
||||
'description' => $adapter->getDescription(),
|
||||
'iconIdentifier' => $adapter->getIconIdentifier(),
|
||||
'options' => $adapter->getFormManagerOptions(),
|
||||
];
|
||||
}
|
||||
return $storageAdapters;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user