A classic story: a developer fixed a couple of lines directly in catalog/controller/product/product.php, the store started working as it should, and everyone forgot about it. Six months later, an engine update or migration to another host, files get overwritten, and the fix disappears. Nobody remembers what exactly was changed because no trace was left. And sometimes it gets worse: dozens of fixes scattered throughout the catalog, and you can only tell the author's code from the system code by running a diff with a clean build.
This is exactly what OCMOD saves you from. It's the standard modification system for OpenCart, built-in since version 2.0 (the ideological successor to vQmod, only without separate installation). The principle is simple: you describe the fix in an XML file according to the principle "find such a line, insert such code," and the engine applies it itself without touching the originals.
What happens under the hood
OCMOD never edits system files. Instead, it takes the original, makes changes to its copy, and stores it in the modification cache: system/storage/modification/. When the site runs, the autoloader looks for the file there first, and only if the modified copy doesn't exist does it take the original.
The XML instructions themselves are stored in the database, in the oc_modification table. The Refresh button in the Extensions → Modifications section reads all active modifications from the database and rebuilds the cache from scratch. Until you click it, your fixes won't be applied.
A minimal modifier
A working example: adding your own code to the home page.
<?xml version="1.0" encoding="utf-8"?>
<modification>
<name>My first modifier</name>
<code>my_first_mod</code>
<version>1.0</version>
<author>You</author>
<link>https://example.com</link>
<file path="catalog/controller/common/home.php">
<operation>
<search><![CDATA[$data['column_left'] = $this->load->controller('common/column_left');]]></search>
<add position="after"><![CDATA[
$data['my_variable'] = 'Hello from OCMOD';
]]></add>
</operation>
</file>
</modification>
The header (name, code, version, author, link) is mandatory: without it, the installer won't accept the file. code must be unique, otherwise a new modifier will overwrite the old one with the same code in the database. Sometimes this is used intentionally for updates, but more often it's a surprise when two different modules from the same author suddenly "eat" each other.
Always wrap all code in search and add with <![CDATA[ ]]>. PHP code is full of characters like <, >, &, and without CDATA you'll get invalid XML.
search and add: where the nuances hide
The <search> tag searches for a match within a single line. This is the most important thing to know about OCMOD, and this is exactly where most beginners stumble. You put a three-line code block in search and the modifier silently doesn't apply. For multi-line scenarios, there are formally offset attribute and regular expressions, but there will be caveats about both.
search attributes:
trim="true"— trims whitespace at the edges before comparison (true by default anyway, but useful to know);index— if the search line appears in the file multiple times, limits the fix to specific occurrences. Numbering starts from zero:index="0"— first occurrence,index="2"— third. You can list several:index="0,2". This is a classic trap for those transitioning from vQmod, because there counting starts from one. Without index, the fix applies to all matches, which is sometimes exactly what you need, and sometimes a disaster;
add attributes:
position—before,after, orreplace. The default is replace, so if you forget to specify position, your code won't be added to the found line, but will replace it. Check this first when "somehow" a piece of original code disappeared;offset="2"— line offset. For example,position="after" offset="2"will insert code not immediately after the found line, but after two lines. Andposition="replace" offset="2"will replace the found line plus the next two.
But there's a problem with offset, which is why I wouldn't touch it at all. It counts lines blindly, without looking at what's written in them. If another modifier inserted its code near your anchor, or the theme or engine update shifted the file by a line or two, offset will silently replace or catch something completely different from what you intended. The error log will be clean: search found its line, operation is formally successful. These bugs are caught very slowly later.
A more reliable tactic: instead of searching for one line with offset, split the fix into several operations, where each search hooks onto a unique meaningful line. That way the modifier either applies exactly where it should, or doesn't apply at all and honestly reports this in the log. The second behavior is much better than "applied but in the wrong place." Leave offset for the last resort, when there's simply no stable line nearby for an anchor, and definitely with a minimum value.
Regular expressions are a separate story. When regex="true", the attributes position, trim, and offset don't work: everything is controlled by the expression itself, like in preg_replace. Only limit is available to restrict the number of replacements. And keep in mind an old bug: combining regex with position before/after behaved incorrectly and deleted the found fragment, so with regular expressions it's safe to rely only on the logic of full replacement. Honestly, in nine out of ten cases you don't need regular expressions in OCMOD. Regular search with index covers almost everything and reads much easier.
Separately about paths. In path, masks work, and this saves you when working with themes:
<file path="catalog/view/theme/*/template/product/product.twig">
An asterisk instead of a theme name means "apply to all themes." Without it, a modifier written for default simply won't find files of your Promo theme or any other.
OpenCart 2.x vs 3.x: what changed
The difference is significant, and this is exactly where instructions from the internet break down.
In OpenCart 2.x, through Extensions → Installer, you could upload both an *.ocmod.zip archive and a plain *.ocmod.xml file. The archive could contain install.sql with database queries and install.php, which would be executed during installation.
In OpenCart 3.x, the installer only accepts *.ocmod.zip. Inside the archive, the modifier must be named strictly install.xml, and module files should be in the upload/ folder (the folder structure mirrors the site structure). And importantly: install.php and install.sql in 3.x are no longer executed by default. If a module for version 3 requires database changes, it does them through the install() method of its controller, not through an SQL file in the archive. Old manuals for 2.x are downright harmful here: a person packs install.sql, uploads it, and tables don't appear in the database, and they look for the problem in the wrong place.
If the admin panel is inaccessible for some reason or the installer is being picky about permissions, nothing stops you from throwing the contents of upload/ via FTP manually and adding the install.xml itself through a third-party modification editor. But that's plan B.
Typical pitfalls
Forgot to click "Refresh." The leader by a large margin. The modifier is installed, active, but nothing changed on the site. Extensions → Modifications → Refresh, and a miracle happens.
search can't find the line. The second most common reason for a "non-working" module. You're searching for a line from clean OpenCart, but in the file it's already different: the theme overrode the template, or another modifier already replaced this fragment with position="replace". OCMOD applies modifications sequentially, and each next one works with the result of the previous ones.
Manual edits didn't take effect. A mirror situation: you're manually editing a system file, but the site ignores the changes. Remember about the cache: the site reads the copy from system/storage/modification/, not your edited original. After manual edits to files covered by modifications, the cache needs to be rebuilt with the same "Refresh" button.
And a separate detail that costs hours: OCMOD doesn't validate attributes strictly. Write possition instead of position, and the XML will load without complaint, simply silently using the default replace. Before looking for complex reasons, reread the attributes letter by letter.
If the store went down after a modifier
It happens: a bad modifier replaced the wrong thing, and the site shows a white page. Don't panic, the originals are safe. Here's what to do: get into the admin panel (it usually survives because the catalog breaks more often), disable the suspicious modifier, click "Refresh." If the admin panel went down too, delete the contents of system/storage/modification/ via FTP — the site will immediately start working on clean originals, without any modifications. Then calmly go to the admin panel, find the culprit, and rebuild the cache.
By the way, there's a log in the modifications section for diagnostics: messages fall there about which operations didn't apply and why. Before blaming the module developer, check there, often the answer lies on the surface: search couldn't find the line because a neighboring modifier already rewrote it.
When OCMOD isn't the best choice
For extending controller logic in OpenCart 2.2+, there's an events system (events): subscribe to before/after calls without any line searching. Events are more resistant to updates and don't conflict with each other the way text replacements do. A practical rule: for PHP logic, first check if the task is solved by an event, and leave OCMOD for templates, language files, and places where events can't reach. And one more limitation by definition: OCMOD works with engine files, so it won't make changes in the cache files themselves or in third-party scripts outside the OpenCart structure. But events is a separate article.
Checklist: writing and installing a modifier
- Check the XML header:
name,code,version,author,linkare in place,codeis unique. - All code in
searchandaddis wrapped in CDATA. - In
searchonly one line of code. Split multi-line fixes into separate operations with unique anchors,offsetonly as a last resort. positionis specified explicitly, even if it's replace.- For theme templates, the path contains the mask
theme/*/. - For OC 3.x: archive
*.ocmod.zip, insideinstall.xmlplus folderupload/. Don't count on install.sql. - After installation — Extensions → Modifications → Refresh. Always.
- Changes didn't apply — first check the modifications log, then verify that the search line exists in the cached file.
- Site went down — disable the modifier and refresh the cache, in the worst case clear
system/storage/modification/