TYPO3 Plugins as Content Elements

E.g. to add a simplified and project-specific News Extension Content element for backend editors.

You might think “I know what plugins, within TYPO3, are”. Maybe that’s true, maybe you will still learn something new.

This blog post will first explain what TYPO3 plugins are. But it will also explain how to define site specific plugins for existing installed 3rd party extensions, and why this might be useful.

This will only cover Extbase plugins, as most extensions only provide Extbase nowadays. But it also works, partly, for pibase extensions. The basic idea dates back to 2018, when I first started to work on this. We now make use of this concept within an actual project, so this covers not only abstract concepts, but real world examples.

Target audience

This blog post requires some TYPO3 knowledge in order to understand everything. This post targets integrators and developers, who already know how to write and use TypoScript, TSconfig, Fluid and TCA configuration. You should also know what FlexForms are.

Also the official TYPO3 documentation section Adding your own content elements is required in order to follow this blog post. This post will provide a complete example, but will not explain every taken step in order to create the new content element. Instead we will focus on the plugin becoming a regular content element.

What is a TYPO3 plugin?

To understand the whole blog post, one needs to understand the basics of plugins within TYPO3.

TYPO3 itself is nothing then a collection of so called “Extensions”. An extension is something that extends TYPO3 in any way. This can either be done by providing plugins and custom PHP code, or by providing CSS, JS, Fluid Templates, Hooks, or anything else. Within this post, we will only cover a specific aspect of plugins.

Plugins are a way to integrate custom PHP logic into TYPO3 for frontend websites. An editor is able to insert a new content element of type “Insert Plugin”, where he can select the specific plugin. This plugin can be something like “List news” or “List events”. A plugin can also be a search form or search result or some other kind of form. In the end, a plugin can be anything.

Most extensions provide plugins out of the box. Most likely you will have a single plugin per extension. The extension author allows the editor to select further configuration options through the content element, via so called FlexForms. E.g. the editor can select the “mode”, e.g. “list” or “detail” for something like news.

Within TYPO3 Extbase, a plugin consists of the following:

  • A title for the editor within the TYPO3 backend

  • An optional icon within TYPO3 backend

  • TypoScript defining the rendering of the plugin

  • A combination of callable controllers and actions

  • A combination of non cached callable controllers and actions

  • An identifier, so called “plugin signature”

  • An optional FlexForm for further configuration via editors

  • An optional “New Content Element Wizard” entry for new content elements

Why adding plugins for existing extensions?

So extensions already provide plugins, why should one add further plugins to existing 3rd party extensions?

Example 1 EXT:solr + news

Let’s assume there is a TYPO3 installation with a search and news. The search is provided by EXT:solr, and news are implemented using the “Custom Page Type approach™”, see (Blog Post: Everything is Content, that can be served via Solr and TYPO3 Documentation Page Types). The news should be displayed by using EXT:solr, as there is no need for another extension, in this use case.

A typical use case would be to display a list of recent news, e.g. the five recent news on the startpage. Maybe also some pre filtered news should be displayed on sub pages, e.g. only news regarding new products or news regarding the company. All those use cases are solved by using EXT:solr.

Of course one could now add TypoScript to pages to configure EXT:solr to start in filter mode instead of search mode. Also filters can be added to only show news records from these categories. This is not that flexible. The editor is not able to add new “News listings” to further pages, as TypoScript is involved.

It would be better if the integrator can add a new plugin “news” within the “Sitepackage™” of the installation. This plugin duplicates the existing plugin, provided by EXT:solr.

Benefits of this approach would be:

  • Instead of keeping the result action none cacheable, it can define that this action should be cacheable.

  • Also a new plugin allows to add a different FlexForm to this plugin. This FlexForm can provide a drop down with possible categories, or allows an editor to define how many news should be displayed. Thanks to Extbase conventions, all options available within TypoScript settings section can be used within FlexForms. Due to a different plugin signature, the plugin can be configured differently via TypoScript.

This new Plugin speeds up the delivery of the page, as it’s fully cached. Also an editor can now add a “news” content element and select the specific category and number of news to display. He does not need to understand that solr is used.

Example 2 - EXT:news

In case of EXT:news, one might want to add “recent news” to the pages. This might contain a configurable number of news entries and different layouts, like “list” or “slider”. This is another example where custom plugins for existing 3rd party extensions might be useful. One can create those content elements and plugins.

Another benefit of this example: One can add “recent news” on a news detail page without thinking about any limitations. Due to being another plugin with a different signature, no arguments might create trouble. Also links created between those plugins can make use of the Extbase setting:

plugin.tx_news_recentnews {
    features {
        skipDefaultArguments = 1
    }
}

