Handling OpenCart Extension Installation and Upgrade Scripts (OCMod and vQmod)

Install an extension on OpenCart, click refresh on the modifications page, and watch nothing happen: no error, no warning, just your change quietly missing from the storefront. Most developers hit this in their first month working with OpenCart extension installation and upgrade, and most of them assume the extension is broken. Usually it isn’t. OpenCart’s modification system works in a very particular way, and once you understand the mechanics, ninety percent of the “why isn’t this working” tickets stop being mysterious.

This piece walks through how OCMod actually rewrites files, where vQmod fits (or doesn’t, on current versions), and the handful of failure patterns that account for almost every support ticket involving OpenCart OCMod or OpenCart vQmod. Real XML, real file paths, from a working extension.

What OpenCart Extension Installation and Upgrade Actually Touches on Disk

An OpenCart extension usually ships three kinds of thing: PHP files that get copied straight into admin/, catalog/, or system/, database changes handled by the extension’s own install method, and an XML file describing edits to core files the extension didn’t ship. That third piece is the modification system, and it’s the part almost nobody reads the source for before they need to debug it.

Here’s a real fragment from a working OpenCart extension development project, a contact-form module that needs to inject a menu entry into the admin sidebar:

<file path="admin/controller/common/column_left.php">
  <operation error="skip">
    <search><![CDATA[$this->load->language('common/column_left');]]></search>
    <add position="before"><![CDATA[
        $this->load->language('extension/module/kbcontactform');
    ]]></add>
  </operation>
</file>

That’s the whole idea: find a line, insert something before or after it. No core file gets touched directly. Once you see it in a real XML file rather than a documentation snippet, the appeal is obvious, dozens of extensions can each add their own line near the same anchor point without any of them physically editing column_left.php.

OpenCart OCMod: How the Refresh Step Turns XML Into Working Code

Click “Refresh” under Extensions > Modifications and OpenCart does the following, in this order, every single time: it deletes everything currently sitting in system/storage/modification/, reads every enabled modification XML file (plus system/modification.xml and any loose .ocmod.xml files dropped straight into system/ for local development), and rebuilds patched copies of the target files from scratch. It writes the result to system/storage/modification/, mirroring the original folder structure exactly, so admin/controller/common/column_left.php ends up as system/storage/modification/admin/controller/common/column_left.php.

The clever part, and the part that trips people up, is what happens on every page load afterward. A function in system/startup.php intercepts every file include and checks whether a patched twin exists in the modification cache:

function modification($filename) {
    if (defined('DIR_CATALOG')) {
        $file = DIR_MODIFICATION . 'admin/' . substr($filename, strlen(DIR_APPLICATION));
    }
    // ... same idea for catalog and system contexts
    if (is_file($file)) {
        return $file;
    }
    return $filename;
}

If the cached file exists, that’s what loads. If it doesn’t, OpenCart silently falls back to the untouched original. The real controller or template file on disk is never edited: everything visible in the storefront or admin panel is coming from that cache directory, invisibly, on every request.

Once that clicks, a lot of confusing behaviour stops being confusing. Edit the actual controller file directly and refresh the modifications page, and your edit gets wiped, because refresh regenerates the cache from the XML and the untouched original, not from whatever you last saved. Edit the cached copy in system/storage/modification/ directly, thinking you’re being clever, and the same thing happens on the next refresh: gone, replaced, no trace. The cache is a build artifact. Treat it like one.

OpenCart Extension Installation Scripts and the Theme Wildcard Problem

Storefront-facing extensions almost always need to touch a theme template, and OpenCart’s default theme, along with any custom theme a store might install, lives under catalog/view/theme/*/template/. The wildcard matters here more than it looks like it should.

<file path="catalog/view/theme/*/template/common/header.twig" error="skip">
  <operation>
    <search><![CDATA[</header>]]></search>
    <add position="after"><![CDATA[
{% if kbcontactform_header is defined and kbcontactform_header %}<div class="container kbcf-position-container">{{ kbcontactform_header }}</div>{% endif %}
    ]]></add>
  </operation>
</file>

That asterisk gets expanded with glob() at refresh time, across every folder that matches, default theme, any purchased theme, any custom child theme a developer built for the client. It’s genuinely convenient: one XML block, and the header injection lands in every theme the store has installed, current and future.

It’s also exactly where a lot of “it works on staging but not on the live site” tickets come from. A store running a heavily customised premium theme might not even have a header.twig at the expected path, some themes restructure the template folder entirely, rename common/header.twig, or split it into partials. The wildcard only touches paths that actually match; anything structured differently is silently skipped. error=”skip” on the operation, not error=”abort”, is doing real work in this example: if the search text isn’t found for whatever reason, that one file just doesn’t get patched, and everything else in the same XML still proceeds.

If a store runs three themes and only one shows the feature, that’s the first thing worth checking, not a bug in the extension, just a template structure that doesn’t match what the search string expects.

OpenCart vQmod Extension Upgrade Process and Why It’s Mostly History Now

