You too can become a hero and support this Blog at GitHub Sponsors or Patreon.

More about support

Extensions are the essence of TYPO3. There is (nearly) nothing that cannot be achieved through a TYPO3 extension. From a small adjustment to the backend configuration up to a fully integrated web application in the center of a huge business. During the (long) lifetime of TYPO3 old standards changed and new ones got introduced.

This post is a collection of good practices when developing a TYPO3 extension. It is by no means complete. So please contact me if you feel an information is missing or plainly wrong.

Also have a look at Oliver Klees collection of "Best Practices with Extbase and Fluid".

Folder structure

Most likely an extension contains some kind of code. This code will reside in files and those files want to be placed in folders. So the following folders should be used in TYPO3 extensions. Each has its dedicated type of content but all of them are optional.

  • Classes. This is where PHP code goes. It serves as root for the PSR-4 based autoloading, which means the subfolder structure must match the namespace after the vendor and extension key. (Extbase) Extensions put their models in Domain/Model, their repositories in Domain/Repository, their controllers in Controller and so forth.
  • Configuration. Here lives any kind of TYPO3 configuration. Every type has its own subfolder: FlexForms, PageTSconfig, TCA, TypoScript, UserTSconfig.
  • Documentation. For any extension uploaded to TER, a .rst based documentation is highly recommended. The files and directories should be located in this folder.
  • Resources. The Resources folder is split into a Public and a Private subfolder. Where Public contains only files that have to be accessible through web requests like CSS or JavaScript files, images or other assets, Private holds files that are also resources but that are not ment to be publicly available.  Resources/Private should be protected by the webserver (e.g. with a .htaccess rule if you are using apache). Private resources are e.g. fluid templates, partials and layouts, as well as language files or sass, resp. less files.
  • Tests. If you have unit or functional (or even acceptance) tests for your application code, they go here.

So a TYPO3 extension structure might look like this:

tree my_extension
├── Classes
│   ├── Command
│   ├── Controller
│   ├── Domain
│   │   ├── Model
│   │   └── Repository
│   ├── Service
│   ├── Utility
│   └── ViewHelpers
├── Configuration
│   ├── FlexForms
│   ├── PageTSconfig
│   ├── TCA
│   │   └── Overrides
│   └── TypoScript
├── Documentation
├── Resources
│   ├── Private
│   │   ├── Language
│   │   ├── Layouts
│   │   ├── Partials
│   │   └── Templates
│   └── Public
│       ├── Css
│       ├── Images
│       └── JavaScript
└── Tests
    ├── Functional
    │   └── Controller
    └── Unit
        └── Domain
            └── Model

In the old(er) days of TYPO3 there were different standards (if any). You might see folders like res, pi1, doc, lib, mod1, static, cm1, compat, hook, modfunc1 ... If you can, don't keep them. Identify the type of the content and migrate it to the appropriate folder from the structure above. If you do, make sure to migrate all references to files as well (e.g. in includes).



The Table Configuration Array (TCA) is the glue between the database and the CMS. It basically holds the configuration of what database fields should be editable in the backend and how they should be rendered. It is well documented but where to store TCA configuration in an extension was kind of a mess until TYPO3 6.2 LTS.

Many extensions had their TCA completely in the ext_tables.php file or they outsourced the columns configuration to a 'dynamicConfigFile' like tca.php or something like this. Changes to existing TCA configuration e.g. to core tables were also located in the ext_tables.php. The situation gave the overall impression that "clean code" and TCA were just not compatible.

In the blogpost "Cleaning the Hood: TCA" Andreas Fernandez told us already how to keep things clean in TYPO3 6.2 and upward:

The TCA configuration for a new table goes into a file with the exact name of the table in Configuration/TCA/. This file returns the whole array. No dynamicConfigFile, nothing else. Just the complete TCA configuration for the table. And if you have more than one table added by your extension you have one file per table.

For example the file my_extension/Configuration/TCA/tx_myextension_domain_model_mymodel.php simply returns the TCA config for this table:

