Joomla Installer Scripts and SQL Updates
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
Most extensions need to do more than copy files. A component creates tables, a plugin has to be enabled once, an update has to rename a column and move data, and a release that drops a template file has to delete the old copy. Joomla gives an extension two hooks for this: an optional install script (a PHP file named by <scriptfile> in the manifest) and SQL files declared in the <install>, <uninstall> and <update> elements. This page explains both in depth, how the installer calls them, what happens when they fail, and how to write them for the current platform.
It was checked on 2026-10-30 against Joomla 6.2.x and PHP 8.3 or later (the minimum of Joomla 6), using the Joomla Developer Manual, the installer classes of the 6.2 source tree (libraries/src/Installer) and the core component com_admin. It is written for the extension developer. The running example is the Tallpine Hiking Club package pkg_tallpine, whose component com_tallpine (namespace Tallpine\Component\Tallpine) owns the table #__tallpine_trails. The manifest as a whole is described in Extension types, manifests and packaging; the administrator screens that run an installation are described in Extensions: installing, updating and update sites; publishing the next version to sites is covered in Update servers and the JED.
What the installer does, in order
Every installer adapter (component, module, plugin, template, library, file, package, language) shares one generic install() method in InstallerAdapter. The order matters because it decides what each hook can rely on.
Several consequences follow from reading InstallerAdapter::install():
-
preflightruns after the manifest has been parsed and the script file loaded, but before any file of the extension is copied and before any database change. It is the only hook that can refuse an installation without leaving anything behind. -
The extension row in
#__extensionsexists by the time the SQL runs, so the SQL and theinstall()/update()methods can use the newextension_id. -
install()orupdate()runs after the SQL. An update script therefore sees the new schema, not the old one. Data migrations that need the old shape must be written as SQL update files instead. -
A
falseresult (or an exception) frompreflight,installorupdateaborts the whole operation and the installer rolls back what it can: copied files and the extension record are removed. SQL statements already executed are not undone, because MySQL and MariaDB commit DDL statements implicitly. A failing update therefore cannot be assumed to leave the schema as it was. -
A
falseresult frompostflight, and fromuninstall, is ignored: the installer only aborts for the other three hooks. A script that must stop an uninstall cannot do so fromuninstall(); usepreflightfor that, because on theuninstallroutepreflightis called first and its failure does abort. -
Everything a script prints is captured in an output buffer and appended to the installer message shown after
postflight(and afteruninstall). PreferenqueueMessage()for messages meant for the administrator.
The route (the $type argument given to preflight and postflight) is one of install, update, discover_install or uninstall. discover_install is used when an extension that is already on disk is registered with the Discover tool or extension:discover:install; the interface documents the values as "install or discover_install, update, uninstall".
The three ways to supply a script
The loader in InstallerAdapter::setupScriptfile() does the following with the file named by <scriptfile>:
-
It resolves the file inside the unpacked package and loads it with
require_once. If the manifest names a file that does not exist, it raises a deprecation notice that "in version 7.0 this will crash". -
If the file returns a service provider, the installer registers it in a child container created just for this extension. If the file returns an object that implements
InstallerScriptInterface, that object is put into the container directly. -
If the container still has no
InstallerScriptInterfaceentry, the installer falls back to the legacy way (see below), triggering the deprecation message "Legacy installer files are deprecated and will be removed in 7.0". -
The script object is fetched from the container. If it has a
setApplication()method it receives the current application, and if it implementsDatabaseAwareInterfaceit receives the database from the container, so theDatabaseAwareTraitworks in a script without any wiring.
So there are three shapes, in order of preference:
| Shape | How it is written | Status |
|---|---|---|
Service provider returned from |
|
Documented approach since Joomla 4.2, the one used in this page. |
Object returned directly |
|
Supported by the loader. Simpler, but the script receives no constructor-injected services. |
Legacy class |
A named class |
Still loaded in 6.2 and wrapped in |
LegacyInstallerScript is the adapter that keeps old scripts alive. It implements InstallerScriptInterface, forwards each call to the old object when the method exists, treats a method that returns nothing (null) as success, injects the database into scripts that implement DatabaseAwareInterface, and proxies __get, __set and __call to the wrapped object. A legacy script that does not implement DatabaseAwareInterface has no $this->db supplied by the installer and must obtain the database itself; do not call the removed Factory::getDbo(). The core component com_admin still ships such a legacy JoomlaInstallerScript class, which shows both that the style works and that core has not yet migrated every script.
The interface and what each method is for
Joomla\CMS\Installer\InstallerScriptInterface has existed since Joomla 4.2.0 and declares five methods. All receive the InstallerAdapter that is running the operation, and the two that bracket the process also receive the route.
<?php
namespace Joomla\CMS\Installer;
interface InstallerScriptInterface
{
public function install(InstallerAdapter $adapter): bool;
public function update(InstallerAdapter $adapter): bool;
public function uninstall(InstallerAdapter $adapter): bool;
public function preflight(string $type, InstallerAdapter $adapter): bool;
public function postflight(string $type, InstallerAdapter $adapter): bool;
}
| Method | Runs | Typical use and effect of false |
|---|---|---|
|
First, before files and database. Called for |
Minimum PHP and Joomla checks, refusing a downgrade, checking that a required extension is installed. Returning |
|
After files and install SQL, on a fresh installation only. |
Seed rows, enable a plugin, create a default category. Returning |
|
After files and pending update SQL, when an existing extension is replaced. |
Data migrations that run after the schema change, clean-up of obsolete settings. Returning |
|
After the uninstall SQL has run, when the extension is removed. |
Remove data the SQL file cannot, such as media files. The return value is ignored. |
|
Last, after the manifest has been copied and the update record cleaned. |
Delete files shipped by earlier releases, show a closing message, enable the extension. The return value is ignored. |
The $adapter gives access to the parsed manifest (getManifest()), the extension name (getName()), the element (getElement()) and the parent Installer (getParent()), which knows the source path (getPath('source')) and the installed paths (getPath('extension_administrator'), getPath('extension_root') and so on).
InstallerScriptTrait (Joomla 6.0)
Before 6.0, a script that wanted the usual checks had to extend a base class that was tied to the pre-4.2 style. Joomla 6.0.0 added Joomla\CMS\Installer\InstallerScriptTrait, which gives a class that implements InstallerScriptInterface the common behaviour without inheritance. It provides default install(), update() and uninstall() methods that return true, plus working preflight() and postflight() methods. Because the trait is new in 6.0.0, a package that must also install on 5.x cannot use it: a 5.x site has no such trait. The trait is described in PHP traits; this section covers only what it does.
| Member | Default | Meaning |
|---|---|---|
|
unset (no check) |
Lowest PHP version, compared with |
|
unset (no check) |
Lowest Joomla version, compared with |
|
|
When |
|
|
Files to delete in |
|
|
Folders to delete recursively in |
|
set in |
The extension name taken from |
What the trait’s methods do:
preflight($type, $adapter)-
Stores the extension name, then calls
checkCompatibility()andcheckDowngrade(). A failed PHP or Joomla check logs the language stringsJLIB_INSTALLER_MINIMUM_PHPorJLIB_INSTALLER_MINIMUM_JOOMLAas a warning and returnsfalse. The downgrade check applies only when$typeisupdate, reads the installed manifest withgetOldManifest()and, if its version is higher than the version in the package, logsJLIB_INSTALLER_ERROR_DOWNGRADEand returnsfalse. Both checks areprotectedand can be overridden. postflight($type, $adapter)-
Calls
removeFiles(), which deletes every entry of$deleteFilesthat is a file and every entry of$deleteFoldersthat is a directory. Each path is run throughPath::check(), which throws for a path that contains..or escapes the root, and a failed deletion is logged and does not abort anything.postflightruns for every route, so a delete list is also applied on a fresh install, where there is simply nothing to delete. getOldManifest(InstallerAdapter $adapter): ?SimpleXMLElement-
Returns the manifest of the currently installed copy (found under the administrator extension path) or
nullwhen no such file exists. Use it to compare the installed<version>with the new one insideupdate(). setApplication()andgetApplication()-
The installer calls
setApplication()on a script that has the method, passingFactory::getApplication(). A script that uses the trait and also receives the application in its constructor, as in the example below, still gets the installer’s call. addDashboardMenuModule(string $dashboard, string $preset)-
Creates an administrator
mod_submenumodule in the positioncpanel-<dashboard>with a menu preset, through thecom_modulesmodel. It tries a translated title from the keyCOM_<EXTENSION>_DASHBOARD_<DASHBOARD>_TITLEand otherwise falls back to a title such as "Tallpine Dashboard". It is meant for components that add their own administrator dashboard (see the administrator tour, dashboards and guided tours).
The trait’s preflight and postflight are declared public, so a class that wants to add its own logic cannot simply redefine them and still call the trait. Importing them under an alias is the documented pattern, and the Manual’s example does exactly that.
A complete service-provider script for com_tallpine
The following script.php sits at the root of pkg_tallpine's component folder (next to com_tallpine.xml). It refuses PHP older than 8.3 and Joomla older than 6.2, refuses a downgrade, refuses a site without the mbstring extension, deletes a layout file and a media folder that an earlier release shipped, reports a fresh install, and on update fills in a default difficulty for trails that have none.
<?php
/**
* @package Tallpine.Administrator
* @subpackage com_tallpine
*/
\defined('_JEXEC') or die;
use Joomla\CMS\Application\AdministratorApplication;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScriptInterface;
use Joomla\CMS\Installer\InstallerScriptTrait;
use Joomla\CMS\Language\Text;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->set(
InstallerScriptInterface::class,
new class (
$container->get(AdministratorApplication::class),
$container->get(DatabaseInterface::class)
) implements InstallerScriptInterface {
use InstallerScriptTrait {
InstallerScriptTrait::preflight as traitPreflight;
InstallerScriptTrait::postflight as traitPostflight;
}
protected string $minimumPhp = '8.3.0';
protected string $minimumJoomla = '6.2.0';
protected bool $allowDowngrades = false;
protected array $deleteFiles = [
'/components/com_tallpine/tmpl/trails/legacy.php',
];
protected array $deleteFolders = [
'/media/com_tallpine/js/legacy',
];
public function __construct(
private AdministratorApplication $app,
private DatabaseInterface $db
) {
}
public function preflight(string $type, InstallerAdapter $adapter): bool
{
if (!$this->traitPreflight($type, $adapter)) {
// The trait has already logged the reason.
return false;
}
if ($type !== 'uninstall' && !extension_loaded('mbstring')) {
$this->app->enqueueMessage(Text::_('COM_TALLPINE_ERROR_MBSTRING'), 'error');
return false;
}
return true;
}
public function install(InstallerAdapter $adapter): bool
{
// The tables already exist: the install SQL has run.
$this->app->enqueueMessage(Text::_('COM_TALLPINE_INSTALLED'));
return true;
}
public function update(InstallerAdapter $adapter): bool
{
$old = $this->getOldManifest($adapter);
// Idempotent: administrators re-run failed updates.
$query = $this->db->createQuery()
->update($this->db->quoteName('#__tallpine_trails'))
->set($this->db->quoteName('difficulty') . ' = ' . $this->db->quote('moderate'))
->where($this->db->quoteName('difficulty') . ' = ' . $this->db->quote(''));
$this->db->setQuery($query)->execute();
$this->app->enqueueMessage(
Text::sprintf('COM_TALLPINE_UPDATED_FROM', (string) ($old?->version ?? '?'))
);
return true;
}
public function uninstall(InstallerAdapter $adapter): bool
{
// Return value is ignored on uninstall; the SQL file has already dropped the tables.
return true;
}
public function postflight(string $type, InstallerAdapter $adapter): bool
{
return $this->traitPostflight($type, $adapter);
}
}
);
}
};
Points worth noticing in the example:
-
The guard is
\defined('_JEXEC') or die;. Joomla 6 no longer definesJPATH_PLATFORMunless the Behaviour - Backward Compatibility 6 plugin is enabled, so a guard on that constant would stop the script on a default Joomla 6 site. -
The classes come from the container of the extension being installed.
AdministratorApplicationandDatabaseInterfaceare registered by the core, so a script never needsFactory::getApplication()orFactory::getDbo(). The script could equally useDatabaseAwareTrait(the installer then callssetDatabase()for you) instead of a constructor argument. -
Building queries with
quoteName()andquote()is acceptable for constants. Anything that comes from a variable goes throughbind(); see the Joomla database API. -
update()is written to be run twice with the same result. A failed update leaves the extension row in the#__extensionstable with the old version, and the administrator runs it again.
The manifest line that activates the script is a single element, a path relative to the zip root:
<?xml version="1.0" encoding="utf-8"?>
<extension type="component" method="upgrade">
<name>com_tallpine</name>
<version>1.2.0</version>
<namespace path="src">Tallpine\Component\Tallpine</namespace>
<scriptfile>script.php</scriptfile>
<install>
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
<file driver="postgresql" charset="utf8">sql/install.postgresql.utf8.sql</file>
</sql>
</install>
<uninstall>
<sql>
<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
<file driver="postgresql" charset="utf8">sql/uninstall.postgresql.utf8.sql</file>
</sql>
</uninstall>
<update>
<schemas>
<schemapath type="mysql">sql/updates/mysql</schemapath>
<schemapath type="postgresql">sql/updates/postgresql</schemapath>
</schemas>
</update>
<files folder="site">
<folder>src</folder>
<folder>tmpl</folder>
</files>
<administration>
<files folder="admin">
<filename>access.xml</filename>
<filename>config.xml</filename>
<folder>services</folder>
<folder>sql</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
</administration>
</extension>
Source: Joomla Manual: Manifest files
The method="upgrade" attribute lets the same package install over an existing copy. The installer copies the script file to the extension’s administrator folder so that it is available on uninstall, when the original package is gone.
The older InstallerScript base class
Joomla 3.6 introduced Joomla\CMS\Installer\InstallerScript as a base class for the legacy style, and it is still present in 6.2. It is not marked deprecated in its docblocks, but its constructor-less, property-based design belongs to the legacy script shape that the loader reports as deprecated, so new code should use the interface and trait. Its useful members are:
| Member | Behaviour |
|---|---|
|
Same meaning as in the trait. They are |
|
Checks the PHP and Joomla minimums, records |
|
Delete lists with the same site-root-relative paths. Unlike the trait, the base class does not call |
|
Moves old-style CLI scripts from the extension into |
|
Helpers that read and write the JSON |
|
Since 4.0.0, creates a |
A legacy script for the same component looks like this. It still works in 6.2, with the deprecation notice, and shows what a migration has to translate.
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScript;
class Com_TallpineInstallerScript extends InstallerScript
{
protected $minimumPhp = '8.3.0';
protected $minimumJoomla = '6.2.0';
protected $deleteFiles = ['/components/com_tallpine/tmpl/trails/legacy.php'];
public function postflight($type, $parent)
{
$this->removeFiles();
return true;
}
}
Source: joomla-cms: InstallerScript
Migrating it to the current shape means: return a service provider from script.php, implement the five interface methods (all five are now mandatory, unlike the legacy script where missing methods were skipped), switch the property declarations to the typed ones of the trait, drop extends, and call the trait’s traitPostflight instead of removeFiles().
Minimum PHP and Joomla checks
There are three places that can state a minimum, and they do different jobs:
-
The script (
$minimumPhp,$minimumJoomla). Runs inpreflight, before anything is copied, and prints a warning that names the required version. This is the strongest check, because it aborts the operation. -
The update server file (
<targetplatform name="joomla" version="6.[2-9]"/>and<php_minimum>). Joomla reads these when it offers an update and hides releases whose platform or PHP minimum does not match the site; they do not stop a manual upload. Detailed in Update servers and the JED.
Use the script check for correctness and the update server entries to avoid offering an update that cannot succeed. The platform rule matters because the core raises its own requirements only in the first release of a major version, so a minimum of 6.2.0 expresses a feature requirement of your extension and not a core policy.
The downgrade check compares the manifest versions with version_compare(), so 1.10.0 is newer than 1.9.0. Keep versions numeric and dotted, or suffix them in a form version_compare() understands (1.2.0-rc1).
SQL: install, uninstall and update files
The SQL declared in the manifest is executed by the installer itself, not by the script. The rules come from Installer::parseSQLFiles() (install and uninstall) and Installer::parseSchemaUpdates() (update).
Install and uninstall files
Each <file> element in <install><sql> or <uninstall><sql> names an SQL file. The installer runs a file only if both of these hold:
-
its
driverattribute matches the site’s database type (mysql, orpostgresql; the aliasesmysqli,pdomysqlandpgsqlare normalised to these), and -
its
charsetattribute isutf8. A<file>that omitscharset="utf8"is silently skipped, and so is its SQL, which is a frequent cause of "the tables were not created".
A file that is named in the manifest but missing from the package makes the installer log JLIB_INSTALLER_ERROR_SQL_FILENOTFOUND and fail. A file that is empty is allowed. The installer splits the text into statements with Installer::splitSql(), which understands quoted strings and comments, and executes them one by one. The prefix #__ is replaced with the site’s table prefix.
-- admin/sql/install.mysql.utf8.sql
CREATE TABLE IF NOT EXISTS `#__tallpine_trails` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`asset_id` int unsigned NOT NULL DEFAULT 0,
`catid` int unsigned NOT NULL DEFAULT 0,
`title` varchar(255) NOT NULL,
`alias` varchar(400) NOT NULL DEFAULT '',
`distance_km` decimal(6,2) NOT NULL DEFAULT 0.00,
`elevation_gain` int unsigned NOT NULL DEFAULT 0,
`difficulty` varchar(20) NOT NULL DEFAULT 'moderate',
`state` tinyint NOT NULL DEFAULT 0,
`access` int unsigned NOT NULL DEFAULT 0,
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_catid` (`catid`),
KEY `idx_state` (`state`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE = utf8mb4_unicode_ci;
-- admin/sql/uninstall.mysql.utf8.sql
DROP TABLE IF EXISTS `#__tallpine_trails`;
Source: Joomla Manual: Manifest files
The query builder does not make raw install SQL portable between databases. A component that supports both MySQL/MariaDB and PostgreSQL ships both sets of files, using each database’s own types (serial, bigint, timestamp without time zone in PostgreSQL) and quoting (double quotes instead of backticks); a component that supports only MySQL and MariaDB declares only the mysql entries. The install file must always describe the newest schema, because a fresh installation runs no update file.
Update files named by version
The <update><schemas><schemapath type="..."> element names a folder per database type. Every *.sql file in it is an update step, and the file name is the version: 1.1.0.sql, 1.2.0.sql, or with a suffix such as 1.2.0-2026-10-30.sql, the form core itself uses (6.2.0-2026-09-15.sql in com_admin). The names are sorted with PHP’s version_compare(), so 1.9.0 precedes 1.10.0.
-- admin/sql/updates/mysql/1.1.0.sql
ALTER TABLE `#__tallpine_trails` ADD COLUMN `difficulty` varchar(20) NOT NULL DEFAULT 'moderate';
-- admin/sql/updates/mysql/1.2.0.sql
ALTER TABLE `#__tallpine_trails` ADD COLUMN `gpx_file` varchar(255) NOT NULL DEFAULT '' /** CAN FAIL **/;
CREATE INDEX `idx_difficulty` ON `#__tallpine_trails` (`difficulty`) /** CAN FAIL **/;
-- admin/sql/updates/postgresql/1.1.0.sql
ALTER TABLE "#__tallpine_trails" ADD COLUMN IF NOT EXISTS "difficulty" varchar(20) NOT NULL DEFAULT 'moderate';
Source: Joomla Manual: Manifest files
The marker /** CAN FAIL **/ placed just before the terminating semicolon of a statement tells the installer to continue if that statement fails. It is how core and extensions make an ALTER or CREATE INDEX safe to re-run on MySQL, which has no ADD COLUMN IF NOT EXISTS in every supported version. The marker must be the last thing before the semicolon.
The #__schemas table
The table #__schemas has two columns, extension_id and version_id, and one row per extension that has an update folder. version_id holds the name (without .sql) of the last update file that ran. It is the only record of where an extension’s database stands. The installer uses it as follows.
-
On a fresh install the installer runs the install file, then calls
setSchemaVersion(), which deletes any existing row and writes the highest update file name. No update file is run, which is why the install file must already contain the final schema. -
On an update it reads
version_idand, for every file newer than it, runs the statements and then writes that file’s name intoversion_id. The row is updated after each file, so a failure in1.2.0.sqlleavesversion_idat1.1.0and the next attempt resumes at1.2.0. -
If no row exists for the extension, the version is taken as
0.0.0and every update file runs. This is the usual explanation when an extension that was copied in with Discover, or whose row was deleted, replays all its migrations on the next update and fails onADD COLUMNfor a column that is already there. -
A statement that fails (without the
CAN FAILmarker) logs the statement and the database error to the update log, shows the message to the administrator, stops the update and aborts the installation. Statements of the same file that ran before the failure stay applied, because DDL in MySQL commits implicitly. -
The
<version>in the manifest andversion_idare independent. Name update files after the manifest versions that introduced the change, so both move together; a release without a schema change needs no file. The one thing to avoid is an update file whose version is lower thanversion_idfor an installed site, which is skipped for ever.
Update files are never edited after a release. Fixing a mistake means a new, higher file.
The database screen in System > Maintenance
Joomla checks extension schemas in the administrator under System > Maintenance > Database. For each extension that has an update folder, com_installer's database model reads the files, finds the highest name and compares it with version_id; a difference is reported as a schema error. It also compares the version recorded in the extension’s manifest_cache with the <version> in the manifest on disk. The Fix button runs fixSchemaVersion(), which deletes the extension’s #__schemas row and inserts the highest file name; it does not run any SQL, so it only repairs the record and not the missing columns. Use it when the database really has the changes and the record is behind, never to "complete" an update that failed half way. The check looks for the update folder at sql/updates/ under the extension’s administrator folder first and only then reads the manifest.
Running and testing the scripts locally
A script and its SQL are best verified on a throw-away site. This Compose file starts Joomla 6.2 with MariaDB; the extension zip is mounted read-only and installed with the console command so the run is repeatable.
services:
db:
image: mariadb:11.4
environment:
MARIADB_DATABASE: joomla
MARIADB_USER: joomla
MARIADB_PASSWORD: CHANGE_ME
MARIADB_ROOT_PASSWORD: CHANGE_ME_ROOT
volumes:
- dbdata:/var/lib/mysql
joomla:
image: joomla:6.2-php8.3-apache
depends_on:
- db
ports:
- "8080:80"
environment:
JOOMLA_DB_HOST: db
JOOMLA_DB_USER: joomla
JOOMLA_DB_PASSWORD: CHANGE_ME
JOOMLA_DB_NAME: joomla
volumes:
- ./dist:/dist:ro
- joomla:/var/www/html
volumes:
dbdata:
joomla:
# Install, then update, from the zip built by your pipeline.
docker compose exec joomla php cli/joomla.php extension:install --path=/dist/pkg_tallpine-1.1.0.zip
docker compose exec joomla php cli/joomla.php extension:install --path=/dist/pkg_tallpine-1.2.0.zip
# What did the installer record?
docker compose exec db mariadb -ujoomla -pCHANGE_ME joomla \
-e "SELECT e.name, s.version_id FROM jos_schemas s JOIN jos_extensions e USING (extension_id)"
Source: Joomla User Guide: Using the CLI
Test three paths: a clean install of the latest zip, an update from the previous release (to exercise the update files and update()), and an uninstall (to prove that nothing is left behind). The image tag and the table prefix (jos_ here) depend on the image you use and on the value chosen in the installer. How to build the zip is explained in Build and CI for extensions, and how to test classes that the script calls in Testing extensions.
Security
An install script runs with the rights of the web server and with the administrator’s session, before the extension’s own access checks exist. Treat it as the most privileged code in the package.
-
Installing is restricted to users who may install extensions; a malicious or compromised package can do anything the script can, which is why extensions are vetted before installation (see Extensions: installing, updating and update sites).
-
Write only inside the extension’s own folders and tables. Never execute strings that come from the archive, a remote URL or the database, and never
includea path built from input. -
$deleteFilesand$deleteFoldersare relative to the site root and joined toJPATH_ROOT. Keep the lists literal and never build them from a variable, so a typo cannot delete a core folder. -
Use
bind()for any value in a query run fromupdate()and escape any text from the database before putting it in anenqueueMessage()call that is rendered as HTML. -
Install SQL contains no credentials, tokens or secrets; configuration that must differ per site belongs in
config.xmlparameters or the environment, never in an SQL seed. -
Do not use the script to open network connections, change
configuration.phpor disable security plugins. Those actions belong to the administrator.
Common errors and pitfalls
-
Tables are not created on install. The
<file>lackscharset="utf8", itsdriverdoes not match the site’s database, or thesqlfolder was not listed in the administrator<files>and is missing from the install. -
An update replays every migration. The
#__schemasrow is missing (extension registered with Discover or copied by hand). Insert it with the highest file that has already been applied. -
"The update stops with a SQL error and the extension is half updated." A statement failed. Fix the cause, add
/** CAN FAIL **/only to statements that are genuinely safe to fail, and ship a new higher update file instead of editing the released one. -
Update files are not found. The folder in
<schemapath>is not copied to the site because it is not in<files>, or the file names contain letters thatversion_compare()orders unexpectedly (1.2.0band1.2.0-bdiffer). -
The script runs, nothing happens. The
<scriptfile>is not at the zip root, the script file returns nothing and defines no legacy class, or the legacy class name does not equal<element>InstallerScriptwith hyphens removed. -
Class "Joomla\CMS\Installer\InstallerScriptTrait" not found. The package is being installed on Joomla 5.x or earlier. Limit the update server’s<targetplatform>accordingly and do not use the trait in a package that must support both generations. -
falsefromuninstall()orpostflight()has no effect. Onlypreflight,installandupdatecan abort. -
dieguard fails on Joomla 6. The guard checksJPATH_PLATFORM. Use\defined('_JEXEC') or die;. -
A message from
preflightnever shows. Output echoed by a hook is buffered by the installer; useenqueueMessage()and returnfalse.
Version notes and corrections to older material
-
InstallerScriptInterfaceand the service-provider style appeared in Joomla 4.2.0.InstallerScriptTraitappeared in 6.0.0. The legacy<element>InstallerScriptclass is loaded in 6.2 with a deprecation notice and is announced for removal in 7.0, which also turns a missing<scriptfile>file into an error. See Deprecations and preparing for Joomla 7. -
Joomla 6.0 no longer defines
JPATH_PLATFORM(unless Behaviour - Backward Compatibility 6 is enabled) and moved the legacyJ*class aliases behind the same plugin, so scripts that callJFactory,JTextorJLogbreak on a default site. -
Older books such as Developing Extensions for Joomla! 5 (2023) show an installer script as a plain class that extends nothing. That works only through the legacy loader. The documented approach is
InstallerScriptInterfacewithInstallerScriptTrait(6.0), optionally returned from a service provider. Material for Joomla 3 shows scripts that callJFactory::getDbo(),JText::_()andJPATH_PLATFORM; none of that is available on Joomla 6 by default. -
The Manual page Install process and script files does not list the values of
$type, which areinstall,update,discover_installanduninstallas the interface documents, and does not describe the legacy loading path; the source ofInstallerAdapter::setupScriptfile()is the reference for both. -
Older tutorials describe the update files as running "from the installed version to the new manifest version". The installer compares file names with
#__schemasand never reads the manifest version for this. -
Query-builder examples in older books that claim the builder translates install SQL for every database are wrong for installation files: MySQL and PostgreSQL still need their own files.
For the combined list of outdated claims, see Where older books and tutorials are outdated. Class names and the namespaces used by the script’s container are explained in Namespaces, autoloading and dependency injection.