Skip to main content

Edits made to an installed module

A module on a site is a copy of a package. An update replaces that copy, so every file edited in app/Modules/<Name>/ is temporary by definition: the next release of the module — or the next nightly cms9:auto-update — brings its own files.

ECMS9 will not do that silently. Every package ships a .baseline.json (a sha256 per file, written by the package builder), and the installer compares the tree with it before touching anything:

  • files that differ or were added stop the update with local_changes and are listed by name;
  • the automatic updater skips the module and raises a banner in the admin panel instead of overwriting it;
  • the edits are written down as a patch — see below — so they can be carried into the new version.

That makes an edit survivable, not permanent. The supported ways to change behaviour are further down this page; use them for anything that must outlive an update.

See what this site changed: cms9:module-drift​

php spark cms9:module-drift # every module with a baseline
php spark cms9:module-drift my_module # one module
php spark cms9:module-drift my_module --diff # ... and the edits themselves
php spark cms9:module-drift --all --json # machine-readable

The command only reads: it never writes into a module, and without --diff it makes no network call at all — the answer comes from .baseline.json on disk.

Exit codes are meant for CI and rollout scripts:

codemeaning
0nothing differs from the packages
1at least one module was edited on this site
7usage error (unknown module name)

What --diff can and cannot show​

--diff needs the original bytes of the version installed here. The site does not keep them (.baseline.json holds hashes, not files), so they are fetched from the catalog — and the catalog's download only ever serves the newest package of a module. Two consequences, both stated in the output rather than guessed around:

  • if the site runs an older version than the catalog, the original of that version is not obtainable and the command says so and lists the files without a diff;
  • download stamps last_seen on the licence row of the update server, so --diff (and only --diff) touches the licence. Plain runs never do.

The downloaded package is unpacked into writable/store_drift_tmp/ and removed again when the command ends.

The patch: writable/store_local_changes/​

Both moments an update meets a hand-edited module write writable/store_local_changes/<Studly>-<version>-<date>.patch:

  • the update was refused (local_changes) — from the CLI, from the Store tab, or from an unattended cms9:auto-update run;
  • the update overwrote the edits on purpose (--overwrite-local, or «Update anyway» in the Store tab).

The file is a unified diff with a/ b/ paths relative to the module root:

cd app4/app/Modules/MyModule
patch -p1 --dry-run < /path/to/writable/store_local_changes/MyModule-1.0.4-20260924-011500.patch
# no conflict reported → run it again without --dry-run
# conflicts → *.rej files hold the hunks that need a decision

A header comment in the patch itself repeats those two lines, names the module and the version, and says why it was written.

Where an original cannot be proved — the catalog no longer has that version, or the file is binary — nothing is invented: the whole edited file is copied next to the patch (<same name>/files/… plus a manifest.json) and the patch says which files those are. A patch that guessed at an original would apply cleanly and produce wrong code, which is worse than no patch.

A file deleted locally is named in the patch but never deleted by it: the update restores it, and removing it again is a decision for a human.

Writing the patch is best effort. If it fails, the update still runs (or is still refused) and a warning goes into the log — the patch must never be the reason a site cannot update.

The overwrite keeps two copies

--overwrite-local also moves the whole edited tree to writable/store_backups/<Studly>-<date>/, and cms9:module-rollback restores it. The patch is the portable form of the same thing: the backup is the old module, the patch is only your part of it.

Carrying the edits somewhere they survive​

A patch re-applied after every release is a chore, not a solution. Three supported places to put the change instead:

1. The theme, when the change is presentation​

Templates, markup, CSS and JS of the storefront belong to the theme, and a theme is not part of any package — it survives every core and module update. A module widget that must look different is overridden in the theme, not edited in app/Modules/.

2. A small module of your own, listening to events​

This is the general answer for behaviour. A module of your own never conflicts with an update of someone else's, and CI4 auto-discovery loads its Config/Events.php as soon as the namespace is known:

// app/Modules/MyShopTweaks/Config/Events.php
use CodeIgniter\Events\Events;

Events::on('order_placed', static function (array $payload): void {
// A listener must never break the operation it observes.
if (! module_enabled('my_shop_tweaks')) {
return;
}

try {
service('myTweaks')->exportOrder((int) $payload['order_id']);
} catch (\Throwable $e) {
log_message('error', 'my_shop_tweaks: ' . $e->getMessage());
}
});

Storefront markup is contributed the same way, through render points — a storefront_render_point:<name> listener — instead of editing views.

3. Ask the module for an event​

If the module you need to change fires no event where you need one, the fix is an event (or a registry key) in that module, released with it — not a local edit of its code. That is a small, reviewable change that every site then gets, and it is the difference between a one-line patch in the module and a patch you re-apply forever.

Checklist​

  • Edited a module to get something working? Run cms9:module-drift --diff and keep the output — that is the specification of what has to move.
  • Moving it into a theme or a module of your own? Do it before the next release of the edited module, not after.
  • Cannot move it (no event yet)? Open a task for the event, and expect the update to stop on local_changes until it exists — with the patch waiting in writable/store_local_changes/.
  • Re-applied a patch? Re-run cms9:module-drift afterwards: it should now show exactly the files you meant to keep and nothing else.

See also: Module structure, Render points, Store and updates.