return [
  'ctrl' => [...],
  'interface' => [...],
  'columns' => [...],
  'types' => [...]

As for changing already existing TCA configurations from the core or another extension, we should also create one file per table we want to change. But this time in Configuration/TCA/Overrides. This is also true for the call of any helper method that lets us register, add or modify something in the TCA. E.g. if we want to register an extbase plugin, it means in nearly every case that we nee to add an option to the select of list_type in the tt_content table. Most of the time ExtensionUtility::registerPlugin() is used for this but still it is a change in the TCA, so it belongs in Configuration/TCA/Overrides.

Let's have a look at an example that modifies the tt_content TCA in Configuration/TCA/Overrides/tt_content.php:

defined('TYPO3_MODE') or die();

        'Plugin Description'
    $newColumn = [
        'programming_language' => [
            'exclude' => true,
            'label' => 'Programming Language',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $newColumn);

    // Reload on change
    $GLOBALS['TCA']['tt_content']['ctrl']['requestUpdate'] .= ',programming_language';
    // Use type icon
    $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes']['fs_code_snippet'] = 'fs-code-snippet';

Wrapping the modifications to the TCA in a call_user_func() ensures that variables are only declared inside the functions scope rather than globally. I consider this to be a good practice.



TYPO3 extensions can already be downloaded by composer. This is due to a "local" composer repository a where every TER extension is made available (if you want to know more, read my introduction to the topic here). A modern TYPO3 extension should ship a composer.json. But what should be in it?

Helmut Hummel posted a "composer.json specification for TYPO3 extensions" in his blog, a post that you should read! To summarize the must-haves: A composer.json in a TYPO3 extension must contain a name like danielgoerz/my-extension as well as an description of what the extension does. The type has to be exactly typo3-cms-extension. This is important so composer recognizes a TYPO3 extension as such and downloads it to typo3conf/ext/. There has to be a requirement for the TYPO3 core as well as autoloading definitions and a replace section if the extension name contains an underscore (which is converted to a dash in the composer.json, so my_extension will become my-extension for composer).

A minimal composer.json for an extension called my_extension looks like this:

  "name": "danielgoerz/my-extension",
  "description": "An example extension for TYPO3",
  "type": "typo3-cms-extension",
  "require": {
    "typo3/cms-core": ">=7.6.0 <9.0.0"
  "replace": {
    "my_extension": "self.version",
    "typo3-ter/my-extension": "self.version"
  "autoload": {
    "psr-4": {
      "DanielGoerz\\MyExtension\\": "Classes/"

For a detailed description of every section a composer.json could, should and must have read Helmuts article.

One more thing: If your extension is publicly available, please register it at This has several upsides:

  • You own "your" vendor-name after you registered the first package with the vendor-name. If anybody else registers anything with "your" vendor-name first, you will no longer be able to use it yourself.
  • Your extension can be required with vendor-name/ext-name instead of typo3-ter/ext-name. This means that you are independent of th composer integration on, which might be less stable than packagist. Additionally if a project only uses extensions that are registered at packagist, there is no need to add in the projects root composer.json any more.


Common files

There are some famous files that can exist in the root directory of any extension in addition to the ext_emconf.php wich is mandatory for an extension to be installable by the extension manager module. These are:


Try to avoid using this file and if you have to use it try to put as little code as possible into it. It gets loaded last, after all other configuration files and should be avoided whenever possible as the file might not have a very bright future ;).


Use this file for registering to Hooks and Signals, configure plugins or just change $GLOBALS['TYPO3_CONF_VARS']. By the way, don't register a signal just in FE context (TYPO3_MODE === 'FE') because then you cannot find them in the configuration module in the backend. The other way around is fine. If something is only needed in the backend context, skip it in FE mode. It's all about performance.

ext_typoscript_setup.txt and ext_typoscript_constants.txt

TypoScript from these files will be included after all sys_templates. Try to not use it, since it is not flexible at all.



  • Use PSR-2 for your PHP code. At least in TYPO3 7 LTS and above.
  • Try to avoid X-Classes. Use Hooks, Signals or the extendability of extbase wherever possible.
  • Keep an eye on the deprecation log while developing. Do not implement new things on top of deprecated APIs.
  • Put your extension code on github rather than This request was announced at the TYPO3 Developer Days 2016 to reduce the load on the TYPO3 infrastructure.
  • Put as less TypoScript in the database as possible. Instead collect your TypoScript in an extension and include it in the template record in the TYPO3 backend with <INCLUDE_TYPOSCRIPT ...>. Ideally you have one line in the constants and one line in the setup part (each being includes) in your template and that's it.


Further Information