This can also be enabled for the whole extension:

plugin.tx_news {
    features {
        skipDefaultArguments = 1
    }
}

Or the whole installation / page:

config.tx_extbase {
    features {
        skipDefaultArguments = 1
    }
}

A link between those plugins can look like this, assuming to link from “Recent News” to “Detail News” custom plugin:

<f:link.action pageUid="11"
   pluginName="Details"
   arguments="{news: news}"
>
   <h4>{news.title}</h4>
</f:link.action>

As each plugin has its own default Controller-Action-Combination, there is no need to add them to the URL generation. Also thanks to the configuration of skipDefaultArguments, these will not be added to the url, resulting in an URL like this with CMS v9:

/?news_details%5Bnews%5D=1785&cHash=1f740d5404dddcf84b2c8bebc985deb9

How to add a new TYPO3 plugin

To add a new plugin, first of one API call is necessary. After this was done, the plugin is already available to the frontend. Next, the content element can be created in the preferred way, which depends on the agency and developer.

Afterwards the optional FlexForm and TypoScript configuration can be added.

For further information, take a look at Real world example.

Conclusion for Extbase controller

Each controller within an Extbase extension consists of actions, which should only do a single task each. By providing fine grained actions for single tasks, the Integrator is able to configure installation specific plugins, with new combinations of existing controllers and actions.

A contrary example was developed by myself and our team during my training. There we created a single controller with nearly 10 actions, all doing the same. The reason for those actions was to provide 10 different template variants. Today one could use ten custom plugins. Or even better use a setting like the layout field within the content element, together with an f:render call within Fluid to switch the rendering. But this will not be covered here. Just make sure, actions and controllers are written in a clean, reusable way.

Real world example

