What’s ext_emconf.php? What’s TER? And that does it have to do with Composer? The success story of decade-long support and upgrade paths!
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.
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.
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.
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 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.
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:
require, conflict and suggest sections. Composer constraints such as ^13.4 are translated into the range notation the Extension Manager understands, typo3/cms-core becomes the TYPO3 dependency, and other packages are matched against the composer names registered on TER.composer.json cannot express: the category, the state and an author company. Uploads with only an ext_emconf.php work as before. Nothing that publishes today stops publishing.ext_emconf.php, because TYPO3 v13 in classic mode finds extensions by looking for that file.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.
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"
}
}
}
name is your Packagist name. TER stores it, and if your extension has no composer name registered yet, TER registers this one, provided it is well-formed and no other extension has claimed it. Other extensions reference you by this name in their require, and TER resolves it back to your extension key when it reads their manifests.type must be typo3-cms-extension. TYPO3 detects an extension by this type since 14.0, the Composer installer places it by this type, and TER rejects an upload without it.description is one string that becomes two fields: everything before the first “ - “ is the title, everything after it the description. This is how the Extension Manager fills its list since 14.0 and how TER fills its listing now. Without the separator, the whole string is the title. If you also ship an ext_emconf.php with a title, TER falls back to it when your description has no separator, so existing listings do not change.license is required by Packagist anyway. TER stores it now and reports it in the FAIR metadata; before September every TER extension claimed the default license there.authors: the first entry becomes the author name and email TER shows. A company has no field in composer.json; it is one of the three things an ext_emconf.php can still add.support.docs is the documentation URL. TER uses it for the documentation status and the FAIR metadata.require is the heart of it. typo3/cms-core becomes the TYPO3 dependency, and ^13.4 || ^14.3 is translated to 13.4.0-14.99.99, which the Extension Manager understands. php stays a PHP dependency, typo3/cms-fluid-styled-content becomes fluid_styled_content, and a third-party extension is resolved through its composer name on TER. A package TER does not know, a Symfony component for instance, is left out of the dependency list, because a classic installation could not install it anyway. Declare the TYPO3 versions you actually test, nothing more generous.suggest is mapped the same way, but the values are free text, as Composer defines them.autoload points PSR-4 to Classes/. Composer needs it, and TYPO3 generates the class loading information for classic mode from it.extra.typo3/cms.extension-key is the key. Not the folder name, not derived from the package name, declared. Mandatory in Composer mode since 11.4 and in classic mode since 14.0.version.Composer advises against it for tagged packages, and TYPO3 does not need it. TER takes the version from the publish command and the archive name, and the Extension Manager writes the version it downloaded into extra.typo3/cms.version on install. If you declare it anyway, it has to match what you publish; tailor set-version keeps it in sync.extra.typo3/cms.Package.providesPackages. It tells a classic installation which Composer packages your extension ships in its own vendor folder. Mandatory in classic mode since 15.0, but the Extension Manager fills it in on download since 14.3. Set it only if you build classic bundles by hand.replace, provide, config, scripts. Nothing in the TYPO3 tool chain reads them for extensions.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',
],
],
];
- 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.
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.
A change that spans two decades and three code bases is never one person’s work:
composer.json in TYPO3 14ext_emconf.phpFor 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.
What’s ext_emconf.php? What’s TER? And that does it have to do with Composer? The success story of decade-long support and upgrade paths!
With TYPO3 v14, Camino introduces a default theme that removes friction from first installs. Why this matters—and how TYPO3 laid the groundwork.
Composer and TYPO3 have had a long journey together. Now, work done in v11 is paving the way for v12—so that TYPO3 can become a fully-realized standard Composer project and consequently, an even more secure CMS.
In 2021, many individuals and companies sponsored bug fixes and new features in TYPO3. We’re grateful for their support!
I’m pretty sure that the new Folder Tree in TYPO3 v11.1 feature will make editors daily lives with TYPO3 easier.
Let’s see if we can improve TYPO3’s Page Tree, from a non-developer perspective
Welcome to Core Insights by b13 - what is this place?