Container\FlexFormEntryContainer::class, 'flexFormContainerContainer' => Container\FlexFormContainerContainer::class, 'flexFormElementContainer' => Container\FlexFormElementContainer::class, 'flexFormNoTabsContainer' => Container\FlexFormNoTabsContainer::class, 'flexFormSectionContainer' => Container\FlexFormSectionContainer::class, 'flexFormTabsContainer' => Container\FlexFormTabsContainer::class, 'fullRecordContainer' => Container\FullRecordContainer::class, 'inline' => Container\InlineControlContainer::class, 'inlineRecordContainer' => Container\InlineRecordContainer::class, 'file' => Container\FilesControlContainer::class, 'fileReferenceContainer' => Container\FileReferenceContainer::class, 'siteLanguage' => Container\SiteLanguageContainer::class, 'listOfFieldsContainer' => Container\ListOfFieldsContainer::class, 'noTabsContainer' => Container\NoTabsContainer::class, 'formWrapContainer' => Container\FormWrapContainer::class, 'paletteAndSingleContainer' => Container\PaletteAndSingleContainer::class, 'singleFieldContainer' => Container\SingleFieldContainer::class, 'tabsContainer' => Container\TabsContainer::class, // Default single element classes 'check' => Element\CheckboxElement::class, 'checkboxToggle' => Element\CheckboxToggleElement::class, 'checkboxLabeledToggle' => Element\CheckboxLabeledToggleElement::class, 'email' => Element\EmailElement::class, 'group' => Element\GroupElement::class, 'folder' => Element\FolderElement::class, 'input' => Element\InputTextElement::class, 'number' => Element\NumberElement::class, 'datetime' => Element\DatetimeElement::class, 'link' => Element\LinkElement::class, 'password' => Element\PasswordElement::class, 'hidden' => Element\InputHiddenElement::class, 'imageManipulation' => Element\ImageManipulationElement::class, 'none' => Element\NoneElement::class, 'radio' => Element\RadioElement::class, 'selectCheckBox' => Element\SelectCheckBoxElement::class, 'selectMultipleSideBySide' => Element\SelectMultipleSideBySideElement::class, 'selectTree' => Element\SelectTreeElement::class, 'selectSingle' => Element\SelectSingleElement::class, 'selectSingleBox' => Element\SelectSingleBoxElement::class, 'color' => Element\ColorElement::class, 'codeEditor' => Element\CodeEditorElement::class, 'text' => Element\TextElement::class, 'textTable' => Element\TextTableElement::class, 'unknown' => Element\UnknownElement::class, 'user' => Element\UserElement::class, // special renderType for type="user" on sys_file_storage is_public column 'userSysFileStorageIsPublic' => Element\UserSysFileStorageIsPublicElement::class, 'fileInfo' => Element\FileInfoElement::class, 'mfaInfo' => Element\MfaInfoElement::class, 'slug' => Element\InputSlugElement::class, 'language' => Element\SelectSingleElement::class, 'country' => Element\SelectCountryElement::class, 'category' => Element\CategoryElement::class, 'passthrough' => Element\PassThroughElement::class, 'belayoutwizard' => Element\BackendLayoutWizardElement::class, 'json' => Element\JsonElement::class, 'uuid' => Element\UuidElement::class, 'tablePermission' => Element\TablePermissionElement::class, // special renderTypes for user settings 'avatar' => AvatarElement::class, // Default classes to enrich single elements 'fieldControl' => NodeExpansion\FieldControl::class, 'fieldInformation' => NodeExpansion\FieldInformation::class, 'fieldWizard' => NodeExpansion\FieldWizard::class, // Element information 'adminIsSystemMaintainer' => FieldInformation\AdminIsSystemMaintainer::class, 'backendLayoutFromParentPage' => FieldInformation\BackendLayoutFromParentPage::class, 'noSelectableItemsAvailable' => FieldInformation\NoSelectableItemsAvailable::class, // Element wizards 'defaultLanguageDifferences' => FieldWizard\DefaultLanguageDifferences::class, 'localizationStateSelector' => FieldWizard\LocalizationStateSelector::class, 'otherLanguageContent' => FieldWizard\OtherLanguageContent::class, 'otherLanguageThumbnails' => FieldWizard\OtherLanguageThumbnails::class, 'recordsOverview' => FieldWizard\RecordsOverview::class, 'selectIcons' => FieldWizard\SelectIcons::class, 'shortcutValidation' => FieldWizard\ShortcutValidation::class, 'tableList' => FieldWizard\TableList::class, // Element controls 'addRecord' => FieldControl\AddRecord::class, 'editPopup' => FieldControl\EditPopup::class, 'elementBrowser' => FieldControl\ElementBrowser::class, 'insertClipboard' => FieldControl\InsertClipboard::class, 'linkPopup' => FieldControl\LinkPopup::class, 'listModule' => FieldControl\ListModule::class, 'resetSelection' => FieldControl\ResetSelection::class, 'passwordGenerator' => FieldControl\PasswordGenerator::class, ]; public function __construct() { $this->nodeTypes = $this->getAdditionalNodeTypesFromConfiguration($this->nodeTypes); $this->nodeResolver = $this->getRegisteredNodeResolvers(); } /** * Create a node depending on type * * @param array $data All information to decide which class should be instantiated and given down to sub nodes * @throws Exception */ public function create(array $data): NodeInterface { if (empty($data['renderType'])) { throw new Exception( 'Missing "renderType" in TCA of field "[' . ($data['tableName'] ?? 'unknown') . '][' . ($data['fieldName'] ?? 'unknown') . ']".', 1431452406 ); } $type = $data['renderType']; if (!is_string($type)) { throw new Exception( '"renderType" in TCA of field "[' . ($data['tableName'] ?? 'unknown') . '][' . ($data['fieldName'] ?? 'unknown') . ']" does not contain a string. It might be an array instead of a string which could be the result of an array_merge_recursive() operation, for example on existing "fieldControl" keys (these need to have unique array key indices).', 1739882175 ); } $className = $this->nodeTypes[$type] ?? $this->nodeTypes['unknown']; if (!empty($this->nodeResolver[$type])) { // Resolver with the highest priority is called first. If it returns with a new class name, // it will be taken and loop is aborted, otherwise resolver with next lower priority is called. /** @noinspection PhpUnusedLocalVariableInspection leave for debugging purpose */ foreach ($this->nodeResolver[$type] as $priority => $resolverClassName) { $resolver = $this->initializeNodeResolverClass($resolverClassName, $data); // Resolver classes do NOT receive the name of the already resolved class. Single // resolvers should not have dependencies to each other or the default implementation, // so they also shouldn't know the output of a different resolving class. // Additionally, the globalOptions array is NOT given by reference here, changing config is a // task of container classes alone and must not be abused here. $newClassName = $resolver->resolve(); if ($newClassName !== null) { $className = $newClassName; break; } } } return $this->initializeNodeClass($className, $data); } /** * Add node types from nodeRegistry to $this->nodeTypes. * This can be used to add new render types or to overwrite existing node types. The registered class must * implement the NodeInterface and will be called if a node with this renderType is rendered. * * @throws Exception if configuration is incomplete or two nodes with identical priorities are registered */ private function getAdditionalNodeTypesFromConfiguration(array $nodeTypes): array { // List of additional or override nodes $registeredTypeOverrides = $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry']; // Sanitize input array $registeredPrioritiesForNodeNames = []; foreach ($registeredTypeOverrides as $override) { if (!isset($override['nodeName']) || !isset($override['class']) || !isset($override['priority'])) { throw new Exception( 'Key class, nodeName or priority missing for an entry in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'formEngine\'][\'nodeRegistry\']', 1432207533 ); } if ($override['priority'] < 0 || $override['priority'] > 100) { throw new Exception( 'Priority of element ' . $override['nodeName'] . ' with class ' . $override['class'] . ' is ' . $override['priority'] . ', but must between 0 and 100', 1432223531 ); } if (isset($registeredPrioritiesForNodeNames[$override['nodeName']][$override['priority']])) { throw new Exception( 'Element ' . $override['nodeName'] . ' already has an override registered with priority ' . $override['priority'], 1432223893 ); } $registeredPrioritiesForNodeNames[$override['nodeName']][$override['priority']] = ''; } // Add element with the highest priority to registry $highestPriority = []; foreach ($registeredTypeOverrides as $override) { if (!isset($highestPriority[$override['nodeName']]) || $override['priority'] > $highestPriority[$override['nodeName']]) { $highestPriority[$override['nodeName']] = $override['priority']; $nodeTypes[$override['nodeName']] = $override['class']; } } return $nodeTypes; } /** * Add resolver and add them sorted to a local property. * This can be used to manipulate the nodeName to class resolution with own code. * * @throws Exception if configuration is incomplete or two resolver with identical priorities are registered */ private function getRegisteredNodeResolvers(): array { // List of node resolver $registeredNodeResolvers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeResolver']; $resolversByType = []; foreach ($registeredNodeResolvers as $nodeResolver) { if (!isset($nodeResolver['nodeName']) || !isset($nodeResolver['class']) || !isset($nodeResolver['priority'])) { throw new Exception( 'Key class, nodeName or priority missing for an entry in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'formEngine\'][\'nodeResolver\']', 1433155522 ); } if ($nodeResolver['priority'] < 0 || $nodeResolver['priority'] > 100) { throw new Exception( 'Priority of element ' . $nodeResolver['nodeName'] . ' with class ' . $nodeResolver['class'] . ' is ' . $nodeResolver['priority'] . ', but must between 0 and 100', 1433155563 ); } if (isset($resolversByType[$nodeResolver['nodeName']][$nodeResolver['priority']])) { throw new Exception( 'Element ' . $nodeResolver['nodeName'] . ' already has a resolver registered with priority ' . $nodeResolver['priority'], 1433155705 ); } $resolversByType[$nodeResolver['nodeName']][$nodeResolver['priority']] = $nodeResolver['class']; } $sortedResolversByType = []; foreach ($resolversByType as $nodeName => $prioritiesAndClasses) { krsort($prioritiesAndClasses); $sortedResolversByType[$nodeName] = $prioritiesAndClasses; } return $sortedResolversByType; } /** * Instantiate a NodeInterface class and set data. * * @param array $data Main data array */ private function initializeNodeClass(string $className, array $data): NodeInterface { /** @var NodeInterface $node */ $node = GeneralUtility::makeInstance($className); $node->setData($data); return $node; } /** * Instantiate a NodeResolverInterface class and set data. * * @param array $data Main data array */ private function initializeNodeResolverClass(string $className, array $data): NodeResolverInterface { /** @var NodeResolverInterface $node */ $node = GeneralUtility::makeInstance($className); $node->setData($data); return $node; } }