Ocmod file generator for an online store on Opencart

Is it realistic to focus on your algorithms when developing modifications for the popular Opencart online store engine, and leave the preparation of a file for uploading to this CMS at the mercy of special scripts? Actually, this is what would make life much easier for developers under Opencart, and in this article I offer my solution.



A little introduction:



The ocmod format is a pretty elegant solution for defining modifications to source files, regardless of their format. One of the parts of the format is an XML file, in which it is written in which file and where in this file certain changes must be made. Here is an example of an ocmod file (taken from ocmod.net, a more detailed description can be found there):



<?xml version="1.0" encoding="utf-8"?>
<modification>
    <name>   </name>
    <code>product-page-views</code>
    <version>1.0</version>
    <author>https://ocmod.net</author>
    <link>https://ocmod.net</link>
    <file path="catalog/controller/product/product.php">
        <operation>
            <search>
                <![CDATA[$data['images'] = array();]]>
            </search>
            <add position="after">
                <![CDATA[
                	$data['view'] = $product_info['viewed'];
                ]]>
            </add>
        </operation>
    </file>
    <file path="catalog/language/en-gb/product/product.php">
        <operation>
            <search>
                <![CDATA[$_['text_search']]]>
            </search>
            <add position="before">
                <![CDATA[
                	$_['text_view']              = 'View: ';
                ]]>
            </add>
        </operation>
    </file>
</modification>


In general terms, we set the following:



<file path="  ">
        <operation>
            <search><![CDATA[ ]]></search>
            <add position=" – ,   ">
                <![CDATA[    ]]>
            </add>
        </operation>
    </file>


Although the principle is quite transparent, the question arises: is it possible to automate the process of its creation or will it be necessary to write it by hand, because the programmer should be engaged in programming, and not masturbate with silly routine activities.



Ideally, writing a modification for Opencart would look like this: we downloaded the "immaculate" version of the store, made some changes right in its source code and ran the "magic" script that generated the entire ocmod right on the spot. In fact, everything is a little more complicated, but we will try to get closer to this scheme. The main problem is determining the location in the file to insert (what is between <search> ... </search>). This must be done by the programmer. As a rule, they try to make it as universal as possible in order to cover more potential versions of the source, and at the same time so that it changes exactly where it is needed. This is clearly handmade. Everything else is automated.



A small digression: the search occurs in the entire string, and insertion is possible only before, after or instead of it, but not inside (in the classic OCMOD package for Opencart). This is a limitation that is incomprehensible to me personally. Also, I don't understand why it is impossible to set several <search> tags to find the desired insertion point, which would be consistently processed - after all, the search would be much more flexible. For example, if in the PHP code, then, say, find the name of the function, then find the right place in it, or in some other way at the discretion of the programmer. But I did not find this, if I am mistaken, please correct it.



And now the most important thing: you can automate the process of creating an ocmod file, and you just need to adhere to the desired scheme. First, in the source file, we need to somehow indicate the place of our changes - both just for order, and so that our ocmod generator knows everything addressably. Let's say our name is Pyotr Nikolaevich Ivanov (coincidences are random). Let's enclose all our changes between the <PNI> ... </PNI> tags, and so that the tags do not spoil the source, we will place these tags in the comments of the language we are currently working on. Between the tags, right in place, we set the search string between <search> </search> and the added code between <add> </add>. It will be clearer with an example:



For changes in PHP:



…
(   opencart)
…
// <PNI>
//             -
//      ,    (   )
// <search> public function index() {</search>
// <add position=”after”>
$x = 5;
$y = 6;
//</add> </PNI>
…


Or like this:



…
(   opencart)
…
/* <PNI>
     <search> public function index() {</search>
     <add position=”after”> */
$x = 5;
$y = 6;
/*</add> </PNI> */
…


If <search> or <add> has any attributes, for example, <search index = ”1”>, then they will be transferred β€œas is” to our ocmod file. What we write in between doesn't require any XML escaping, we just write the search string and code.



Another example, already for the twig file we are modifying:



            {# <PNI>
            <search><li><span style="text-decoration: line-through;">{{ price }}</span></li></search>
            <add position="replace">
            #}
            <li><span class="combination-base-price" style="text-decoration: line-through;">{{ price }}</span></li>
            {# </add></PNI> #}


After we have formalized all our changes in this way, we need a script that will handle all this, as well as an archiver. I share with you my version: it consists of a configuration file and a script itself.



The configuration file make-ocmod.opencart.local.cfg.php (UTF-8 encoding, this is an example, everyone does it for themselves):



<?php

define("ROOT_PATH", "../../opencart.local");

define("ENCODING", "utf-8");
define("NAME", " ocmod");
define("CODE", "product-page-views");
define("VERSION", "1.0");
define("AUTHOR", "AS");
define("LINK", "");

define("TAG_OPERATION_BEGIN", "<PNI>");
define("TAG_OPERATION_END", "</PNI>");
define("TAG_SEARCH_BEGIN", "<search"); // !!  >
define("TAG_SEARCH_END", "</search>");
define("TAG_ADD_BEGIN", "<add"); // !!  >
define("TAG_ADD_END", "</add>");

//     </add>      
// ( ,   , ).
//    ,   , 
//   </add> ( , \t, \r, \n  ,  )
$commentsBegin = [ '//', '/*', '<!--', '{#' ];
//     <add>      
// ( ,   , ).
//    ,   , 
//   <add> ( , \t, \r, \n  ,  )
$commentsEnd = [ '*/', '-->', '#}' ];

//       ,     
//  .
$exclude = [ '/cache/', '/cache-/' ];

//      upload.
//     ,   .
$upload = [
  'admin/view/stylesheet/combined-options.css',
  'admin/view/javascript/combined-options.js',
  'catalog/view/theme/default/stylesheet/combined-options.css',
  'admin/view/image/combined-options/cross.png',
  'catalog/view/javascript/combined-options/combined.js',
  'catalog/view/javascript/combined-options/aswmultiselect.js',
  'admin/view/image/combined-options/select.png'
];

//     install.sql.
// (   Opencart  )
$sql = "";

?>


Now the main thing is the ocmod xml file generator.

Make-ocmod.php script (UTF-8 encoding):



<?php

include_once ($argv[1]);

function processFile($fileName, $relativePath) {
  global $commentsBegin, $commentsEnd, $xml, $exclude;

  if ($exclude)
    foreach ($exclude as $ex)
      if (false !== strpos($relativePath, $ex))
        return;

  $text = file_get_contents($fileName);
  $end = -1;
  while (false !== ($begin = strpos($text, TAG_OPERATION_BEGIN, $end + 1))) {
    $end = strpos($text, TAG_OPERATION_END, $begin + 1);
    if (false === $end)
      die ("No close operation tag in ".$fileName);
    $search = false;
    $searchEnd = $begin;
    while (false !== ($searchBegin = strpos($text, TAG_SEARCH_BEGIN, $searchEnd + 1)) and $searchBegin < $end) {
      $searchBeginR = strpos($text, '>', $searchBegin + 1);
      $searchAttributes = substr($text, $searchBegin + strlen(TAG_SEARCH_BEGIN), $searchBeginR - $searchBegin - strlen(TAG_SEARCH_BEGIN));
      if (false === $searchBeginR or $searchBeginR >= $end)
        die ("Invalid search tag in ".$fileName);
      $searchEnd = strpos($text, TAG_SEARCH_END, $searchBeginR + 1);
      if (false === $searchEnd or $searchEnd >= $end)
        die ("No close search tag in ".$fileName);
      //  
      $search = substr($text, $searchBeginR + 1, $searchEnd - $searchBeginR - 1);
    }
    $addBegin = strpos($text, TAG_ADD_BEGIN, $begin + 1);
    if (false === $addBegin or $addBegin >= $end)
      die ("No begin add tag in ".$fileName);
    $addBeginR = strpos($text, '>', $addBegin + 1);
    $addAttributes = substr($text, $addBegin + strlen(TAG_ADD_BEGIN), $addBeginR - $addBegin - strlen(TAG_ADD_BEGIN));
    if (false === $addBeginR or $addBeginR >= $end)
      die ("Invalid add tag in ".$fileName);
    $addEnd = strpos($text, TAG_ADD_END, $addBeginR + 1);
    if (false === $addEnd or $addEnd >= $end)
      die ("No close add tag in ".$fileName);
    $codeBegin = $addBeginR + 1;
    $codeEnd = $addEnd;
    //       ,
    //    - .        .
    $p = $codeBegin;
    while (@$text[$p] === " " or @$text[$p] === "\t" or @$text[$p] === "\r" or @$text[$p] === "\n")
      $p ++;
    if ($p < $addEnd) {
      foreach ($commentsEnd as $tag)
        if (substr($text, $p, strlen($tag)) === $tag)
          $codeBegin = $p + strlen($tag);
    }
    $p = $codeEnd - 1;
    while (@$text[$p] === " " or @$text[$p] === "\t" or @$text[$p] === "\r" or @$text[$p] === "\n")
      $p --;
    if ($p >= $codeBegin) {
      foreach ($commentsBegin as $tag)
        if (substr($text, $p - strlen($tag) + 1, strlen($tag)) === $tag)
          $codeEnd = $p - strlen($tag) + 1;
    }
    $code = substr($text, $codeBegin, $codeEnd - $codeBegin - 1);

    $xml .= "
    <file path=\"".str_replace('"', '\"', $relativePath)."\">
        <operation>".(false !== $search ? "
            <search{$searchAttributes}>
                <![CDATA[{$search}]]>
            </search>" : "")."
            <add{$addAttributes}>
                <![CDATA[{$code}]]>
            </add>
        </operation>
    </file>";
  }
}

function processDir($dir, $relativePath = '') {
  global $exclude;

  $cdir = scandir($dir);
  foreach ($cdir as $key => $value) {
    if (!in_array($value,array(".", ".."))) {
      $fileName = $dir . DIRECTORY_SEPARATOR . $value;
      $newRelativePath = ($relativePath ? $relativePath.'/' : '').$value;
      $excluded = false;
      if ($exclude)
        foreach ($exclude as $ex)
          $excluded = $excluded or false !== strpos($newRelativePath, $ex);
      if ($excluded)
        continue;
      if (is_dir($fileName)) {
        processDir($fileName, $newRelativePath);
      } else {
        processFile($fileName, $newRelativePath);
      }
    }
  }
}

function delTree($dir, $delRoot = false) {
  $files = array_diff(scandir($dir), array('.','..'));
  foreach ($files as $file) {
    (is_dir("$dir/$file")) ? delTree("$dir/$file", true) : unlink("$dir/$file");
  }
  return $delRoot ? rmdir($dir) : true;
}

$xml = "<?xml version=\"1.0\" encoding=\"".ENCODING."\"?>
<modification>
    <name>".NAME."</name>
    <code>".CODE."</code>
    <version>".VERSION."</version>
    <author>".AUTHOR."</author>
    <link>".LINK."</link>";

processDir(ROOT_PATH);

$xml .= "
</modification>";

file_put_contents('publish/install.xml', $xml);
file_put_contents('publish/install.sql', $sql);

delTree('publish/upload');
foreach ($upload as $file) {
  $srcfile = ROOT_PATH.(@$file[0] === '/' ? '' : '/').$file;
  $dstfile = 'publish/upload'.(@$file[0] === '/' ? '' : '/').$file;
  mkdir(dirname($dstfile), 0777, true);
  copy($srcfile, $dstfile);
}

?>


The make-ocmod.cmd command line that runs all of this:



del /f/q/s publish.ocmod.zip
php make-ocmod.php make-ocmod.opencart.local.cfg.php
cd publish
..\7z.exe a -r -tZip ..\publish.ocmod.zip *.*


I use 7zip, so 7z.exe should be in the same place as our command line. Whoever wants to use it can download it from https://www.7-zip.org/ .



This is a command manager for Windows. Who on Linux, I think, will rewrite without problems.



Summary: In my opinion, it is much easier to work this way than to manually edit ocmod every time. When we add code, we set our search tags for this piece of code right on the spot, and then we focus only on our work. We no longer care about the structure of the xml file, and we make any correction of our modification on the spot, immediately check its work and then generate a new ocmod file with one click.



All Articles