Русский
Русский
English
Статистика
Реклама

Opencart

Стоит ли платить за плагины OpenCart?

18.11.2020 16:23:31 | Автор: admin

У нас обновление в маркетплейсе: теперь вы можете заказать с предустановленным OpenCart.

Вместо того, чтобы рассказывать, что такое OpenCart, давайте лучше порассуждаем о расширениях: в каком случае стоит покупать готовое, как их проверять?

Мы подготовили краткий список вопросов, которые стоит себе задать, прежде чем вынимать кровные шекели на готовый плагин.

На что смотреть, принимая решение о покупке расширения?


Качество кода


Первое, на что надо смотреть перед принятием решения качество кода в расширении, которое вы собираетесь покупать. Иначе можно легко включить кусок мусора прямо в своей проект.

Хороший плагин должен быть написан в соотвествии со , а также быть аккуратным и чистым, чтобы не грузить сервер. Еще один признак качественного плагина подробная документация про установку и использования.

Такие плагины не обязательно платные, среди хорошо написанных расширений есть и много бесплатных поэтому ищите чуть дольше, найдите несколько вариантов и делайте нагрузочные тестирования.

Если стоит выбор между качественный платным и некачественным, но бесплатным, лучше все же заплатить. Так что переходим к цене.

Стоимость


Иногда кажется, что сэкономив на дешевом или бесплатном плагине, вы удешевляете проект для заказчика.

Это может быть ложным ощущением, если доработка расширения или самостоятельное написание функционала выйдет клиенту дороже (в ваших оплачиваемых часах, как специалиста), чем просто купить готовый код.

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

Возможность использования плагина в будущем


Прежде чем покупать расширение, подумайте:

  • Можно ли его использовать в других проектах повторно?
  • Идет ли он в связке с каким-то другим плагином или может работать солоо?
  • Обновляется ли плагин регулярно?


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

Многие бесплатные расширения хорошо поддерживаются, а многие платные нет.

Есть разные схемы, кто платит за платное расширение: заказчик или разработчик. Чаще всего, если разработчик использует какие-то расширениям постоянно и во многих проектах, то платит он сам, а за специфичные расширения конкретно под свой сайт уже покупает клиент. Объяснить затраты клиенту просто: сообщаете, что написать такой плагин с нуля будет куда дороже, чем купить готовый и клиент, обычно, соглашается.

Но вернемся к OpenCart.

Как мы создавали образ для OpenCart в нашем маркетплейсе




Готовый образ в нашем

Требования к серверу


Для использования Opencart 3 рекомендуется использовать 2 Гб RAM и 2 ядра CPU.

Основные файлы Opencart занимают около 50 Мб, дополнительно вам понадобится место для хранения базы данных, резервных копий, и данных сайта, которое будет зависить от объема опубликованной информации.

Opencart может использовать Apache или Nginx с PHP 7.3+, а в качестве базы данных MySQL.
Мы будем создавать образ с использованием Nginx и MySQL.

Подготовка образа


Обновим установленные пакеты до последней версии:

dnf update -y

Добавим постоянное разрешение для входящего трафика на http/80 и https/443 порты:

firewall-cmd --permanent --add-service=httpfirewall-cmd --permanent --add-service=https

Применим новые правила файрвола:

systemctl reload firewalld

Установим Nginx:

dnf install nginx -y

Запустим и включим сервер Nginx:

systemctl start nginxsystemctl enable nginx

Так как на данный момент в основном репозитории Centos используется версия PHP 7.2, добавим репозиторий REMI с PHP 7.4.
Для этого добавим репозиторий EPEL (требуется репозиторием REMI):

rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm

Добавим репозиторий REMI:

sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm

Включим модуль php:remi-7.4 для установки php 7.4:

sudo dnf module enable php:remi-7.4 -y

Установим php-fpm и php-cli:

sudo dnf install -y php-fpm php-cli

Установим модули PHP требуемые для Opencart:

dnf install php-mysqlnd php-date php-dom php-filter php-gd php-hash php-json php-pcre php-pdo php-session php-simplexml php-spl php-tokenizer php-xml php-zip -y

Так же установим модули PHP mbstring, opcache:

dnf install php-mbstring php-opcache -y

Установим сервер MySQL:

dnf install mysql-server -y

Включим и запустим сервер MySQL:

systemctl start mysqldsystemctl enable mysqld

Так как мы делаем шаблон для VDS, а они могут быть медленными, добавим задержку старта mysqld 30 секунд, иначе могут быть проблемы со стартом сервера при первоначальной загрузке системы:

sudo sed -i '/Group=mysql/a \ExecStartPre=/bin/sleep 30' /usr/lib/systemd/system/mysqld.service

Изменим группу и пользователя из под которого будет работать nginx внеся изменения в /etc/php-fpm.d/www.conf:

sudo sed -i --follow-symlinks 's/user = apache/user = nginx/g' /etc/php-fpm.d/www.confsudo sed -i --follow-symlinks 's/group = apache/group = nginx/g' /etc/php-fpm.d/www.conf

Изменим владельца каталога сессий PHP так же соответственно на nginx:

sudo chown -R nginx. /var/lib/php/session

Удалим строки с коментариями из файла конфигурации /etc/nginx/nginx.conf (что бы не было двойных срабатываний для sed):

sudo sed -i -e '/^[ \t]*#/d'  /etc/nginx/nginx.conf

Добавим в /etc/nginx/nginx.conf настройки компрессии gzip

sudo sed -i '/types_hash_max_size 2048;/a \\    gzip on;\    gzip_static on;\    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/x-icon image/svg+xml application/x-font-ttf;\    gzip_comp_level 9;\    gzip_proxied any;\    gzip_min_length 1000;\    gzip_disable "msie6";\    gzip_vary on; \' /etc/nginx/nginx.conf


Добавим в /etc/nginx/nginx.conf настройки индексного файла index.php:

sudo sed -i '/        root         \/usr\/share\/nginx\/html;/a \        index index.php index.html index.htm;\' /etc/nginx/nginx.conf

Добавим настройки для дефолтного сервера обработку php через сокет php-fpm, отключим лог для статических файлов, увеличим время expire, отключим лог доступа и ошибок для favicon.ico и robots.txt и запретим доступ к файлам .ht для всех:

