Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

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…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/guides/extensions.md

On this page

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

FactWhere 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-user

Expected: the block [ExtensionSettings] with ExtensionDirectory=extension and one ActiveExtensions[]=<name> line per active extension, in loading order.

2. Find an extension

  1. 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.
  2. 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.
  3. 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-user

Expected: 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/ezflow

By 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.ezpkg

php 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.xml

Expected: 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-user

The 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-user

Expected 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.

  1. Find out which file sets the value and what is in effect:

    ./console exp:ini where site.ini/ExtensionSettings/ActiveExtensions --allow-root-user

    Expected: every file that sets the variable, in load order, and the value in effect.

  2. Change it in the scope you mean:

    Scope wordFile writtenUse 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-user
  3. Clear 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/hello

Create these files.

extension/myhello/ezinfo.php (what Setup > About and the extension list read):

<?php
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):

<?xml version="1.0" encoding="utf-8" ?>
<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):

<?php /* #?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):

<?php /* #?ini charset="utf-8"?

[ExtensionSettings]
DesignExtensions[]=myhello

*/ ?>

extension/myhello/modules/hello/module.php (the module, with one view):

<?php
$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):

<?php
$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-user

Expected 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(); in module.php and '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.

  1. Open /setup/rad. The page lists the extension points in seven groups (content, templates, modules, workflow, storage, packaging, access); /setup/rad/(show)/tools lists only the ones with a tool.
  2. 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.
  3. 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.
  4. Switch the new extension on (section 4) and regenerate the autoloads.
  5. 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 wantOpen
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.

  1. Open /setup/handlerextension/mail. The page is titled for that kind and says what the default does and what you replace.
  2. 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.
  3. 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.
  4. Press Refresh preview. Every file, before it is written lists the paths.
  5. Press Create in extension/. Expected: a message listing the files written under extension/my_mail.
  6. 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-user

Expected: 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 wantPut it in your extensionThen
Your own settingssettings/<name>.ini.append.php, or a new settings/<name>.ini with defaultsread with eZINI::instance( '<name>.ini' ); show or change them with exp:ini
A cronjob partcronjobs/<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 commandbin/php/<name>.phpphp extension/<name>/bin/php/<name>.php --help
A translationtranslations/<locale>/translation.tsadd TranslationExtensions[]=<name> in [RegionalSettings]
A database tableshare/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 keysa 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 previewa view mode in content.ini [VersionView] ViewModes[] and its node/view/<mode>.tplsee Version preview in other view modes
A policy limitation of your own for content/read and the other content functionsa 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.

  1. Choose the number from what is already published. Never from memory:

    git fetch --tags
    git tag -l 'v*' --sort=version:refname | tail -5

    Sort by version, not by name: 0.1.10 sorts before 0.1.8 by name. After 0.1.9 the next release is 0.1.10; only the last position counts up unless you decide otherwise.

  2. Write the number into both metadata files, 'Version' in ezinfo.php and <version> in extension.xml, without a v prefix, and into version in composer.json if you keep one. Check:

    php -l extension/myhello/ezinfo.php
    xmllint --noout extension/myhello/extension.xml

    Expected: No syntax errors detected and no output from xmllint.

  3. 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 (read bash bin/shell/generatefilelist.sh --help first, and use --dry-run); the explanation is in File consistency check.

  4. 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.1
  5. Tell 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.

  6. 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

SymptomCheck
The page /hello/world says the module does not existThe 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 contentThe 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 foundphp 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 versionezinfo.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

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.

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x