A classic story: a developer fixed a couple of lines directly in catalog/controller/product/product.php, the store started working properly, everyone forgot about it. Half a year later, an engine update or migration to another hosting server, files get overwritten, the fix disappears. No one remembers exactly what was changed, because no trace was left behind. And sometimes it's worse: dozens of fixes pile up, scattered throughout the entire catalog, and you can only distinguish the author's code from the system code by running a diff against a clean build.
That's exactly what OCMOD saves you from. It's the standard modification system for OpenCart, built in starting from version 2.0 (the ideological successor to vQmod, only without a separate installation). The concept is simple: you describe the fix in an XML file according to the principle "find such a line and 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 modifications cache: system/storage/modification/. When the site runs, the autoloader first looks for the file there, and only if the modified copy doesn't exist—it takes the original.
The XML instructions themselves are stored in the database, in the oc_modification table. The Refresh button in the Extensions → Modifications section rereads all active modifications from the database and rebuilds the cache from scratch. Until you click it, your fixes won't be applied.
Minimal modifier
A working example: we add our 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 the 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 in <![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 occurrences within a single line. This is the most important thing to know about OCMOD, and most beginners stumble on it. If you put a three-line code block in search, the modifier silently won't be applied. For multi-line scenarios, there's formally the offset attribute and regular expressions, but both will have caveats below.
Attributes of search:
trim="true"— trims whitespace at the edges before comparison (it's true by default anyway, but useful to know);index— if the searched 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 multiple:index="0,2". This is a classic trap for those transitioning from vQmod, because there the count starts from one. Without index, the fix will apply to all matches, which is sometimes exactly what you need, and sometimes a disaster;
Attributes of add:
position—before,after, orreplace. Default is replace, so if you forgot to specify position, your code won't be added to the found line, it will replace it. Check this first when "somehow" a piece of original code disappeared;offset="2"— offset in lines. For example,position="after" offset="2"will insert code not immediately after the found line, but two lines later. Andposition="replace" offset="2"will replace the found line plus the next two.
But offset has a problem that I wouldn't touch it at all because of. It counts lines blindly, without looking at what's in them. If another modifier inserted its code near your anchor, or a theme or engine update shifted the file by a line or two, offset will silently replace or touch something completely different from what you intended. The error log stays clean: search found its line, the operation is formally successful. Such bugs are caught very hard later.
A more reliable tactic: instead of searching for one line with offset, split the fix into several operations, where each search latches onto a unique meaningful line. That way the modifier either applies exactly where needed, or doesn't apply at all and honestly reports it in the log. The second behavior is much better than "applied in the wrong place". Leave offset for the last resort, when there's simply no stable line nearby to anchor to, and always with a minimal value.
Regular expressions are a separate story. When regex="true", attributes position, trim, and offset don't work: everything is controlled by the expression itself, like in preg_replace. Only limit is available to limit 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. Masks work in path, 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 the files of your theme Promo or any other.
OpenCart 2.x vs 3.x: what changed
The difference is significant, and it's exactly where internet instructions break.
In OpenCart 2.x, through Extensions → Installer you could upload both an archive *.ocmod.zip and a bare file *.ocmod.xml. An archive could contain install.sql with database queries and install.php, which would execute during installation.
In OpenCart 3.x, the installer only accepts *.ocmod.zip. Inside the archive, the modifier must be strictly named install.xml, and module files must be in the upload/ folder (the folder structure mirrors the site structure). And fundamentally: install.php and install.sql no longer execute by default in 3.x. If a module for 3.x requires database changes, it makes them through the install() method of its controller, not through an SQL file in the archive. Old manuals for 2.x are frankly harmful here: a person packs install.sql, uploads it, and the tables don't appear in the database and looks for the problem in the wrong place.
If the admin panel is inaccessible for some reason or the installer is acting up with permissions, nothing stops you from throwing the contents of upload/ manually via FTP, and adding the install.xml itself through a third-party modifications editor. But that's already plan B.
Typical pitfalls
Forgot to click "Refresh." The leader by a wide margin. The modifier is installed, active, and nothing changed on the site. Extensions → Modifications → Refresh, and magic happens.
search doesn't find the line. The second most common reason for a "non-working" module. You're searching for a line from clean OpenCart, but it's already different in the file: the theme has overridden the template, or another modifier previously replaced this fragment with position="replace". OCMOD applies modifications sequentially, and each next one works with the result of the previous ones.
Manual editing of the original had no effect. The mirror situation: you manually edit the system file, but the site ignores the changes. Remember the cache: the site reads a copy from system/storage/modification/, not your edited original. After manual edits to files covered by modifications, the cache must be rebuilt with the same "Refresh" button.
And one more small thing that costs hours: OCMOD doesn't strictly validate attributes. You write possition instead of position, and the XML loads without a word of complaint, it just silently uses the default replace. Before looking for complex reasons, reread the attributes letter by letter.
If the store crashes after a modifier
It happens: a broken modifier replaced the wrong thing, and there's a white page on the site. Don't panic, the originals are intact. Here's what to do: go to the admin panel (it usually survives because the catalog breaks more often), disable the suspicious modifier, click "Refresh." If the admin panel also crashed, 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 it, the answer is often on the surface: search didn't find the line because a neighboring modifier already rewrote it.
For whom OCMOD is not the best choice
For extending controller logic in OpenCart 2.2+ there's an events system (events): subscription to before/after calls without any line searching. Events are more resilient to updates and don't conflict with each other the way text replacements do. A rule from practice: for PHP logic, first see if the task can be solved with an event, and leave OCMOD for templates, language files, and those places where events can't reach. And one more limitation by definition: OCMOD works with engine files, so it can't make edits in the cache files themselves or in third-party scripts outside the OpenCart structure. But events — that's a separate article.
Checklist: writing and installing a modifier
- Check the XML header:
name,code,version,author,linkin place,codeis unique. - All code in
searchandaddwrapped in CDATA. - In
searchonly one line of code. Split multi-line fixes into separate operations with unique anchors,offset— only as a last resort. positionspecified explicitly, even if it's replace.- For theme templates, the path has a 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 searched line exists in the cached file.
- Site crashed — disable the modifier and update the