sudo sed -i '/        location \/ {/a \try_files $uri $uri/ /index.php?q=$uri&$args;\        }\    \        location ~* ^.+.(js|css|png|jpg|jpeg|gif|ico|woff)$ {\        access_log off;\        expires max;\        }\    \        location ~ \.php$ {\        try_files  $uri =404;\        fastcgi_pass   unix:/run/php-fpm/www.sock;\        fastcgi_index index.php;\        include fastcgi_params;\        fastcgi_intercept_errors on;\        fastcgi_ignore_client_abort off;\        fastcgi_connect_timeout 60;\        fastcgi_send_timeout 180;\        fastcgi_read_timeout 180;\        fastcgi_buffer_size 128k;\        fastcgi_buffers 4 256k;\        fastcgi_busy_buffers_size 256k;\        fastcgi_temp_file_write_size 256k;\        }\    \        location = /favicon.ico {\        log_not_found off;\        access_log off;\        }\    \        location = /robots.txt {\        allow all;\        log_not_found off;\        access_log off;\        }\    \        location ~ /\.ht {\        deny all;' /etc/nginx/nginx.conf

Установим wget требуемый для установки certbot:

sudo dnf install wget -y

Скачаем исполняемый файл certbot с оффсайта:

cd ~wget https://dl.eff.org/certbot-auto

Переместим certbot в /usr/local/bin/:

mv certbot-auto /usr/local/bin/certbot-auto

И назначим права и владельцем root:

chown root /usr/local/bin/certbot-autochmod 0755 /usr/local/bin/certbot-auto

Установим зависимости certbot, ответ Y в конвеер на вопрос установки зависимостей, и --install-only, что бы не инициировать установку сертификатов на данном этапе:

yes | certbot-auto --install-only

Удалим файл в каталоге веб-сервера по умолчанию:

rm -f /usr/share/nginx/html/index.html

Скачаем архив с установочными файлами Opencart:

cd ~wget https://opencart.ru/opencart-3.0.3.2-ru.zip

Распакуем архив в настроенный каталог веб-сервера:

unzip opencart-3.0.3.2-ru.zip "upload-3032-ru/*" -d /usr/share/nginx/html/

Переместим файлы из подкаталога upload-3032-ru в корневой каталог:

