Реально ли при разработке модификаций для распространенного движка интернет-магазинов 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)
…
// <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> */
…
{# <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> #}
<?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 = "";
?>
<?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);
}
?>
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 *.*
идея интересная но существенный недостаток что в случае с ocmod сразу можно обновить модификации и проверить синтаксис и что модуль работает, а тут все сугубо на плечах программиста. неправильно ввел ocmod правило — фейл, перенес кусок кода и забыл поменять правило — снова фейл, а на тесте все будет работать нормально.
в идеале нужен какойто механизм который сам автоматически определяет положение кода и создает правила search и add
Согласен, надо быть деликатным с определением search, но хорошего способа автоматизировать и этот процесс лично я пока не вижу (о плохом способе напишу внизу), а если и найдется нетривиальный способ, то не факт, что он отработает лучше человека. Здесь еще важно, кто как вообще привык писать модификации, я делал под себя (в хорошем смысле этого выражения :)).
А мне показалось удобным так: написал и отладил модификацию на чистом opencart, получил ocmod, применил его на другой копии opencart-а и проверил, как все работает. Потом часто делать это не придется, только search проверить. Задание search делал один раз, больше менять его не понадобилось, соответственно, многократная доработка модификации шла «наживую» и только иногда проверялась через генерацию ocmod (например, перед публикацией или если появились новые области кода), да и тогда исправления были легкими. Другими словами, мне отлаживать скрипты проще по принципу сохранил скрипт-обновил страницу без промежуточных звеньев вроде переподгрузки модификаций, а с ocmod заморачиваться уже в конце, и то не сильно.
Кстати, было бы интересно, кто как еще придумал делать ocmod.
А вот плохой способ автоматизации определения search (для тех, кто захочет реализовать/дописать мой скрипт):
— Взять последнюю непустую строку в файле перед нашим кодом, если она есть. Если она уникальна в файле, то сократить до минимума, пока она уникальна. Если неуникальна, то посчитать ее индекс.
— Взять первую непустую строку в файле после нашего кода, если она есть. Если она уникальна в файле, то сократить до минимума, пока она уникальна. Если неуникальна, то посчитать ее индекс.
— Если обе уникальны, выбрать короче, если обе неуникальны, выбрать с меньшим индексом, иначе выбрать уникальную. В зависимости от этого генерить search position=«after» или search position=«before» и при необходимости задавать индекс.
— Если нужен search position=«replace», то особым образом комментировать убираемую строку. Регулярка в этой схеме не используется.
В-общем, например, так — первое, что пришло на ум. Кто захочет, может додумать.
Сомнительная элегантность, если честно. Это же просто костыль, который придумали как расширение, при том от безысходности, потому что никаких альтернатив платформа не предлагала. А он потом перекочевал во вторую версию CMS.
С использованием регулярных выражений вставка/замена возможна хоть где.