The following example demonstrates the concept based on EXT:news and a new content element to display recent news. The editor can configure how many news should be displayed.

  1. Register plugin within ext_localconf.php:

    \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
        'GeorgRinger.news',
        'Recent',
        [
            'News' => 'list',
        ],
        [],
        \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
    );
    
  2. Configure TCA for content element within Configuration/TCA/Overrides/tt_content_news_recent.php:

     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
    (function ($tablename = 'tt_content', $contentType = 'news_recent') {
        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($GLOBALS['TCA'][$tablename], [
            'ctrl' => [
                'typeicon_classes' => [
                    $contentType => 'content-recent-news',
                ],
            ],
            'types' => [
                $contentType => [
                    'showitem' => implode(',', [
                        '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general',
                            '--palette--;;general',
                            'pi_flexform',
                        '--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,--palette--;;frames,--palette--;;appearanceLinks,',
                        '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,',
                        '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
                          --palette--;;hidden,
                          --palette--;;access,
                        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
                             categories,
                        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes,
                             rowDescription,
                        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,'
                    ]),
                ],
            ],
            'columns' => [
                'pi_flexform' => [
                    'config' => [
                        'ds' => [
                            '*,' . $contentType => 'FILE:EXT:sitepackage/Configuration/FlexForms/ContentElements/RecentNews.xml',
                        ],
                    ],
                ],
            ],
        ]);
    
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
            $tablename,
            'CType',
            [
                'Recent News',
                $contentType,
                'content-recent-news',
            ],
            'textmedia',
            'after'
        );
    })();
    
  3. Optionally, add and register FlexForm.

    Registration is happening in TCA, see above example, line 27-35.

    The FlexForm itself can look like the following Configuration/FlexForms/ContentElements/RecentNews.xml.:

    <T3DataStructure>
       <sheets>
          <sDEF>
                <ROOT>
                   <TCEforms>
                      <sheetTitle>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_tab.settings</sheetTitle>
                   </TCEforms>
                   <type>array</type>
                   <el>
                      <!-- Limit Start -->
                      <settings.limit>
                            <TCEforms>
                               <label>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_additional.limit</label>
                               <config>
                                  <type>input</type>
                                  <size>5</size>
                                  <eval>num</eval>
                               </config>
                            </TCEforms>
                      </settings.limit>
    
                      <!-- Offset -->
                      <settings.offset>
                            <TCEforms>
                               <label>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_additional.offset</label>
                               <config>
                                  <type>input</type>
                                  <size>5</size>
                                  <eval>num</eval>
                               </config>
                            </TCEforms>
                      </settings.offset>
    
                      <!-- Category Mode -->
                      <settings.categoryConjunction>
                            <TCEforms>
                               <label>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction</label>
                               <config>
                                  <type>select</type>
                                  <renderType>selectSingle</renderType>
                                  <items>
                                        <numIndex index="0" type="array">
                                           <numIndex index="0">LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction.all</numIndex>
                                           <numIndex index="1"></numIndex>
                                        </numIndex>
                                        <numIndex index="1">
                                           <numIndex index="0">LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction.or</numIndex>
                                           <numIndex index="1">or</numIndex>
                                        </numIndex>
                                        <numIndex index="2">
                                           <numIndex index="0">LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction.and</numIndex>
                                           <numIndex index="1">and</numIndex>
                                        </numIndex>
                                        <numIndex index="3">
                                           <numIndex index="0">LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction.notor</numIndex>
                                           <numIndex index="1">notor</numIndex>
                                        </numIndex>
                                        <numIndex index="4">
                                           <numIndex index="0">LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categoryConjunction.notand</numIndex>
                                           <numIndex index="1">notand</numIndex>
                                        </numIndex>
                                  </items>
                               </config>
                            </TCEforms>
                      </settings.categoryConjunction>
    
                      <!-- Category -->
                      <settings.categories>
                            <TCEforms>
                               <label>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.categories</label>
                               <config>
                                  <type>select</type>
                                  <renderMode>tree</renderMode>
                                  <renderType>selectTree</renderType>
                                  <treeConfig>
                                        <dataProvider>GeorgRinger\News\TreeProvider\DatabaseTreeDataProvider</dataProvider>
                                        <parentField>parent</parentField>
                                        <appearance>
                                           <maxLevels>99</maxLevels>
                                           <expandAll>TRUE</expandAll>
                                           <showHeader>TRUE</showHeader>
                                        </appearance>
                                  </treeConfig>
                                  <foreign_table>sys_category</foreign_table>
                                  <foreign_table_where>AND (sys_category.sys_language_uid = 0 OR sys_category.l10n_parent = 0) ORDER BY sys_category.sorting</foreign_table_where>
                                  <size>15</size>
                                  <minitems>0</minitems>
                                  <maxitems>99</maxitems>
                               </config>
                            </TCEforms>
                      </settings.categories>
    
                      <!-- Include sub categories -->
                      <settings.includeSubCategories>
                            <TCEforms>
                               <label>LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.includeSubCategories</label>
                               <config>
                                  <type>check</type>
                               </config>
                            </TCEforms>
                      </settings.includeSubCategories>
                   </el>
                </ROOT>
          </sDEF>
       </sheets>
    </T3DataStructure>
    
  4. Configure PageTSconfig for the content element to add it to the new content element wizard:

    mod {
        wizards.newContentElement.wizardItems.common {
            elements {
                news_recent {
                    iconIdentifier = content-recent-news
                    title = Recent News
                    description = Displayes recent news
                    tt_content_defValues {
                        CType = news_recent
                        pi_flexform (
                            <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
                            <T3FlexForms>
                                <data>
                                    <sheet index="sDEF">
                                        <language index="lDEF">
                                            <field index="settings.limit">
                                                <value index="vDEF">4</value>
                                            </field>
                                        </language>
                                    </sheet>
                                </data>
                            </T3FlexForms>
                        )
                    }
                }
            }
            show := addToList(news_recent)
        }
        web_layout.tt_content.preview.news_recent = EXT:sitepackage/Resources/Private/Templates/ContentElementsPreview/RecentNews.html
    }
    
  5. Configure TypoScript for rendering of content element: (This example assumes EXT:fluid_styled_content is used)

    plugin.tx_news_recent {
        settings {
            orderBy = datetime
            orderDirection = desc
        }
        view {
            templateRootPaths {
                10 = EXT:sitepackage/Resources/Private/Templates/Plugins/News/RecentNews/
            }
            pluginNamespace = news_recent
        }
    }
    
  6. Add fluid template accordingly to configured paths.

  7. Optionally, register Icon for content element within ext_localconf.php:

    $icons = [
        'content-recent-news' => 'EXT:news/Resources/Public/Icons/Extension.svg',
    ];
    $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
    foreach ($icons as $identifier => $path) {
        $iconRegistry->registerIcon(
            $identifier,
            \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
            ['source' => $path]
        );
    }
    

Note

For several reasons, don’t hardcode labels, instead use LLL:EXT:sitepackage/Resources/Private/locallang.xlf references. In order to keep the example code short, this rule is broken.

Acknowledgements

Acknowledgements to pietzpluswild GmbH and KM2 >> GmbH who allowed me to dive into the topic and to implement a solution for their customers.

Also thanks to Josef Glatz for proof reading and contributing to the Blog post. He also motivated me to finish this post.

Checked for TYPO3 Versions

The post was checked against TYPO3 version 8 LTS, 9 LTS.