Episode 13

One Manifest: TYPO3 Extensions No Longer Need an ext_emconf.php

Benni Mack
Benni Mack more

For twenty years, every TYPO3 extension carried a file called ext_emconf.php. It told the Extension Manager what the extension was, which version it had and which TYPO3 it worked with. Then Composer came along, and every extension carried a second file that said the same things in a different way. Since September 2026, composer.json is the only manifest a TYPO3 extension needs, in TYPO3 itself, on extensions.typo3.org and in the tooling around it.

This is not a story about deleting a file. It is a story about how many pieces have to move before one file can go, and about the people who moved them.

The Background

When Kasper Skårhøj split TYPO3 into a core and extensions with version 3.5 in 2003, three things arrived together: the extension concept, the Extension Manager to install extensions, and the TYPO3 Extension Repository, TER for short, to share them. The first revision of the Core’s version history from October 2003 already has an ext_emconf.php in every system extension. The Extension Manager set $_EXTKEY to the folder name, included the file, and knew everything it needed.

A year later the .t3x archive format followed, because PHP 4 could not open a zip file. A .t3x is a serialized PHP array with every file of the extension in it, compressed, with an MD5 in front. The Extension Manager unpacked it and wrote a fresh ext_emconf.php from the metadata inside. The file an author uploaded was never the file that arrived at the other end.

With TYPO3 4.0 in 2006, the Extension Manager started downloading one big list of every version of every extension from a mirror: extensions.xml.gz. TYPO3 14.3 still fetches that very file today.

A Brief History

Two Manifests

TYPO3 6.0 added zip support next to the .t3x, and Composer support matured through TYPO3 7 and 8. From then on every public extension had a composer.json for Composer projects and an ext_emconf.php for classic installations and for TER. We all kept them in sync by hand, by script or by CI, and every now and then a release went out with two files disagreeing about their own version number.

Helmut Hummel carried TYPO3’s Composer integration through those years: the Composer installers, the package artifact that replaced PackageStates.php in Composer mode, and in 2021 the change that made TYPO3 prefer the extension key declared in composer.json over the folder name. TYPO3 11.4 stopped reading ext_emconf.php in Composer mode altogether. Classic mode kept it, because classic mode had nothing else to read yet.

The extension key deserves its own paragraph. For eighteen years it simply was the folder name under typo3conf/ext/. The Composer installer derived a key from the package name when nobody declared one, vendor/my-ext became my_ext, and placed the extension in a folder of that name so the old rule kept holding. Since 11.4, the declared key wins. Since 14.0, the declared key is the only one there is.

The TER Side

Until 2020 there were two ways to publish an extension: the upload form on typo3.org, or a SOAP endpoint the Extension Manager used behind the scenes. In autumn 2020 Oliver Bartsch built a REST API for TER, and in the following weeks he and I wrote tailor, a small command line tool that registers keys, sets versions and publishes releases from a pipeline. TYPO3 11.0 rebuilt the Extension Manager around that API and dropped .t3x uploads. Every extension released by a GitHub Action today goes through Oli’s API.

TYPO3 14 and 15

TYPO3 14 is where the Core stopped waiting. In 14.0 we made composer.json mandatory for extension detection in classic mode. Helmut moved title and description to composer.json in the same release, and in 14.2 the version, the state and the constraints followed, with ext_emconf.php deprecated for good. Christian Kuhn (a.k.a. Lolli) removed the deprecated code for TYPO3 15 in May 2026. Since then the Core does not read ext_emconf.php at all.

In 14.3.7 the Extension Manager switched from the .t3x to the zip TER serves, verified by a SHA-256 published in the extension index. That patch depended on a TER column that existed in the database but never reached the API, because it was missing from the TCA. Three layers of fail-soft code and no test had kept that invisible for months. “Shipped” and “working” are different words.

What Changed in September 2026

One place still demanded ext_emconf.php: the upload to extensions.typo3.org. TYPO3 no longer wanted the file, TER refused a zip without it.

The TER as of 14 September 2026 closes that gap:

What you upload is what gets installed, and one file describes your extension everywhere.

tailor just followed in its latest release. It validates composer.json the way TER reads it, checks ext_emconf.php only for what composer.json does not say, and set-version no longer stops when there is no ext_emconf.php to update.

How a composer.json for TYPO3 Should Look

This is the part worth bookmarking. A complete manifest first, then what each field does and who reads it.


			
{
    "name": "vendor/my-ext",
    "type": "typo3-cms-extension",
    "description": "My Extension - Adds the one thing your editors asked for",
    "license": "GPL-2.0-or-later",
    "homepage": "https://github.com/vendor/my-ext",
    "authors": [
        {
            "name": "Jane Doe",
            "email": "[email protected]",
            "role": "Developer"
        }
    ],
    "support": {
        "issues": "https://github.com/vendor/my-ext/issues",
        "source": "https://github.com/vendor/my-ext",
        "docs": "https://docs.typo3.org/p/vendor/my-ext/main/en-us/"
    },
    "require": {
        "php": "^8.2",
        "typo3/cms-core": "^13.4 || ^14.3"
    },
    "suggest": {
        "typo3/cms-dashboard": "Adds a widget with the numbers your editors asked for"
    },
    "autoload": {
        "psr-4": {
            "Vendor\\MyExt\\": "Classes/"
        }
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "my_ext"
        }
    }
}
		

What to Leave Out

When You Still Need an ext_emconf.php

Two cases remain. Your extension supports TYPO3 v13 or older in classic mode, because those versions look for the file. Or you want a category and a state on extensions.typo3.org, which composer.json cannot express. Keep it minimal and in sync.

<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'My Extension',
    'description' => 'Adds the one thing your editors asked for',
    'category' => 'plugin',
    'state' => 'stable',
    'version' => '2.1.0',
    'constraints' => [
        'depends' => [
            'typo3' => '13.4.0-14.99.99',
        ],
    ],
];
		

Publish From Your Pipeline


			
- run: composer require --dev typo3/tailor
- run: ./vendor/bin/tailor set-version ${{ github.ref_name }} --no-docs
- run: ./vendor/bin/tailor ter:publish ${{ github.ref_name }} --comment "${{ github.event.release.body }}"
  env:
    TYPO3_API_TOKEN: ${{ secrets.TYPO3_API_TOKEN }}
		

tailor packs the contents of your extension directory, validates the manifests the way TER reads them, and tells you what TER would have complained about before it uploads anything. Only the files at the root of the extension count: a fixture extension under Tests/ with its own ext_emconf.php can no longer veto a release with its version number.

What Comes Next

The .t3x is still generated for every version on TER. It stays until the Extension Managers of 14.2 and older have left the stage, then it goes away for versions that require 14.3 or newer. In the Core, the Extension Manager is losing its last ext_emconf.php readers in a patch under review, and two changes for TYPO3 15 follow: the “Composer Support of Extensions” module from the v11 migration retires, and the .t3x download fallback goes, which removes the last class that knows how to write an ext_emconf.php.

Thanks

A change that spans two decades and three code bases is never one person’s work:

For me, this closes a gap I first ran into while writing sync scripts for two manifests years ago. If your extensions still carry both files, now is a good time to look at your composer.json. And if you run into anything on the way, let me know on GitHub, or get in touch.

More Articles by Benni Mack