How to create TYPO3 Form select element with options selected from database

TYPO3s new form framework allows to write custom form elements. This way you are able to define a new select element, based on the existing one, but filled with options fetched from database.

E.g. you want your user to select from sys_cateogy or some other custom records. In this blog post I will show how to provide the necessary logic in a custom PHP class, how to register a new element extending the existing one and how to use this new element in your forms.

The following steps are necessary:

  1. Write custom element as PHP Class, extending base element.
  2. Define custom element as prototype in yaml-Configuration, which inherits existing configuration of a select.
  3. Use the new element in the form.

The PHP Class

As we want to provide custom functionality, we need to create a PHP Class which will contain this functionality. In our example we want to provide sys_category records based on a parent sys_category as possible options. Therefore we need to provide one option, the uid of the parent system category. Also we need to fetch the records from database using Doctrine and provide them as options for the element, e.g. select in our case.

The implementation is done with the following class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
namespace DS\ExampleExtension\Domain\Model\FormElements;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;

class SystemCategoryOptions extends GenericFormElement
{
    public function setProperty(string $key, $value)
    {
        if ($key === 'systemCategoryUid') {
            $this->setProperty('options', $this->getOptions($value));
            return;
        }

        parent::setProperty($key, $value);
    }

    protected function getOptions(int $uid) : array
    {
        $options = [];

        foreach ($this->getCategoriesForUid($uid) as $category) {
            $options[$category['uid']] = $category['title'];
        }

        asort($options);
        return $options;
    }

    protected function getCategoriesForUid(int $uid) : array
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('sys_category');
        $queryBuilder->setRestrictions(
            GeneralUtility::makeInstance(FrontendRestrictionContainer::class)
        );

        return $queryBuilder
            ->select('*')
            ->from('sys_category')
            ->where(
                $queryBuilder->expr()->eq(
                    'parent',
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
                )
            )
            ->execute()
            ->fetchAll();
    }
}

First of all we extend the setProperty method, which receives all options. If the current option is the configured systemCategoryUid, we hook into and add the options. In all other situations we just call the original method.

Based on the configured uid, we fetch the records from database in our getCategoriesForUid method.

Afterwards we iterate over the results and prepare them to be used by the select, therefore we need a label and identifier. We use the saved title and uid. The result is set as the options for select element.

The class itself does not contain any relation to the specifics of a select-element. It should also be possible to use the same code for radio or checkboxes, as long as they make use of the options property.

Hint

Therefore it should be possible to separate logic from the elements themselves and to build the concrete elements via yaml. But I didn’t try that yet.

Define custom element

Once our functionality is provided, we need to create a new form element to be available to our forms. Therefore we define the new element:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            SingleSelectWithSystemCategory:
              __inheritances:
                10: 'TYPO3.CMS.Form.prototypes.standard.formElementsDefinition.SingleSelect'
              implementationClassName: 'DS\ExampleExtension\Domain\Model\FormElements\SystemCategoryOptions'
              renderingOptions:
                templateName: 'SingleSelect'

On line 7 we define the identifier of our new element, under which we can use the element in our forms. We inherit the existing configuration of the select element and exchange the concrete php class for implementation. As the path of fluid template is generated from the name, we define to use the same template as for the select element.

That’s all we have to do, to define a new select with different implementation.

Use element

We are now able to use the defined element in our forms:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type: Form
identifier: Example
label: 'Example - Form'
prototypeName: standard
renderingOptions:
  submitButtonLabel: Submit
renderables:
  -
    type: Page
    identifier: page1
    renderingOptions:
      previousButtonLabel: 'previous page'
      nextButtonLabel: 'next page'
    renderables:
      -
        type: SingleSelectWithSystemCategory
        identifier: jobTitle
        label: Job Title
        properties:
          systemCategoryUid: 5
          prependOptionLabel: 'please choose'
          fluidAdditionalAttributes:
            required: required
        validators:
          -
            identifier: NotEmpty

We define our own SingleSelectWithSystemCategory element to be used and define our systemCategoryUid to be used. Everything else is exactly the same as for any other select, as we use the same template.

Further reading

Check out the official doc sections: