Генератор ocmod-файла для интернет-магазина на Opencart +4


AliExpress RU&CIS

Реально ли при разработке модификаций для распространенного движка интернет-магазинов Opencart сосредоточиться на своих алгоритмах, а подготовку файла для подгрузки в эту CMS дать на откуп специальным скриптам? Собственно, это то, что сильно облегчило бы жизнь разработчикам под Opencart, и в данной статье я предлагаю мой вариант решения.

Немного вступления:

Формат ocmod — довольно элегантное решение для определения модификаций исходных файлов, причем независимо от их формата. Одна из частей формата – это XML-файл, в котором прописано, в каком файле и в каком месте этого файла необходимо внести определенные изменения. Вот пример ocmod-файла (взято с ocmod.net, более подробное его описание можно взять там же):

<?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>

В общих чертах задаем следующее:

<file path="в каком файле">
        <operation>
            <search><![CDATA[что ищем]]></search>
            <add position="где – перед, после или вместо">
                <![CDATA[что вставляем или чем замещаем]]>
            </add>
        </operation>
    </file>

Хотя принцип довольно прозрачен, встает вопрос: можно ли автоматизировать процесс его создания или придется писать его вручную, ведь программист должен заниматься программированием, а не онанирглупой рутинной деятельностью.

В идеале написание модификации под Opencart могло бы выглядеть так: мы скачали «непорочную» версию магазина, прямо в его исходниках внесли кое-какие изменения и запустили «волшебный» скрипт, который прямо на месте нам сгенерировал весь ocmod. На деле все немного сложнее, однако мы постараемся приблизиться к этой схеме. Основная проблема – это определение местоположения в файле для вставки (то, что между <search>…</search>). Это должен сделать программист. Как правило, его стараются сделать максимально универсальным, чтобы охватить больше потенциальных версий исходников, и при этом чтобы менялось именно там, где надо. Это явно ручная работа. Все остальное автоматизируемо.

Небольшое отступление: поиск происходит в строке целиком, и вставка возможна только перед, после или вместо нее, но никак не внутри (в классической поставке OCMOD для Opencart). Это непонятное лично для меня ограничение. Также мне не понятно, почему нельзя задать несколько тегов <search> для поиска нужного места вставки, которые бы последовательно отрабатывались – ведь поиск был бы намного более гибок. Например, если в коде PHP, то, скажем, найти имя функции, потом найти в ней нужное место или еще как-нибудь на усмотрение программиста. Но этого я не нашел, если ошибаюсь, пожалуйста, поправьте.

А теперь самое главное: автоматизировать процесс создания ocmod-файла можно, при этом надо просто придерживаться нужной схемы. Во первых, в исходном файле нам необходимо как-то обозначить место наших изменений – и просто для порядка, и чтобы наш ocmod-генератор адресно все знал. Допустим, нас зовут Петр Николаевич Иванов (совпадения случайны). Давайте все наши изменения заключать между тегами <PNI>…</PNI>, а чтобы теги не портили исходник, будем помещать эти теги в комментариях того языка, над файлом которого мы в данный момент работаем. Между тегами прямо на месте зададим строку поиска между <search></search> и добавляемый код между <add></add>. Будет понятнее на примере:

Для изменений в PHP:

…
(здесь исходный код opencart)
…
// <PNI>
// а вот здесь уже добавляемый нами один из кусков кода нашей модификации -
// мы должны задать и место поиска, и сам код (все в этом комментарии)
// <search> public function index() {</search>
// <add position=”after”>
$x = 5;
$y = 6;
//</add> </PNI>
…

Или так:

…
(здесь исходный код opencart)
…
/* <PNI>
     <search> public function index() {</search>
     <add position=”after”> */
$x = 5;
$y = 6;
/*</add> </PNI> */
…

Если у <search> или <add> присутствуют какие-то атрибуты, например, <search index=”1”>, то они «как есть» будут перенесены в наш ocmod-файл. То, что мы пишем между ними, не требует какого-либо XML-экранирования, мы просто пишем строку поиска и код.

Еще пример, уже для изменяемого нами twig-файла:

            {# <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> #}

После того, как мы оформили подобным образом все наши изменения, нам понадобится скрипт, который все это обработает, а также архиватор. Делюсь с вами своим вариантом: он состоит из файла конфигурации и собственно скрипта.

Файл конфигурации make-ocmod.opencart.local.cfg.php (кодировка UTF-8, это пример, каждый делает под себя):

<?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 = "";

?>

Теперь главное – генератор ocmod xml-файла.
Скрипт make-ocmod.php (кодировка UTF-8):

<?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);
}

?>

Командник make-ocmod.cmd, который все это запускает:

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 *.*

Я использую 7zip, поэтому 7z.exe должен лежать там же, где наш командник. Кто захочет использовать его же, сможет скачать его по адресу https://www.7-zip.org/.

Это командник для Windows. Кто под Linux, думаю, перепишет без проблем.

Резюме: На мой взгляд, так работать намного проще, чем каждый раз вручную править ocmod. Когда мы добавляем код, то один раз прямо на месте задаем наши теги поиска для этого куска кода, а далее сосредотачиваемся только на нашей работе. Нас уже не заботит структура xml-файла, а любое исправление нашей модификации мы вносим на месте, тут же проверяем его работу и далее одним нажатием генерируем новый ocmod-файл.

Теги:




Комментарии (4):

  1. kabantejay
    /#22335400

    идея интересная но существенный недостаток что в случае с ocmod сразу можно обновить модификации и проверить синтаксис и что модуль работает, а тут все сугубо на плечах программиста. неправильно ввел ocmod правило — фейл, перенес кусок кода и забыл поменять правило — снова фейл, а на тесте все будет работать нормально.
    в идеале нужен какойто механизм который сам автоматически определяет положение кода и создает правила search и add

    • abnorm
      /#22335896

      Согласен, надо быть деликатным с определением search, но хорошего способа автоматизировать и этот процесс лично я пока не вижу (о плохом способе напишу внизу), а если и найдется нетривиальный способ, то не факт, что он отработает лучше человека. Здесь еще важно, кто как вообще привык писать модификации, я делал под себя (в хорошем смысле этого выражения :)).
      А мне показалось удобным так: написал и отладил модификацию на чистом opencart, получил ocmod, применил его на другой копии opencart-а и проверил, как все работает. Потом часто делать это не придется, только search проверить. Задание search делал один раз, больше менять его не понадобилось, соответственно, многократная доработка модификации шла «наживую» и только иногда проверялась через генерацию ocmod (например, перед публикацией или если появились новые области кода), да и тогда исправления были легкими. Другими словами, мне отлаживать скрипты проще по принципу сохранил скрипт-обновил страницу без промежуточных звеньев вроде переподгрузки модификаций, а с ocmod заморачиваться уже в конце, и то не сильно.
      Кстати, было бы интересно, кто как еще придумал делать ocmod.

      А вот плохой способ автоматизации определения search (для тех, кто захочет реализовать/дописать мой скрипт):
      — Взять последнюю непустую строку в файле перед нашим кодом, если она есть. Если она уникальна в файле, то сократить до минимума, пока она уникальна. Если неуникальна, то посчитать ее индекс.
      — Взять первую непустую строку в файле после нашего кода, если она есть. Если она уникальна в файле, то сократить до минимума, пока она уникальна. Если неуникальна, то посчитать ее индекс.
      — Если обе уникальны, выбрать короче, если обе неуникальны, выбрать с меньшим индексом, иначе выбрать уникальную. В зависимости от этого генерить search position=«after» или search position=«before» и при необходимости задавать индекс.
      — Если нужен search position=«replace», то особым образом комментировать убираемую строку. Регулярка в этой схеме не используется.
      В-общем, например, так — первое, что пришло на ум. Кто захочет, может додумать.

  2. iyzoer
    /#22337114

    Формат ocmod — довольно элегантное решение для определения модификаций исходных файлов, причем независимо от их формата.

    Сомнительная элегантность, если честно. Это же просто костыль, который придумали как расширение, при том от безысходности, потому что никаких альтернатив платформа не предлагала. А он потом перекочевал во вторую версию CMS.

  3. /#22339636

    Небольшое отступление: поиск происходит в строке целиком, и вставка возможна только перед, после или вместо нее, но никак не внутри (в классической поставке OCMOD для Opencart).

    С использованием регулярных выражений вставка/замена возможна хоть где.