Extensions: find, install, configure, build and release
This guide is for site builders and developers. An extension is a folder under extension/ that adds settings, templates, modules, content types, commands or
cronjobs to Exponential without touching the kernel. By the end of this page you can find an extension, install and
switch one on, configure it, build your own working one (by hand in ten minutes, or generated by the RAD wizards)
and release it so that other people can install it. About 45 minutes, a terminal and a browser. You need a running
Exponential 6 installation (Getting started) and an administrator login. Commands run from the
installation root; the console commands are the same programs as php bin/php/<name>.php.
1. How extensions work in one minute
| Fact | Where it is decided |
|---|---|
An extension is a folder named after the extension, for example extension/ezflow. | [ExtensionSettings] ExtensionDirectory in settings/site.ini (default extension) |
| It does nothing until it is switched on. | [ExtensionSettings] ActiveExtensions[] in settings/site.ini (installation wide) or ActiveAccessExtensions[] (one siteaccess) |
The order of the list is the order of priority: an extension loaded earlier wins over a later one for settings and design templates. With ExtensionOrdering=enabled the declared dependencies in extension.xml decide the loading order first. | The Extensions page, Loading order |
Classes in its classes/ folder are found through an autoload array that you regenerate. | php bin/php/ezpgenerateautoloads.php -e |
Its settings are .ini.append.php files in extension/<name>/settings/. | Settings override placements |
It says what it is in ezinfo.php and extension.xml. | Extension metadata |
See which extensions are switched on in this installation:
./console exp:ini list site.ini/ExtensionSettings --allow-root-userExpected: the block [ExtensionSettings] with ExtensionDirectory=extension and one ActiveExtensions[]=<name>
line per active extension, in loading order.
2. Find an extension
- Open Setup > Extensions (
/setup/extensions) in the administration interface. The list shows every extension that is on disk, active ones first in loading order, then the rest by name. - Search, filter (all, active, inactive, problems) or order the list by name. Each card shows the description, licence, project page and dependencies; Details adds the author, the designs in use and the downloads. The whole page is explained in The Extensions page.
- The version comes from the extension's own files, so a dash means the extension does not state one.
From a terminal, the extensions that exist but are not active are the scopes marked inactive:
./console exp:ini scopes extension --allow-root-userExpected: one line per extension, for example
extension:autonotifications extension extension/autonotifications/settings (inactive extension).
Where to look for extensions that are not on your disk yet: the Composer packages se7enxweb/<name> on Packagist
(for example se7enxweb/ezflow) and the project pages named in the
extension catalogue, which has one page per shipped extension.
Reference: Extension list: sort, inspect and download.
3. Install one
Pick the way that fits. All three end with the same folder under extension/.
With Composer (the extension's package declares the ezpublish-legacy-extension install type; see the extension's
page for the package name):
composer require se7enxweb/ezflowBy copying the folder (a release archive or a git clone):
cd extension
git clone https://github.com/se7enxweb/ezflow.git ezflow
cd ..Only clone by hand an extension that composer.json does not require. For a Composer package, let Composer make
the clone (composer config --global 'preferred-install.se7enxweb/*' source, then composer require or
composer reinstall): a hand-made clone in place of a package Composer installed as an archive is replaced, history
and commits included, at the package's next update. See Develop with Git.
From a package (.ezpkg, for example one you downloaded in section 7):
php ezpm.php import /path/to/ezflow.ezpkgphp ezpm.php help lists the package commands; ezpm: the package manager on the command line
explains each. Install into a copy of the site first: the package installer writes content.
Check that the folder is where the kernel looks:
ls extension/ezflow/ezinfo.php extension/ezflow/extension.xmlExpected: both file names are printed. If you keep your own extensions apart from the ones you install, add a second folder as described in Extensions in more than one folder.
4. Switch it on
Browser: open Setup > Extensions, click Activate on the extension's card, then Review changes and
Apply changes. The review shows the risks and the resulting ActiveExtensions[] lines; the page then tells you
what it saved and where it kept a backup copy of the settings file (The Extensions page).
Terminal (the same change, in the global override file):
./console exp:ini add 'site.ini/ExtensionSettings/ActiveExtensions[]' ezflow global --dry-run --allow-root-user
./console exp:ini add 'site.ini/ExtensionSettings/ActiveExtensions[]' ezflow global --allow-root-userThe first command only prints the change; expected: the whole settings/override/site.ini.append.php as it would
be, with +ActiveExtensions[]=ezflow marked, and the line Dry run: ... nothing written. The second writes it
(a backup of the file goes to var/backup/ini/<timestamp>/). To switch it on for one siteaccess only, use the scope
siteaccess:<name> and the variable ActiveAccessExtensions[].
Then make the system see the new classes and settings:
php bin/php/ezpgenerateautoloads.php -e
php bin/php/ezcache.php --clear-all --allow-root-userExpected from the first: Found <n> classes, added <n> of them to the autoload array. (add -n first to see the
count without writing anything). If the site is served by PHP-FPM or Velocity, reload that service afterwards so its
workers see the change: see Deploying and Operating a site.
Many extensions need more than the switch: a database table, a content class, a cronjob. Read the extension's page in
the catalogue for those steps. Remember that a new .tpl file needs the
template override cache cleared, which --clear-all does.
5. Configure it
An extension's defaults live in extension/<name>/settings/*.ini.append.php. You never edit those. You override them
in a place that wins over them and survives an upgrade.
Find out which file sets the value and what is in effect:
./console exp:ini where site.ini/ExtensionSettings/ActiveExtensions --allow-root-userExpected: every file that sets the variable, in load order, and the value in effect.
Change it in the scope you mean:
Scope word File written Use it for globalsettings/override/<file>.ini.append.phpthis installation siteaccess:<name>settings/siteaccess/<name>/<file>.ini.append.phpone site of a multi-site installation extension:<name>extension/<name>/settings/<file>.ini.append.phpthe defaults an extension ships ./console exp:ini set site.ini/SiteSettings/SiteName "My site" siteaccess:site --allow-root-userClear the INI cache so the change is read:
php bin/php/ezcache.php --clear-tag=ini --allow-root-user.
Never commit settings/override/; it can hold passwords. More on the command:
Change settings from the command line. Settings that apply only while a
siteaccess runs, carried inside an extension: Per-site settings inside extensions.
6. Build your own extension
6.1 The smallest working extension, by hand
You will make a page /hello/world/<name> that greets by name. Pick a name that is yours: lower case, letters,
digits and underscores. This guide uses myhello.
mkdir -p extension/myhello/settings extension/myhello/modules/hello \
extension/myhello/design/standard/templates/helloCreate these files.
extension/myhello/ezinfo.php (what Setup > About and the extension list read):
class myhelloInfo
{
public static function info()
{
return array( 'Name' => "My Hello",
'Version' => "0.1.0",
'Copyright' => "Copyright (C) 2026 You",
'License' => "GNU General Public License v2.0 (or any later version)",
'Info_url' => "https://example.com/myhello" );
}
}
extension/myhello/extension.xml (read first; keep its version equal to the one above):
<software>
<metadata>
<name>My Hello</name>
<version>0.1.0</version>
<license>GNU General Public License v2.0 (or any later version)</license>
<info_url>https://example.com/myhello</info_url>
</metadata>
</software>extension/myhello/settings/module.ini.append.php (tells the kernel to look for modules in this extension):
/* #?ini charset="utf-8"?
[ModuleSettings]
ExtensionRepositories[]=myhello
*/ extension/myhello/settings/design.ini.append.php (tells the template system to look in this extension's design):
/* #?ini charset="utf-8"?
[ExtensionSettings]
DesignExtensions[]=myhello
*/ extension/myhello/modules/hello/module.php (the module, with one view):
$Module = array( 'name' => 'hello' );
$ViewList = array();
$ViewList['world'] = array( 'script' => 'world.php',
'params' => array( 'Name' ) );
extension/myhello/modules/hello/world.php (the view: it returns the page content):
$Module = $Params['Module'];
$name = $Params['Name'] ? $Params['Name'] : 'world';
$tpl = eZTemplate::factory();
$tpl->setVariable( 'name', $name );
$Result = array();
$Result['content'] = $tpl->fetch( 'design:hello/world.tpl' );
$Result['path'] = array( array( 'text' => 'Hello', 'url' => false ) );
extension/myhello/design/standard/templates/hello/world.tpl (the template; wash escapes what the visitor typed):
<div class="content-view-full">
<h1>Hello, $name|wash</h1>
<p>This page is served by my first extension.</p>
</div>Check that PHP accepts each file, then switch the extension on as in section 4:
php -l extension/myhello/ezinfo.php
php -l extension/myhello/modules/hello/module.php
php -l extension/myhello/modules/hello/world.php
./console exp:ini add 'site.ini/ExtensionSettings/ActiveExtensions[]' myhello global --allow-root-user
php bin/php/ezpgenerateautoloads.php -e
php bin/php/ezcache.php --clear-all --allow-root-userExpected from php -l: No syntax errors detected in <file> three times. Then open /hello/world/Ada on your site:
the page shows Hello, Ada inside your site's layout. /setup/extensions now lists My Hello with version 0.1.0.
Two things to know before you build on it:
- A view without a
'functions'entry has no access control: anyone who can reach the site can open it. Before it shows anything private, declare a function and policy:$FunctionList['read'] = array();inmodule.phpand'functions' => array( 'read' )on the view, then grant it in Users > Roles and policies. See Request rules and Security and audit. - A page served by a persistent worker (Velocity) needs the workers to see new classes: reload as described in Deploying.
New code is written as classes: from 6.0.15 the product's own commands, cronjob parts and views are classes, and the plain files you wrote above keep working. To write the view as a class, read Commands, cronjob parts and module views as classes.
6.2 Let the RAD tools write it
The RAD tools (Setup > RAD, /setup/rad) know every place Exponential can be extended and write a complete
extension for most of them. You need the setup/setup policy.
- Open
/setup/rad. The page lists the extension points in seven groups (content, templates, modules, workflow, storage, packaging, access);/setup/rad/(show)/toolslists only the ones with a tool. - Press the tool of the point you want. The wizard asks for an extension name and what to generate, then lists
every file it would write under
extension/<name>, before writing any. - Press Create in extension/. A wizard never overwrites an extension that exists and writes nothing outside
extension/<name>. Download as zip gives you the same files without writing them. - Switch the new extension on (section 4) and regenerate the autoloads.
- Open Setup > RAD > Extension point survey (
/setup/radsurvey) and the health check on/setup/rad: your registration is counted, and anything configured that cannot work is listed with how to fix it.
The wizards and their addresses:
| You want | Open |
|---|---|
| A design (templates, stylesheets, overrides) | /setup/designextension |
| Template operators, fetch functions, template functions | /setup/templateoperator |
| A handler that replaces a part of the system (login, mail, session, search, shop VAT and shipping, REST filters, ...) | /setup/handlerextension/<kind> |
| An extension that only carries settings | /setup/settingsextension |
| A content class written as a script | /setup/contentextension |
| A datatype | /setup/datatype |
| A workflow event | /setup/workflowevent |
| A module with views, or a module over existing database tables | /setup/moduleextension, /setup/modulewizard |
| A kernel class replaced (the last resort) | /setup/kerneloverride |
Reference: RAD tools, the curated extension points, RAD security.
6.3 The handler wizard, step by step
A handler is a class of yours that the system calls in place of its own default, chosen by one ini setting. The mail handler is a good first one.
- Open
/setup/handlerextension/mail. The page is titled for that kind and says what the default does and what you replace. - Under The extension fill Extension name (for example
my_mail), a title and a summary, and keep or change the class name, author, version and licence the wizard proposes. - Under What goes in it leave the ticked parts on. What has to be written lists the methods your class must implement; Switching it on shows the ini line the wizard writes for you.
- Press Refresh preview. Every file, before it is written lists the paths.
- Press Create in extension/. Expected: a message listing the files written under
extension/my_mail. - Switch the extension on (section 4), regenerate the autoloads, clear the caches. The generated class is called by the system as soon as the setting it wrote selects it; edit the class to do your work.
Verify that every class of the installation, including yours, can be loaded:
php bin/php/checkclasses.php --quiet-ok --allow-root-userExpected: no output when everything loads. A class that cannot be loaded is listed by name (it would otherwise take a whole request down the first time something touched it).
6.4 Settings, content and cronjobs of your own
| You want | Put it in your extension | Then |
|---|---|---|
| Your own settings | settings/<name>.ini.append.php, or a new settings/<name>.ini with defaults | read with eZINI::instance( '<name>.ini' ); show or change them with exp:ini |
| A cronjob part | cronjobs/<part>.php, and in settings/cronjob.ini.append.php the lines [CronjobSettings] ExtensionDirectories[]=<name>, then a block [CronjobPart-<part>] with Scripts[]=<part>.php | ./console list cron shows it as cron:<part> |
| A command | bin/php/<name>.php | php extension/<name>/bin/php/<name>.php --help |
| A translation | translations/<locale>/translation.ts | add TranslationExtensions[]=<name> in [RegionalSettings] |
| A database table | share/db_schema.dba (the wizards write it when asked) | install it with the package installer |
| A say in downloads, edit access to drafts, collaboration items, notifications or view cache keys | a listener of content/download/access, content/edit/access, collaboration/item/access, content/notification/create or content/view/cachekeys in [Event] Listeners[] (the settings extension wizard writes one with an example) | see Access and view cache filters |
| A say in whether a cronjob part runs (a stop switch during a release) | a listener of cronjob/part/run in [RunnableSettings] Listeners[] | see Leaving out a cronjob part |
| A print or PDF layout of a draft in the version preview | a view mode in content.ini [VersionView] ViewModes[] and its node/view/<mode>.tpl | see Version preview in other view modes |
A policy limitation of your own for content/read and the other content functions | a listener of module/functionlist and a class implementing ezpContentLimitationHandler, registered in settings/site.ini.append.php (the settings extension wizard writes both, with the event module/functionlist) | see Content policy limitations of extensions; check a user with ./console exp:access:check |
For the class based forms of a command, cronjob part and view, see Commands, cronjob parts and module views as classes and its specification. To replace a whole kernel module with one of yours, see An extension can replace a kernel module.
7. Release it
A release is a number written into the extension's files, a commit and a tag. A published tag can never be corrected, so check before you push, not after.
Choose the number from what is already published. Never from memory:
git fetch --tags git tag -l 'v*' --sort=version:refname | tail -5Sort by version, not by name:
0.1.10sorts before0.1.8by name. After0.1.9the next release is0.1.10; only the last position counts up unless you decide otherwise.Write the number into both metadata files,
'Version'inezinfo.phpand<version>inextension.xml, without avprefix, and intoversionincomposer.jsonif you keep one. Check:php -l extension/myhello/ezinfo.php xmllint --noout extension/myhello/extension.xmlExpected:
No syntax errors detectedand no output fromxmllint.If the extension ships
share/filelist.md5, refresh it in the same commit so the upgrade check does not list every changed file as modified. The installation's maintainer script does this for extensions that already ship one when given--extensions(readbash bin/shell/generatefilelist.sh --helpfirst, and use--dry-run); the explanation is in File consistency check.Commit, then tag that commit. Never tag first and bump afterwards: the tag would carry the old number.
git add ezinfo.php extension.xml composer.json git commit -m "Updated: Version 0.1.1" git tag v0.1.1 git push origin main v0.1.1Tell Packagist. A package on Packagist reads each tag once; if a new version does not appear, check that the repository's webhook to Packagist exists.
If a published version is wrong, do not move or delete its tag. Publish the next version with the fix and say in its notes what was wrong with the previous one. Packagist caches what it read, so a moved tag produces two different packages under one version number.
To hand an extension to someone without a repository, download it as a package from Setup > Extensions: open
Details on the row and press .ezpkg, .zip, .tar.gz or .tar.bz2. The receiver installs it as in section 3.
An extension that holds a secret value or key material is not packed; the page names the files and settings, and
the secret check says how a maintainer marks a known example.
8. When it does not work
| Symptom | Check |
|---|---|
The page /hello/world says the module does not exist | The extension is in ActiveExtensions[] (section 1), ExtensionRepositories[]=myhello is in its settings/module.ini.append.php, and the caches were cleared |
| The page is empty, or shows the layout with no content | The template path: extension/myhello/design/standard/templates/hello/world.tpl, DesignExtensions[]=myhello, and a new .tpl needs php bin/php/ezcache.php --clear-all --allow-root-user |
Class '...' not found | php bin/php/ezpgenerateautoloads.php -e, then the cache clear; then reload PHP-FPM or Velocity |
| Your setting has no effect | ./console exp:ini where <file>/<Block>/<Variable> --allow-root-user shows who wins; a later extension or the override file may |
| The extension list shows a dash as version | ezinfo.php or extension.xml is missing or does not parse: php -l and xmllint --noout |
| Something in the extension fails quietly | /setup/rad health check lists settings that name a class that cannot load, and php bin/php/checkclasses.php --allow-root-user names the class |
Related pages
- Depth on each step: Extension loading order, Extension metadata, Extensions in more than one folder, RAD tools, Extension points, The extension surface.
- The extensions that ship with the distribution and what each does: catalogue, what a new installation comes with.
- Other guides: Getting started, The content model and editing, Operating a site, Deploying, Security and audit.
- How the extension story developed: January 2024 (modules from extensions), August 2026 (the extension list), September 2026, second half (loading order, metadata).
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.