mv /usr/share/nginx/html/upload-3032-ru/* /usr/share/nginx/html/

И удалим подкаталог upload-3032-ru:

rm -rf /usr/share/nginx/html/upload-3032-ru/

Так же удалим скачанный архив:

rm -f ~/opencart-3.0.3.2-ru.zip

Создим пустые файлы конфигурации:

touch /usr/share/nginx/html/config.phptouch /usr/share/nginx/html/admin/config.php

Удалим строку $db->query(SET @@session.sql_mode = 'MYSQL40'); из install.php, т.к. данный режим не поддерживается в MySQL 8:

sed -i -e '/MYSQL40/d' /usr/share/nginx/html/install/model/install/install.php

Назначим владельцем файлов nginx выше уровнем, т.к. opencart потребуются соответсвующие разрешения для автоматического переноса хранилища за пределы каталога веб-сервера:

chown -R nginx. /usr/share/nginx

Создадим базу данных opencart:

mysql -uroot -e "CREATE DATABASE opencart;"

Создадим пользователя mysql opencart с пустым паролем:

mysql -uroot -e "CREATE USER 'opencart'@'localhost' IDENTIFIED BY '';"

Дадим все привелегии пользователю opencart на базу opencart:

mysql -uroot -e "GRANT ALL PRIVILEGES ON opencart.* TO 'opencart'@'localhost';"

Перезапустим и включим PHP-FPM:

systemctl restart nginx php-fpmsystemctl enable php-fpm

Далее создадим скрипт в домашнем каталоге root который по запросу пользователя сгенерирует и установит пароли для root и zabbix и скорректирует их в файлах конфигурации Opencart.
Воспользуемся перенаправлением heredoc с делимитером POSTINSTALL, что бы записать скрипт как есть, без раскрытия специальных символов:

cat <<"POSTINSTALL" > /usr/local/bin/secure_mysql#!/bin/bashOPENCARTPASS="$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-25)"ROOTPASS="$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-25)"mysql -uroot -e "ALTER USER 'opencart'@'localhost' IDENTIFIED BY '$OPENCARTPASS';"mysql -uroot -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$ROOTPASS';"echo "New password for opencart@localhost: $OPENCARTPASS"echo "New password for root@localhost: $ROOTPASS"sed -i --follow-symlinks "s/define('DB_PASSWORD', '');/define('DB_PASSWORD', '${OPENCARTPASS}');/g" /usr/share/nginx/html/config.phpsed -i --follow-symlinks "s/define('DB_PASSWORD', '');/define('DB_PASSWORD', '${OPENCARTPASS}');/g" /usr/share/nginx/html/admin/config.phpsystemctl restart nginx php-fpmOPENCARTPASS=ROOTPASS=rm -rf /usr/share/nginx/html/installrm -f /usr/local/bin/secure_mysqlPOSTINSTALL

Сделаем скрипт исполняемым:

chmod +x /usr/local/bin/secure_mysql


На данном этапе все настройки выполнены, остается выключить сервер и сделать снапшот:

shutdown -h now

Развертывание Opencart из образа


Развернув новый сервер из образа, мы, в роли пользователя, можем перейти по ссылке с адресом сервера, например: http://vps_ip_address/

На первом экране установки выбрать требуемый язык (English/Russian).
На странице конфигурации базы данных, нужно указать пользователя базы данных opencart, пароль оставьте пустым (пароли MySQL будут сгенерированы и настроены чуть позже), имя базы данных укажем opencart (так как мы создали ее ранее), зададим логин, пароль и email учетной записи администратора Opencart.

После завершения установки, мы перейдем в административную часть, и на сообщение Выберите как вы хотите перенести директорию storage, выберем вариант Автоматическое перемещение и нажмем Переместить.
Opencart переместит свое хранилище автоматически и после сообщения Хранилище storage успешно изменено! это сообщение можно закрыть. На этом основная настройка Opencart завершена.

Генерация паролей MySQL


Для того что бы обезопасить сервер, нам осталось подключиться к серверу по SSH и выполнить скрипт, что мы написали ранее на этапе подготовки образа:

secure_mysql

Скрипт сгенерирует пароли для MySQL пользователей root, opencart, выведет их в консоль и пропишет их в конфигурационных файлах opencart.
А так же удалит каталог с установочными файлами opencart: /usr/share/nginx/html/install.
На этом этапе нужно сохранить пароли в надежном месте, так как они выводятся в консоль один раз и нигде больше не сохраняются.

Настройка HTTPS и сертификатов Let's Encrypt


Для настройки HTTPS у VDS должно быть действующее DNS имя, которое мы должны указать в /etc/nginx/nginx.conf в разделе server, например:

server_name domainname.ru;

Перезапустим nginx:

service nginx restart

Запустим certbot:

sudo /usr/local/bin/certbot-auto --nginx

Введем свой e-mail, cогласимся с условиями сервиса (A), Подписка на рассылку (опционально) (N), выберем доменные имена для которых нужно издать сертификат (Enter для всех).

В случае, если все прошло без ошибок, мы увидим сообщение об успешной выдаче сертификатов и настройке сервера:

Congratulations! You have successfully enabled ...

После этого подключения на 80 порт будут перенаправляться на 443 (https).

Добавим в /etc/crontab для автоматического обновления сертификатов:

# Cert Renewal30 2 * * * root /usr/local/bin/certbot-auto renew --post-hook "nginx -s reload"

Готово, теперь у нас есть готовый сервер Opencart с настроенными сертификатами Let's Encrypt!

Хотим напомнить, что вы тоже можете сделать для нас образ


Есть три варианта, как поучаствовать.

Подготовьте образ сами и получите 3000 рублей на баланс


Если вы готовы сразу ринуться в бой и самому создать образ, которого вам не хватает, мы зачислим вам 3000 рублей на внутренний баланс вы сможете потратить на серверы.

Как создать свой образ:

  1. Создайте аккаунт у нас на
  2. Сообщите в поддержку, что вы собираетесь создавать и тестировать образы
  3. Мы зачислим вам 3000 рублей и включим возможность создавать снапшоты
  4. Закажите виртуальный сервер с чистой операционной системой
  5. Установите на эту VPS программное обеспечение и настройте его
  6. Составьте инструкцию или скрипт для развертывания ПО
  7. Создайте снапшот для настроенного сервера
  8. Закажите новый виртуальный сервер, выбрав в выпадающем списке Шаблон сервера созданный ранее снапшот
  9. В случае успешного создания сервера, передайте материалы полученные на этапе 6 технической поддержке
  10. В случае ошибки вы можете уточнить у поддержки причину и повторить настройку

Для владельцев бизнеса: предложите свой софт


Если вы разработчик софта, который разворачивают и используют на VPS, то мы можем включить вас в маркетплейс. Так мы можем помочь вам привести новых клиентов, трафик и узнаваемость.

Расскажите в комментариях, какого образа вам не хватает?


И мы подготовим его сами



Подробнее..

OpenCart popup, модальные окна

13.01.2021 16:08:51 | Автор: admin

Статья нашего сотрудника из его личного блога.

Разрабатывая модуль, в админке мне понадобилось использоватьмодальные окна OpenCartдля вывода определенной информации, а так же для показа формы. Мой опыт frontend на тот момент былтак себе, однако коллега подсказал что в OpenCart используется jquery(2.1.1), а у этой библиотеки есть поддержка popup окон. Но не все так просто

Уточним: модальное окно == всплывающее окно == popup.

Библиотека модальных окон

Кнопка при клике по которой показывается модальное окноКнопка при клике по которой показывается модальное окно

Для того чтобы использоватьмодальное окно OpenCart, надо определитькакая библиотекапредоставляет для этого функционал, используемый в этом движке. Разбирая админку OpenCart 3.0(в 2.3 такой кнопки нет), в разделеПанель состояниябыла найдена кнопка, при клике по которой показалосьвсплывающее окно.

То что нужно, начинаем разбор :)

Модальное окно в админке OpenCart Настройки разработчикаМодальное окно в админке OpenCart Настройки разработчика

Заходим на страницу "Панель состояния", открываем ее исходный код и смотрим в конце скрипт:

$('#button-setting').on('click', function() {    $.ajax({        url: 'index.php?route=common/developer&user_token=D9aTD65JQVdyOY9pcVxcRUx0M3eTefnr',        dataType: 'html',        beforeSend: function() {            $('#button-setting').button('loading');        },        complete: function() {            $('#button-setting').button('reset');        },        success: function(html) {            $('#modal-developer').remove();                         $('body').prepend('<div id="modal-developer" class="modal">' + html + '</div>');                         $('#modal-developer').modal('show');        },        error: function(xhr, ajaxOptions, thrownError) {            alert(thrownError + "\r\n" + xhr.statusText + "\r\n" + xhr.responseText);        }    }); }); 

Как видно, на кнопку с idbutton-settingвешается обработчик на клик, в которомajax запрос, успешный результат которого показывается вpopup окне. А для показа этого окна используется:

$('#modal-developer').modal('show');

Похоже на библиотеку jquerymodal. Однако в bootstrap тоже есть поддержка модальных окон. Пробуем выяснить через отладчик какая библиотека все-таки используется,ставим брекпоинтв вышеприведенном скрипте на методеmodal.

Брекпоинт на методе modalБрекпоинт на методе modal

Нажимаем на кнопку(на первом скрине), попадаем на брекпоинт, шагаем внутрь и попадаем вbootstrap.min.js.

Стек привел в bootstrap.min.jsСтек привел в bootstrap.min.js

Всплывающие окна в OpenCartреализуются через bootstrap.

Использование popup окон в OpenCart

Проинспектировав окновыясняем что оно уже содержит нужную нам логику закрытия окна и состоит из двух важных для нас частей:

  • divс классомmodal-headerв которомh4, который и есть заголовок окна

  • divс классомmodal-bodyC11Cвнутри содержит контент окна

Просмотр html кода модального окна Настройки разработчикаПросмотр html кода модального окна Настройки разработчика

Так как окно уже имеет оформление, то просто возьмем его каркас, классы и на основе верстки этого окна составим свое:

<div id="modal-window" class="modal">    <div class="modal-dialog">        <div class="modal-content">                       <!--заголовок и кнопка закрытия-->            <div class="modal-header">                <button type="button" class="close" data-dismiss="modal" aria-hidden="true"></button>                <h4 class="modal-title"></h4>            </div>                         <!--контентная часть окна-->            <div class="modal-body"></div>                     </div>    </div></div>

Для показа окна с idmodal-windowбудем использовать:

$('#modal-window').modal('show');

Теперь в нужном нам месте страницы в админке разместим свои вёрстку и js. Для этого используем события в OpenCart. Например повесим обработчик на страницу редактирования заказа:

$this->model_extension_event->addEvent('modal_window', 'admin/view/sale/order_form/after', 'extension/module/modal_window/eventSaleOrderFormAfter');

Теперь обработчик:

public function eventSaleOrderFormAfter(&$route, &$args, &$output){    $idOrder = $args["order_id"];     $this->load->model('sale/order');    $this->load->model('catalog/product');     //загрузка списка продуктов заказа    $aOrderProducts = $this->model_sale_order->getOrderProducts($args["order_id"]);         //строка верстки списка товаров    $sOrderProducts = "";     //формируем список товаров    for($i=0; $i<count($aOrderProducts); ++$i)    {        $aProduct = $this->model_catalog_product->getProduct($aOrderProducts[$i]["product_id"]);        $sOrderProducts .= "<li>".$aProduct["name"]." - ".$aProduct["model"]." (".$aOrderProducts[$i]["quantity"]." шт.): ".round($aOrderProducts[$i]["total"], 2)."р. </li>";    }         $sOrderProducts = "<ul>$sOrderProducts</ul>";     //верстка модального окна и скрипт вызова    $sModal = '    <div id="modal_window" class="modal">        <div class="modal-dialog">            <div class="modal-content">                <div class="modal-header">                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true"></button>                    <h4 class="modal-title">Список товаров</h4>                </div>                <div class="modal-body">                    '.$sOrderProducts.'                </div>            </div>        </div>    </div>    <script>$("#modal_window").modal("show");</script>    ';     //находим закрывающий тег body и перед ним вставляем модальное окно и скрипт его показа    $iPos = strripos($output, "</body>");    $output = substr($output, 0, $iPos).$sModal.substr($output, $iPos);}

Каждый раз при входе на страницу редактирования заказа будет появляться всплывающее окно со списком товаров.

Можно вставлять кнопку в панель кнопок в админке(или в любое другое место), повесить на кнопку обработчик клика и показывать модальное окно(как это сделано на страницеПанель состояния). Однако для этого придется использовать регулярные выраженияили парсер DOM.

Автор: Виталий Бутурлин

Подробнее..

Ajax, REST API OpenCart

21.01.2021 12:12:35 | Автор: admin

В статье рассмотрим как устроеныajax запросы в OpenCart, в том числе запросы черезapi OpenCart, познакомимся с новым понятиемfront controllerи немного коснемся темыajax REST API.

Клиент

Клиентская часть OpenCart работает с использованиемjquery, а значит можно использовать$.ajaxиз этой библиотеки.Ссылка на документацию. Примеры ajax запросов на клиентской части можно посмотреть вadmin/view/template/sale/order_form.tpl(.twig для OpenCart 3.0).

Сервер

Просматривая все тот же файлadmin/view/template/sale/order_form.tpl(для OpenCart 2.3)можно понять, чтов качестве адреса вызова используется классическая схема роутинга OpenCart. Посмотрим на один из запросов:

$.ajax({    url: 'index.php?route=customer/customer/autocomplete&token=<?php echo $token; ?>&filter_name=' +  encodeURIComponent(request),    ...

Все просто:url - путь до контроллера и, если надо,имя метода этого контроллера.

То есть, нам нужносоздать класс контроллера, затем из файлов представления можновызывать методы этого контроллераajax запросами.

Создадим контроллер нашего нового тестового модуля по путиadmin/controller/extension/module/myajax.php:

class ControllerExtensionModuleMyAjax extends Controller{    public function index()    {        $this->response->addHeader('Content-Type: application/json');        $this->response->setOutput(json_encode(            [                "success" => true,                 "message" => "ok",                 "data" => []            ]        ));    }}

В классе контроллера есть объектresponse, это экземпляр классаResponse, который расположен по путиsystem/library/response.php. Он позволяет управлять ответом сервера. Нас интересуют только 2 метода:

  • addHeader($header)- добавить http заголовок,headerстроковый аргумент

  • setOutput($output)- установить данные для вывода,outputстроковый аргумент

Для формирования ответа на запрос в методе контроллера можно использовать-

$this->response

Так как OpenCart имеет 2режима доступа/контекста(admin, catalog), то передаваемые данные в запросах разные:

  • admin- требует токен вgetпараметре(получить можно из объекта класса контроллера):

    • для OpenCart 2.3token, который берется из$this->session->data['token']

    • для OpenCart 3.0C4C, который берется изC14CC5C

  • catalog- в общем случае не требует токена, но есть нюансы о которых позже

Теперь чтобы осуществитьajax запросдостаточно в файл представления(читай в html)подставить js(код для OpenCart 2.3):

$.ajax({    url: '<?php echo $admin; ?>index.php?route=extesion/module/myajax&token=<?php echo $token; ?>',    type: 'get',    dataType: 'json',    success: function(json) {        alert("success: "+json["success"]+"\n"+"message: "+json["message"]);    },    error: function(xhr, ajaxOptions, thrownError) {        alert(thrownError + "\r\n" + xhr.statusText + "\r\n" + xhr.responseText);    }});

В этом коде в urladminэто путь указывающий контекст запроса(admin или catalog). Для контекста есть 2 дефайна, определенных вadmin/config:

  • HTTP_SERVERилиHTTP<b style="box-sizing: border-box;">S</b>_SERVER- путь до директорииadmin(проще - админка), где будет осуществлен поиск контроллера для выполнения запроса

  • HTTP_CATALOGилиC6CC15C- корень сайта, однако контроллеры будут браться из директорииC16CC7C

Ajax API

Просматривая файл представленияadmin/view/template/sale/order_form.tpl(OpenCart 2.3), можно увидеть что из админки осуществляются ajax запросы наcatalogконтекст, с использованием особого токена.

Сначала объявляется глобальная переменнаяtoken, затем ajax запрос на адрес/index.php?route=api/login, который отвечает json данными, в которых есть ключtoken:

var token = ''; // Login to the API$.ajax({    url: '<?php echo $catalog; ?>index.php?route=api/login',    type: 'post',    data: 'key=<?php echo $api_key; ?>',    dataType: 'json',    crossDomain: true,    success: function(json) {    //...         if (json['token']) {            token = json['token'];        }    },    error: function(xhr, ajaxOptions, thrownError) {        alert(thrownError + "\r\n" + xhr.statusText + "\r\n" + xhr.responseText);    }});

Контроллер этого запроса находится вcatalog/controller/api/login.php ControllerApiLogin::index. Он:

  • создает новую сессию

    (catalog/model/account/api.php - ModelAccountApi::addApiSession) и

  • генерирует для нее случайный токен(функцияtokenнаходится вsystem/helper/general.php),

который возвращается в json этого ajax запроса, если доступ по api(api_key)разрешен для текущего пользователя(Админка-Система-API).

Дальше разбирая представлениеadmin/view/template/sale/order_form.tplможно увидеть, что последующиеajax запросы, которые по адресуroute=api/...используют этот самыйtokenдля определения права доступа, таким образом(в каждом api файле, в каждом методе)C25C существует такой кусок кода для определения права осуществлять запрос:

if (!isset($this->session->data['api_id'])) {    $json['error']['warning'] = $this->language->get('error_permission');} else {    ...}

Ajax запросы черезcatalogконтекст можно осуществлять с использованиемtokenдля безопасного доступа

А теперь копнем глубже и выясним, как это происходит внутри движка, ведь можно отправлятьajax запросыи без токена.

Просматривая код файлаindex.phpотправляемся вsystem/startup.php, оттуда следуем вsystem/framework.phpв самый конец и видим такое вот:

// Front Controller$controller = new Front($registry); // Pre Actionsif ($config->has('action_pre_action')) {    foreach ($config->get('action_pre_action') as $value) {        $controller->addPreAction(new Action($value));    }}

Здесь видим новое понятиеfront controller, код которого находится вsystem/engine/front.phpв классеFront.

Ниже следует мое субъективное определение этого понятия :)

Подробных комментариев найти не удалось, но судя по кодуfront controllerэтоглавный/передний контроллер, онзапускает общий контроллерstartup/routerотносительно директорииcontrollerконтекста(admin/controllerилиcatalog/controller), которыйвыполняет первичные контроллеры,указанные в$_['action_pre_action'];в файлеsystem/config/catalog.php.

В коде выше происходит только добавление первичных контроллеров воfront controller, а их исполнение осуществляется кодом ниже, в методеdispatch(внутри метода перед выполнением action указанного в$config->get('action_router')):

// Dispatch$controller->dispatch(new Action($config->get('action_router')), new Action($config->get('action_error')));

Среди первичных контроллеров естьstartup/sessionотносительноcatalog/controller, где вControllerStartupSession::indexнаходится интересующий наскод для авторизации в api через токен. Вкратце:

  • происходит проверка обращения кapi/и наличияgetпараметраtoken

  • удаление старых api сессий

  • выборка актуальной api сессии на основании ip адреса запросившего и его токена

  • старт сессии с id из$_COOKIE["api"]

  • обновление времени модификации сессии, (чтобы она осталась жива, то есть не была устаревшей)

Теперь, когда исполнение кода дойдет до целевого контроллера,$this->session->data['api_id']уже будет инициализировано, если указана актуальная комбинация токена и ip адреса.

Ajax REST API

Данная глава описывает возможный вариант создания и встроенные средства реализации REST API в OpenCart.

Мы рассмотрели реализациюajax запросов OpenCartдляadminиcatalogконтекстов.

Если говорить обadmin, то предполагается более рациональным реализовывать контроллеры именно вadminконтексте. Однако, такое не всегда возможно. Иногда один и тот же код контроллера(возможно речь о методе контроллера)должен использоваться в обработчикеcatalogсобытия(например при изменении заказа), так и отдельно непосредственно при работе с заказом через админку. Чтобы устранить такие случаи можно реализовать контроллеры вcatalogконтексте и организовать для нихбезопасный доступ(о чем говорится в предыдущей главе).

Для реализацииREST API в OpenCartесть все необходимое:

  • объект для работы с ответом сервера в контроллере$this->response, а именно методыaddHeaderиsetOutput

  • безопасная работа с административным доступом черезcatalogC22Cконтекст

  • единая точка входа api черезC24CC10CC25C контекст, в директориюC26CC11CC27C, можно размещать свои файлы контроллеров и при помощи ajax осуществлять к ним запросы

На стороне сервера надосоздать контроллерывcatalog/controller/api/, а на стороне клиентадобавить ajax запросы в нужные файлы представленийс использованием токена, полученного в результате ajax запросаapi/login. Если в этих файлах нет такого ajax запроса, тогда необходимо добавить его, например, взяв изadmin/view/template/sale/order_form.tpl.Теперь чтобы сделать REST API достаточно изучить, что это такое, несколько ссылок:

Автор: Виталий Бутурлин

Подробнее..

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

21.11.2020 14:13:58 | Автор: admin
Реально ли при разработке модификаций для распространенного движка интернет-магазинов 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, это пример, каждый делает под себя):

<?phpdefine("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):

<?phpinclude_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.zipphp make-ocmod.php make-ocmod.opencart.local.cfg.phpcd publish..\7z.exe a -r -tZip ..\publish.ocmod.zip *.*

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

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

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

Дополнительное поле в карточке товара OpenCart

17.12.2020 18:19:07 | Автор: admin

Статья нашего разработчика из его личного блога.

Разрабатываямодуль для OpenCart, возникла необходимость сделатьпроизвольное поле в карточке товара, которое должно быть виднотолько в админке. Это должно быть поле слогическим значением"маркирован товар или нет". Вот так в итоге:

Дополнительное поле в карточке товара OpenCartДополнительное поле в карточке товара OpenCart

Недолго поковыряв админку выяснил, чтоиз коробкиOpenCart не поддерживает кастомные поля, однако есть платные модули, например,вот. Значит нужно сделать кастомное поле в OpenCart самому.

Варианты решения задачи:

  • заюзать неиспользуемые поля(sku, upc, ean, jan, isbn, mpn)-почти мгновенно, но возможно кто-то из наших клиентов будет использовать занятое нами поле.

  • вмешаться в код движка-быстро и мало кода, однако тогда решение не переносимо и при обновлении движка придется обновлять и само решение, так как изменения будут потеряны при обновлении движка.

  • разработать модуль-дольше, больше кода, переносимо на другие сайты с той же версией OpenCart, не зависит от обновления движка(за исключением мажорных версий).

Для реализации дополнительного поля в карточке товара будем разрабатывать модуль ProductMarkedField. В общем схема выглядит так:

  • изменить таблицу базы данныхпри установке модуля и прибраться в ней после удаления нашего модуля.

  • показать дополнительное поле в карточке товарав админке OpenCart.

  • сохранить полепри отправке формы

Последние два пункта должны быть выполнены при подписке на событияпоказа карточки товараиизменения товара.

Про детали разработки модулей для opencart поговорим в другой раз, а пока предположим что читатель имеет минимальный опыт в этом деле.

Основная логика модуля умещается в один файл контроллера admin/controller/extension/module/productmarkedfield.php. Для того чтобы модуль отобразился в разделе "Расширений"(чтобы его можно было инсталлировать/деинсталлировать)надо создать языковой файлadmin/language/ru-ru/extension/module/productmarkedfield.phpс таким содержимым:

<?php $_['heading_title'] = 'Кастомное поле в карточке товара "Маркировка"';

Установка модуля

В методе install нужномодифицировать таблицуproduct:

$this->db->query("ALTER TABLE `".DB_PREFIX."oc_product` ADD `marked` TINYINT UNSIGNED NOT NULL DEFAULT '0';");

В ocStore 2.3.x все нормально, но в ocStore 3.0.2.0 при использовании MySQL 8, запрос добавления нового столбца в таблицу заканчивался ошибкой:

Для решения этой проблемы изменим значение по умолчанию для столбца date_available:

$this->db->query("ALTER TABLE `".DB_PREFIX."product` CHANGE `date_available` `date_available` DATE NOT NULL;");

Теперь надо добавитьобработчики событий, чтобы с их помощью мы моглиизменять верстку карточки товараисохранять значение нашего дополнительного поля(для ocStore 2.3.x):

$this->load->model('extension/event'); //событие "после загрузки формы товара" - для показа дополнительного поля товара (обязательна маркировка или нет)$this->model_extension_event->addEvent(  'productmarkedfield', //код, в данном случае название модуля  'admin/view/catalog/product_form/after', //событие   'extension/module/productmarkedfield/eventProductFormAfter' //обработчик); //событие "после редактирования товара" - для сохранения статуса маркировки$this->model_extension_event->addEvent(  'productmarkedfield',   'admin/model/catalog/product/editProduct/after',   'extension/module/productmarkedfield/eventProductEditAfter');

Для ocStore 3.0.x модель событий загружается таким образом:

$this->load->model('setting/event');

И вместо объекта model_extension_event нужно использовать model_setting_event соответственно.

Дополнительное поле в карточке товара

Выводить наше поле в карточку товара мы будем после загрузкиadmin/view/template/catalog/product_form.twig. наш метод будет принимать 3 аргумента:

public function eventProductFormAfter(&$route,&$args, //переданные аргументы в этот шаблон&$output//html верстка страницы)

Нас интересует третий аргумент &$output, именно его мы и будем модифицировать вставляя туда верстку со значением нашего нового поля.

Для модификации формы нам понадобитсяSimple HTML DOM, икраткий мануал. Скачиваем и кладём его в system/library, а в коде подгружаем расположенный внутри класс таим образом(@ чтобы не выводить ошибки, так как оригинальная версия этой библиотеки загружается с некритичными ошибками):

@$this->load->library('simple_html_dom');

Затем используя регулярные выражения для того чтобы получить id товара.(Искал другие варианты, но id в нормальном виде в $args массиве не нашел):

preg_match("/product_id=(\d+)/", $args["action"], $aMatch);$idProdict = $aMatch[1];

Загружаем модель описывающую товар и получаем необходимую информацию о товаре(данные из таблиц product и product_description):

$this->load->model('catalog/product');$aProduct = $this->model_catalog_product->getProduct($idProdict);

Теперь мы имеем доступ к данным товара, среди которых наше дополнительное поле - значение маркировки.

Однако, этот код будет работать только для редактирования уже существующего товара, но при создании нового товара будут ошибки, так как не будет найден id еще несуществующего товара. Изменим вышеприведенный код:

$isMarked = false;if(preg_match("/product_id=(\d+)/", $args["action"], $aMatch)){    $idProduct = $aMatch[1];    $this->load->model('catalog/product');    $aProduct = $this->model_catalog_product->getProduct($idProduct);    $isMarked = $aProduct["marked"];}

То есть мы создали новую переменную isMarked, которая будет хранить в себе значение по умолчанию false и если удастся найти id товара, тогда в isMarked будет записано значение из нашего произвольного поля в карточке товара.

Теперь при помощи Simple HTML DOM найдем вкладку "Данные" и вставим в самое начало наше поле маркировки, предварительно подсмотрев на верстку необходимого gui элемента вadmin/view/template/catalog/product_form.twig(в ocStore 2.3.x tpl расширение, и внутри нет Twig):

$html = str_get_html($output);$html->find('div#tab-data', 0)->innertext = '<div class="form-group">    <label class="col-sm-2 control-label">Маркирован</label>    <div class="col-sm-10">        <label class="radio-inline">            <input type="radio" name="marked" value="1" '.($aProduct["marked"] ? 'checked="checked"' : "").'>Да        </label>        <label class="radio-inline">            <input type="radio" name="marked" value="0" '.(!$aProduct["marked"] ? 'checked="checked"' : "").'>Нет        </label>    </div></div>' . $html->find('div#tab-data', 0)->innertext;

Код рабочего метода целиком:

public function eventProductFormAfter(&$route, &$args, &$output){    @$this->load->library('simple_html_dom');    $isMarked = false;    if(preg_match("/product_id=(\d+)/", $args["action"], $aMatch))    {        $idProduct = $aMatch[1];        $this->load->model('catalog/product');        $aProduct = $this->model_catalog_product->getProduct($idProduct);        $isMarked = $aProduct["marked"];    }         $html = str_get_html($output);    $html->find('div#tab-data', 0)->innertext =     '<div class="form-group">        <label class="col-sm-2 control-label">Маркирован</label>        <div class="col-sm-10">            <label class="radio-inline">                <input type="radio" name="marked" value="1" '.($isMarked ? 'checked="checked"' : "").'>Да            </label>            <label class="radio-inline">                <input type="radio" name="marked" value="0" '.(!$isMarked ? 'checked="checked"' : "").'>Нет            </label>        </div>    </div>' . $html->find('div#tab-data', 0)->innertext;    $output = $html->outertext;}

Сохранение значения дополнительного поля

Для сохранения результатов в момент когда администратор нажимает кнопку "Сохранить", необходимо вручную(с помощью обработчика события)внести изменения в базу данных, так как модель catalog/product при редактировании товара(ModelCatalogProduct::editProduct)сохраняет только определенный набор данных, и наше новое поле не входит в этот набор.

Для этого мы уже зарегистрировали ранее обработчик события "после редактирования товара":

public function eventProductEditAfter(&$route, &$args){  //в $args[0] лежит id товара  $sSql = "UPDATE " . DB_PREFIX . "product SET marked = " . $this->db->escape($args[1]['marked']) . " WHERE product_id = '" . (int)$args[0] . "'";  $this->db->query($sSql);}

Удаление модуля

При деинсталляции модуля надо удалить столбец marked из таблицы product и удалить обработчики событий, установленные нашим модулем. Все это делается в методе uninstall.

Удалить столбец из таблицы товаров:

$this->db->query("ALTER TABLE `".DB_PREFIX."product` DROP `marked`");

Удалить все сообщения (для ocStore 2.3.x):

$this->load->model('extension/event');$this->model_extension_event->deleteEvent('productmarkedfield');

Удалить все сообщения (ocStore 3.0.x):

$this->load->model('setting/event');$this->model_setting_event->deleteEventByCode('productmarkedfield');

Послесловие

В целом не так уж и трудно, однако кажется немного странным тот факт, что верстку надо менять руками, так как нет удобного встроенного инструмента для изменения интерфейса.

Для тех, кто дочитал до конца -ссылкана архив с исходным кодом модуля.

Автор: Виталий Бутурлин

Источник

Подробнее..
Категории: Программирование , Sql , Php , Opencart

События в OpenCart

24.12.2020 12:19:48 | Автор: admin

Статья нашего сотрудника из его личного блога.

В статье речь идет об OpenCart версии> =2.3, а именно рассматриваются 2.3 и 3.0

Система событий в OpenCart достаточно интересна, она не является заранее предопределенным списком событий. Внутренность движка устроена таким образом, что почти каждый метод контроллера, который реагирует на определенный роут, загружает какие-то файлы(другие контроллеры, модели, представления, переводы).

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

Например, рассмотрим контроллерadmin/controller/catalog/product.phpу которого на адрес/admin/index.php?route=catalog/productбудет вызван методindex:

public function index() {    $this->load->language('catalog/product');     $this->document->setTitle($this->language->get('heading_title'));     $this->load->model('catalog/product');     $this->getList();}

В этом методе используется загрузка файла перевода и моделиcatalog/product, и на оба факта загрузки можно установить свои обработчики изменяющие данные.

Какие события есть в OpenCart 2.3+?

Как мы определили ранее, заранее определенного списка событий нет. Однако предполагаемые события можно узнать в файлеsystem/engine/loader.php.$this->loadи есть объектLoader.

Просматривая файл можно увидеть что события генерируются ($this->registry->get('event')->trigger) при загрузке:

  • контроллеров

  • моделей

  • представлений

  • конфигов

  • переводов

Нас интересуют не все объекты. Так как OpenCart построен по MVCl архитектуре, то в нем есть 4 вида загружаемых файлов, на основании загрузки, которых можно изменить/добавить логику движку. MVCl вкратце:

  • Model - файлы моделей, те что работают с БД,admin/modelилиcatalog/model

  • View - файлы представлений, интерфейс/верстка,admin/viewилиcatalog/view

  • Controller - файлы обработчики роутов(путей по которым админы ходят в админке, а клиенты в клиентской части),admin/controllerилиcatalog/controller

  • language - файлы переводов,admin/languageилиcatalog/language

При загрузке каждого такого объекта(кроме конфигов)движка генерируются события:

  • before- до загрузки

  • after- после загрузки

То есть, мы можем изменить(или вообще заменить, но об этом позже)логику при загрузке файла.

Логика работы событий

В момент, когда мы открываем страницу админки, или клиент просматривает товар, или со страницы сайта происходит ajax запрос, движок запускает первый контроллерstartup/router, который в свою очередь на основании get параметраrouteвыполняетactionцелевого контроллера(путь которого указан в route).

Однако, контроллерstartup/routerне выполняет загрузку через$this-> load, а самостоятельно генерирует событиеbefore, получая от него результат, и если этот результатnull, тогда целевой контроллер будет выполнен и наступит событиеafter(куcок кода изadmin/controller/startup/router.phpOpenCart 3.0):

// Trigger the pre events$result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data)); if (!is_null($result)) {    return $result;} // We dont want to use the loader class as it would make an controller callable.$action = new Action($route); // Any output needs to be another Action object.$output = $action->execute($this->registry);  // Trigger the post events$result = $this->event->trigger('controller/' . $route . '/after', array(&$route, &$data, &$output)); if (!is_null($result)) {    return $result;} return $output;

Иными словами,

OpenCart 2.3+ позволяет полностью переопределить поведение запроса приbeforeилиafterсобытии.

Для загрузчиков файлов действует другая логика.

Если в событииbeforeодин из обработчиков возвращает неnull, тогда загрузка файла не будет происходить, ивместо результата загрузки файлабудетрезультат выполнения обработчика, вернувшего неnull. При этом событиеafterбудет сгенерировано и если один из обработчиков вернет неnullтогда результат его работы заменит предыдущий. Это можно увидеть на примере загрузки контроллера (основная логика аналогична и для представлений/моделей/переводов):

public function controller($route, $data = array()) {  // Sanitize the call  $route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);     // Keep the original trigger  $trigger = $route;     file_put_contents($_SERVER['DOCUMENT_ROOT']."/loader-controller.txt", $trigger."\n", FILE_APPEND);     // Trigger the pre events  $result = $this->registry->get('event')->trigger('controller/' . $trigger . '/before', array(&$route, &$data));     // Make sure its only the last event that returns an output if required.  if ($result != null && !$result instanceof Exception) {    $output = $result;  } else {    $action = new Action($route);    $output = $action->execute($this->registry, array(&$data));  }     // Trigger the post events  $result = $this->registry->get('event')->trigger('controller/' . $trigger . '/after', array(&$route, &$data, &$output));     if ($result && !$result instanceof Exception) {    $output = $result;  }     if (!$output instanceof Exception) {    return $output;  }  }

Любой из обработчиков событияbeforeилиafter, может переопределить результат выполнения события.

Вsystem/engine/event.php Event::triggerопределено: если какой-либо обработчик события(вbeforeилиafter)возвращает неnull, тогда после него не будут запущены другие обработчики этого события(дляbeforeилиafter).

Аргументы обработчиков событий

Для четырех файлов (контроллер, представление, модель, перевод) набор аргументов и основная логика загрузки одинаковая.

Аргументы событий передаются по ссылке, а значит изменения значений аргументов будут видны во всех местах где они используются. При загрузке MVCl файлов аргументы:

  • приbeforeсобытии:&$routeи&$data

  • приafterсобытии:&$route,&$dataи&$output

Описание аргументов(дополнительно можно посмотреть здесьsystem/engine/loader.php):

  • &$routeсодержит данные о пути данного события, безcontroller|view|model|languageи безbefore|after, например, для событияcatalog/model/checkout/order/addOrderHistory/afterв&$routeбудетcheckout/order/addOrderHistory

  • &$dataсодержит массив данных для работы события(либо пустой массив), например, для файлов представления это данные для подстановки в tpl/twig файлах

  • &$outputсодержит результат работы самого события(или обработчика, который определил возвращаемое значение), например, для файлов представления это обработанное содержимое файла представления

Для видов в&$dataпередается ассоциативный массив для использования в tpl/twig файлах видов. В&$outputверстка загруженного вида, где данные из&$dataуже вставлены. Изменения&$dataприbeforeсобытии могут не иметь смысла, так как данные уже обработаны. Это относится ко всем загружаемым файлам.

Изменять&$outputприafterсобытии представления можно различными способами, один из которых используя библиотеку Simple Html DOM.

Удаление данных из&$dataприbeforeсобытии может быть критичным для следующих обработчиков, а добавление данных может не иметь смысла если обработчики событий не знают этих данных!

Хранение обработчиков событий в OpenCart

Системные обработчики хранятся в php файлахsystem/config/admin.phpиsystem/config/catalog.phpв ассоциативном массиве$_['action_event'], где ключ это загружаемый файл, а значение обработчик. Как видно из ключей этого массива, вместо полного пути загружаемого файла, можно указывать * в качестве реакции "на все". Таким образом часть событий проходит через "движковые обработчики".

В OpenCart 3.0 появились приоритеты работы обработчиков(чем меньше значение, тем выше приоритет)и объекты моделей больше не обрабатываются "движковыми" обработчиками.

Пользовательские(от модулей)обработчики событий хранятся в БД в таблицеevent:

  • event_id- идентификатор (автоинкремент)

  • code- код обработчика, один и тот же код может быть у нескольких обработчиков, сюда записывается название модуля

  • trigger- событие, например,admin/view/catalog/product_form/after- после загрузки формы товара

  • action- обработчик, напримерextension/module/productmarkedfield/eventProductFormAfter

  • status- включен или нет обработчик (1/0)

  • sort_order- порядок сортировки (приоритет выполнения обработчика)

Добавление обработчиков событий

Работа с событиями заключается в:

  • регистрации обработчиков событий при инсталляции модуля(методinstall)

  • удалении всех обработчиков событий при деинсталляции модуля(методuninstall)

Обязательно надо удалять обработчики событий при удалении модуля, иначе движок будет запускать обработчики из модуля, а при отсутствии файлов удаленного модуля будет ошибка!

Для работы с событиями на стороне админки, для OpenCart 2.3 есть модельextension/event, а для OpenCart 3.0setting/event.

Метод регистрации в обоих версиях одинаковый, за исключением параметра$sort_order(кусок кода из моделиeventдля OpenCart 3.0):

public function addEvent($code, $trigger, $action, $status = 1, $sort_order = 0) {    $this->db->query("INSERT INTO `" . DB_PREFIX . "event` SET `code` = '" . $this->db->escape($code) . "', `trigger` = '" . $this->db->escape($trigger) . "', `action` = '" . $this->db->escape($action) . "', `sort_order` = '" . (int)$sort_order . "', `status` = '" . (int)$status . "'");     return $this->db->getLastId();}

Отличается только названием обращения к объекту, через который ведется регистрация.

Разберем значение аргументов:

  • code- идентификатор группы обработчиков, обычно это название модуля

  • trigger- обрабатываемое событие, напримерadmin/view/catalog/product_form/after

  • action- контроллер обработчик события, например,extension/module/productmarkedfield/eventProductFormAfterэто путь до файла обработчика, относительно того контекста для которого устанавливается обработчик, при этом в указанном файле должен быть класс имя, которого формируется из

"Controller" . $sRelPath . $sFileName

гдеsRelPathэто относительный путь до файла, аsFileNameэто имя файла. Для данного примера имя класса контроллера будетControllerExtensionModuleProductmarkedfield

  • status- статус вкл/выкл, по умолчанию включен

  • sort_order- порядок сортировки (OpenCart 3.0), чем меньше значение, тем выше приоритет выполнения, по умолчанию 0

Отдельно стоит рассказать про отношениеtriggerиaction.triggerуказывается полным путем(почти)до файла(и в случае контроллера или модели еще и указанием выполняемого метода)вместе с контекстомadminилиcatalog, аactionуказывается относительным, безadminилиcatalog. Например,

  • trigger-admin/view/catalog/product_form/after,action-extension/module/productmarkedfield/eventProductFormAfter, полный путь до файла обработчикаadmin/controller/extension/module/productmarkedfield.phpметодControllerExtensionModuleProductmarkedfield::eventProductFormAfter

  • trigger-catalog/model/checkout/order/addOrderHistory/after,action-extension/module/productmarkedfield/eventaddOrderHistoryAfter, полный путь до файла обработчикаcatalog/controller/extension/module/productmarkedfield.phpметодControllerExtensionModuleProductmarkedfield::eventaddOrderHistoryAfter

Для примера работы с функциями добавления/удаления обработчиков, возьмеммодуль дополнительного поля в карточке товараиз предыдущей статьи.

Код регистрации обработчика событий для OpenCart 2.3 будет выглядеть так:

$this->load->model('extension/event'); //событие "после загрузки формы товара" - для показа дополнительного поля товара (обязательна маркировка или нет)$this->model_extension_event->addEvent(  'productmarkedfield', //название модуля  'admin/view/catalog/product_form/after', //событие   'extension/module/productmarkedfield/eventProductFormAfter' //обработчик

А для OpenCart 3.0 так:

$this->load->model('setting/event'); //событие "после загрузки формы товара" - для показа дополнительного поля товара (обязательна маркировка или нет)$this->model_setting_event->addEvent(  'productmarkedfield',  'admin/view/catalog/product_form/after',  'extension/module/productmarkedfield/eventProductFormAfter'

Если количество обязательных аргументов обработчика события превышает количество передаваемых аргументов роутером, то обработчик не будет запущен!

Количество обязательных аргументов обработчика имеет значение.system/engine/action.php Action::executeпри помощи рефлексии определяет количество необходимых аргументов и если их в обработчике больше чем может передать объектactionтогда ожидаемException:

$reflection = new ReflectionClass($class);         if ($reflection->hasMethod($this->method) && $reflection->getMethod($this->method)->getNumberOfRequiredParameters() <= count($args)) {    return call_user_func_array(array($controller, $this->method), $args);} else {    return new \Exception('Error: Could not call ' . $this->route . '/' . $this->method . '!');}

Удаление обработчиков событий

Методы удаления уже имеют значительную разницу.

В OpenCart 2.3 у моделиextension/eventметодdeleteEventудаляет все обработчики событии модуля по коду(кусок кода из OpenCart 2.3):

public function deleteEvent($code) {    $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `code` = '" . $this->db->escape($code) . "'");}

OpenCart 3.0 предоставляет немного больше. МетодdeleteEventудаляет обработчик события по его идентификатору, а методdeleteEventByCodeудаляет все обработчики события по коду, какdeleteEventв OpenCart 2.3(кусок кода OpenCart 3.0):

public function deleteEvent($event_id) {    $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `event_id` = '" . (int)$event_id . "'");} public function deleteEventByCode($code) {    $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `code` = '" . $this->db->escape($code) . "'");}

Таким образом удаление обработчиков событий для OpenCart 2.3 будет выглядеть так:

$this->load->model('extension/event');$this->model_extension_event->deleteEvent('productmarkedfield');

А для OpenCart 3.0:

$this->load->model('setting/event');$this->model_setting_event->deleteEvent('productmarkedfield');

Итог

Система событий OpenCart достаточна интересна, она позволяет многое и гибко, но не без недостатков. Больше всего смущает тот факт, что для изменения интерфейса(события загрузки представлений)необходимо вручную работать с DOM.

Для понимания содержимого аргументов события нужно изучать исходный код загружаемых файлов, а в случае представлений также необходимо изучать контроллер, который передает в это представление данные. Однако со временем этот факт перерастает из "недостатка в достоинство" раскрывая прелесть движка OpenCart :)

Автор: Виталий Бутурлин

Источник

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru