• Home
  • -
  • TYPO3 TCA Tree with non-database items

TYPO3 TCA Tree with non-database items

For a recent hobby project, I wanted to build a select tree in the TYPO3 backend with the form engine, that was not based on database items but instead fetched tree data from an external API.

TYPO3 TCA SelectTree

Here's how to do that:

Context: Content Element with Flexform

First of, I'm using a custom content element with a simple flexform configuration, like this:

Content Element Registration:

File: TCA/Overrides/tt_content.php:

\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
    'Susanne.MyConnector',
    'ListByCategories',
    'Product List (by Categories)'
);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
    '*',
    'FILE:EXT:my_connector/Configuration/FlexForms/Categories.xml',
    'myconnector_listbycategories'
);
// ... TCA + showitem configuration

Flexform Definition with custom TCA TreeDataProvider:

File: Configuration/FlexForms/Categories.xml:

<T3DataStructure>
    <sheets>
        <sDEF>
            <ROOT>
                <TCEforms>
                    <sheetTitle>Settings</sheetTitle>
                </TCEforms>
                <type>array</type>
                <el>
                    <settings.items>
                        <TCEforms>
                            <label>Items</label>
                            <config>
                                <type>select</type>
                                <renderType>selectTree</renderType>
                                <maxitems>100</maxitems>
                                <size>25</size>
                                <internal_type>my_connector</internal_type>
                                <treeConfig>
                                    <dataProvider>Susanne\MyConnector\CategoryTree</dataProvider>
                                    <appearance>
                                        <expandAll>1</expandAll>
                                        <showHeader>1</showHeader>
                                    </appearance>
                                </treeConfig>
                            </config>
                        </TCEforms>
                    </settings.items>
                </el>
            </ROOT>
        </sDEF>
    </sheets>
</T3DataStructure>

Let's look at that FlexForm in detail: First of all, I'm setting the type to select and the renderType to selectTree to configure the basic tree rendering. Then we need to set treeConfig. Here is where the magic happens: We provide a custom dataProvider for the tree to have our own PHP class which will then in turn be able to return items from our external API.

Note
The internal_type is set to avoid TYPO3 setting it to db by default - the value is irrelevant as long as it is not db.

The TreeDataProvider for a TYPO3 TCA SelectTree

Sadly, there is no interface for the TreeDataProvider, so we need to do our best to figure out what a tree needs. I took a look around the code and decided - even though the name suggests otherwise - that I'd extend the \TYPO3\CMS\Core\Tree\TableConfiguration\AbstractTableConfigurationTreeDataProvider as there is no database specific code in there, but quite a few useful helper methods.

When extending that, we need to implement two abstract methods, getRoot and getNodes. This is how my implementation basically looks like:


    public function __construct($tcaConfiguration, $table, $field, $currentValue, EventDispatcherInterface $eventDispatcher) {}

    public function getRoot()
    {
        $categories = $this->productService->getCategories();

        $childNodes $this->buildTree($categories);

        $treeNode = new DatabaseTreeNode();
        $treeNode->setId('root');
        $treeNode->setLabel('MyConnector');
        $treeNode->setChildNodes($childNodes);
        $treeNode->setSelectable(false);
        return $treeNode;
    }

    public function getNodes(TreeNode $node)
    {
        // the abstract defines this, so I need to be here and do nothing as I'm never called
    }

You might notice a few weird things, let's see:

  • The empty constructor (if you do not need to do anything in the constructor) is necessary, as otherwise we'll get a null pointer exception due to a reflection of the class checking the constructor
  • The getNodes() method is empty, because we need it as it's part of the abstract, however, it is never called
  • The getRoot() method actually generates the whole tree instead of just the root node itself, because this method is the one actually called - at a guess, I'd say in earlier version getRoot and getNodes were used in conjunction with ajax calls to load tree parts on demand - however than functionality seems to be gone now
  • Though it is not a database based tree, we are still using DatabaseTreeNode as class for our tree elements, as these are the only ones that can be configured to be selectable or checked.

In this tree, the root node should not be selectable, as I don't have a single root node in the data from the API but multiple - so I'm adding a single non-selectable root node on top.

This basically already renders the tree with our data. However... if you save now, you get the data written to the database and can use it in the frontend - but it's not selected again when re-opening the element. This happens, because our elements are unknown to the form engine rendering part and in sanitizing the data our selected elements get removed. While not nice, we can still fix this by fetching the data ourselves and using it to select the items - now, we'll need the constructor:

    public function __construct($tcaConfiguration, $table, $field, $currentValue, EventDispatcherInterface $eventDispatcher)
    {
        $this->productService = GeneralUtility::makeInstance(ProductService::class);
        $record = BackendUtility::getRecord($table, (int)$currentValue['uid'], 'pi_flexform');
        $flexform = GeneralUtility::xml2array($record['pi_flexform']);
        $this->selectedItems = explode(',', $flexform['data']['sDEF']['lDEF'][$field]['vDEF'] ?? '');
        $this->setSelectedList($this->selectedItems);
    }

(for brevity's sake, I added the logic directly in the __construct call here) - Depending on your FlexForm the data structure might be a bit different, but you get the idea. Now that we have a list of selected items, when rendering the tree in getRoot() we need to check that list for our IDs:

$child = new DatabaseTreeNode();
$child->setLabel($category->name);
$child->setId($category->id);
$child->setSelectable(true);
$child->setSelected(in_array($category->id, $this->selectedItems, true));

That's it - now we have a custom TCA select tree rendered with items we can get from anywhere that we can save!

Expanding
I'm using expandAll in the FlexForm above - I'd recommend using this, as otherwise, we would need to implement saving the expanded state ourselves. While the core does still read the state from $GLOBALS['BE_USER']->uc['tcaTrees'][$this->treeId] in initializeTreeData it does not seem to write it anymore, so the fastest way for me was simply using expandAll.

Closing Remarks


You might have noticed that I struggled a bit with the implementation, while I could have taken steps to improve the core implementation, I purposefully wanted to use what was currently available and see where that takes me. If you have any ideas on how to use TCASelectTrees with non-database items in a more convenient way, I'd be happy to hear about them on Twitter or on TYPO3 Slack @susi.

PS
At the current rate, I'll have a bit of time for Core development in December - so maybe I'll make some improvements then ;)