Before OCMod existed as a core feature, vQmod was the third-party project that did the equivalent job, XML files under a vqmod/xml/ folder, each one describing edits to search for and inject, compiled at runtime into a vqcache folder rather than through an admin refresh button. It’s the reason a lot of older tutorials and older extensions still reference vQmod syntax even though nobody should be starting a new OpenCart 3.x or 4.x project with it today.

OpenCart absorbed the idea natively starting with 2.x, and OCMod is the result: same underlying concept, search-and-insert against a cached copy, but managed through the admin panel with a visible modifications list, enable/disable toggles, and (critically) a log file instead of vQmod’s separate caching layer. Anyone still maintaining a genuinely old OpenCart install might run into both systems side by side, an ancient extension shipping vQmod XML alongside a newer one using OCMod, and the two don’t talk to each other. If an upgrade script assumes vQmod is present and it isn’t, or the reverse, that’s usually a sign the extension itself hasn’t been touched since the OpenCart 2.x days and needs a proper port, not a patch.

For anything being built or maintained now, OCMod is the only one worth designing around. Reaching for vQmod syntax on a modern OpenCart extension upgrade is asking for compatibility problems nobody’s actively fixing upstream anymore.

Common OpenCart Extension Compatibility Failures During Install and Upgrade

A short list, each one drawn from an actual failure mode in the refresh logic rather than a guess.

Two extensions patch the same line, and one of them loses. OCMod applies modifications in whatever order the enabled list processes them, and each operation searches the file state left behind by the previous one. If Extension A’s search string was itself inserted by Extension B and Extension B gets disabled later, A’s operation stops finding its anchor and silently no-ops (assuming error=”skip”) or aborts the whole file’s changes (under error=”abort”). Nothing crashes. The feature just isn’t there anymore, and the actual cause is two versions back in the install order.

The original file was already hand-edited on the store. This is the single most common support ticket. A merchant, or a previous developer, opened catalog/controller/product/product.php directly and changed something nearby, maybe just reformatted the line, maybe added their own logic above it. The OCMod search string no longer matches character-for-character (OCMod does line-based, case-insensitive matching via stripos, but the target line still has to actually contain the search text), the operation fails to find its anchor, and depending on the error attribute either that one change quietly skips or the whole modification for that file aborts. The fix isn’t to blame the extension. It’s to diff the live file against a clean core copy of the same version and see what actually changed before assuming the XML is wrong.

Cache didn’t rebuild, or rebuilt from stale XML. Refresh wipes system/storage/modification/ entirely and rebuilds from every enabled modification’s stored XML plus loose .ocmod.xml files under system/. If an extension’s install script updates the database row holding its XML but nobody clicks refresh afterward (some install flows do trigger it automatically, plenty don’t), the live cache keeps running the previous version’s edits. A version bump that changed the search string, meanwhile, leaves the old cached file exactly as it was until the next manual refresh. This is why “I upgraded the extension but nothing changed” is so often just a missing refresh click rather than a broken upgrade script.

Someone modified a file inside the modification cache directly. It happens under deadline pressure: a quick fix goes straight into system/storage/modification/catalog/controller/… because it’s faster than finding the real source and re-triggering a build. It works, briefly. The next refresh, for any reason, any extension, wipes that directory and rebuilds it clean from XML plus original source. The quick fix is gone with no warning and no backup, because as far as OpenCart is concerned that directory never held anything worth preserving.

Verify the Log Before Guessing What Went Wrong

Before treating any of the above as a diagnosis rather than a guess, read system/storage/logs/ocmod.log. Every refresh appends a fresh trace: which modification ran, in what order, which file it touched, whether each operation’s search string matched, and the exact line number where it landed a change. A line reading NOT FOUND – OPERATION SKIPPED! under a given file’s block means precisely what it says, the search text for that operation wasn’t present in the file state at that point in the sequence. That single log entry usually settles the argument about whether the extension’s OpenCart extension upgrade scripts are broken or whether something upstream, another extension, a hand-edit, an install order issue, moved the target out from under it.

The admin modifications page (Extensions > Modifications) surfaces this same log inline, with a clear-log button next to it, so there’s rarely a reason to go hunting through the filesystem directly unless admin access itself is the problem.

Building This Correctly: What Actually Holds Up Across Upgrades

A handful of habits separate an OpenCart modification system integration that survives years of core updates from one that breaks on the first client theme change.

Scope every search string as tightly as the surrounding code allows, and prefer error=”skip” over the default so one missing anchor doesn’t take down every other edit in the same file. Use the theme wildcard for anything touching catalog/view/theme/*/, rather than hardcoding default and hoping every store never installs anything else. Version the XML alongside the extension’s own version number, so an upgrade script that changes a search string ships as a deliberate, tested change rather than something that silently stops matching after a core update shifts a line by one character. And never treat the modification cache as a place to make a permanent edit: any fix that matters has to go into the source XML or the extension’s shipped files, because the cache is disposable by design and will be discarded the next time anyone, for any reason, clicks refresh.

Extension development on OpenCart rewards understanding this one mechanism more than almost anything else in the platform. Once the search-cache-fallback pattern is genuinely internalised, most support tickets about a module “not installing properly” resolve in minutes rather than hours, because the actual failure is almost always sitting in that log file, waiting to be read.

Leave a Reply