Opencart 2.x - Как Создать Пользовательский Модуль - 1 Часть

Тема в разделе "Модули и дополнения", создана пользователем admin, 18 фев 2018.

  1. TopicStarter Overlay
    Offline

    admin Команда форума Администратор

    Сообщения:
    2.642
    Симпатии:
    118.322
    Репутация:
    210
    разработчику всегда интересно создать что-то свое в каком-нибудь фреймворке, то же самое можно сказать и про модули для OpenCart. В этой серии из двух уроков я объясню, как создавать пользовательские модули в OpenCart. Мы пройдемся по всем деталям продвинутой разработки в OpenCart для новичков, а также создадим небольшой модуль, чтобы показать все аспекты структуры разработки в OpenCart.

    В первом уроке мы создадим пользовательский плагин, который будет показывать недавно просмотренные товары на стороне back-end’а. Изменить количество товаров можно на стороне back-end’а. Цель сегодняшнего урока – разработать back-end плагин с формой конфигураций.

    На момент написания статьи использовалась версия 2.1.0.2. Перед самой разработкой модуля я хочу рассказать вам про базовую архитектуру модулей в OpenCart.

    Коротко о шаблоне MVCL
    OpenCart спроектирован по одному из самых популярных шаблонов веб-разработки MVC с небольшими изменениями или, можно сказать, дополнениями. Это дополнение превращает шаблон в MVCL. Возможно, вы уже слышали о данном шаблоне, но я все равно быстро пробегусь по нему для новичков.

    Буква M в MVC расшифровывается как модель, именно там расположена почти вся логика. В контексте OpenCart модель взаимодействует с абстрактным слоем базы данных и выполняет всю тяжелую работу, необходимую для работы магазина. Большую часть времени вы будете проводить в модели.

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

    C – контроллер в MVC, обрабатывающий все запросы. Данный слой охватывает большую часть логики приложения: от обработки и валидации вводимых данных до загрузки подходящей модели и вида для подготовки страницы на вывод.
    Наконец, есть дополнительный компонент L, который расшифровывается как язык. С его помощью намного легче создавать многоязычные сайты.

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

    Скелет любого плагина в OpenCart
    Давайте взглянем на то, какие файлы нам понадобятся для создания пользовательского back-end плагина.

    admin/language/english/module/recent_products.php: файл, в котором хранятся статичные названия, которые используются в админке.

    admin/controller/module/recent_products.php: файл контроллера, в котором хранится логика приложения нашего модуля.

    admin/view/template/module/recent_products.tpl: файл-шаблон представления с XHTML кодом.

    В следующей секции мы создадим все эти файлы и подробно их разберем. Согласно объявлению выше, файлы пользовательского плагина должны быть в папке module. Так как это back-end плагин, то эту папку нужно искать в папке admin. В соответствии с архитектурой, описанной выше, все файлы раскиданы по разным каталогам или компонентам.

    Создаем файлы для back-end плагина
    В этом разделе мы будем создавать файлы модуля. Первым мы создадим файл языка admin/language/english/module/recent_products.php со следующим кодом. Крайне важный файл, так как OpenCart должен найти его в вашем плагине.

    Код:
    <?php
    // admin/language/english/module/recent_products.php
    // Заголовок
    $_['heading_title'] = 'Recent Products';
    // Текст
    $_['text_module'] = 'Modules';
    $_['text_success'] = 'Success: You have modified Recent Products module!';
    $_['text_edit'] = 'Edit Recent Products Module';
    // Вход
    $_['entry_name'] = 'Module Name';
    $_['entry_limit'] = 'Limit';
    $_['entry_status'] = 'Status';
    // Ошибка
    $_['error_permission'] = 'Warning: You do not have permission to modify Recent Products module!';
    $_['error_name'] = 'Module Name must be between 3 and 64 characters!';
    Из кода видно, что статические лейблы занесены в PHP массив. Позже, когда массив будет переконвертирован в PHP переменные, они станут доступны в шаблоне представления.

    Также вы могли обратить внимание, что файл был создан в папке english – это папка с языком по умолчанию для магазина. Если у вас многоязычный сайт, необходимо будет создать дополнительные папки. К примеру, для французского необходимо будет создать файл в папке admin/language/french/module/recent_products.php.

    Далее мы создадим один из важнейших файлов плагина – файл контроллера. Создайте файл admin/controller/module/recent_products.php с кодом ниже.

    Код:
    <?php
    // admin/controller/module/recent_products.php
    class ControllerModuleRecentProducts extends Controller {
    private $error = array();
    public function index() {
    $this->load->language('module/recent_products');
    $this->document->setTitle($this->language->get('heading_title'));
    $this->load->model('extension/module');
    if (( $this->request->server['REQUEST_METHOD'] == 'POST' ) && $this->validate()) {
    if (!isset( $this->request->get['module_id'] )) {
    $this->model_extension_module->addModule( 'recent_products', $this->request->post );
    } else {
    $this->model_extension_module->editModule( $this->request->get['module_id'], $this->request->post );
    }
    $this->session->data['success'] = $this->language->get('text_success');
    $this->response->redirect( $this->url->link( 'extension/module', 'token=' . $this->session->data['token'], 'SSL' ) );
    }
    $data['heading_title'] = $this->language->get('heading_title');
    $data['text_edit'] = $this->language->get('text_edit');
    $data['text_enabled'] = $this->language->get('text_enabled');
    $data['text_disabled'] = $this->language->get('text_disabled');
    $data['entry_name'] = $this->language->get('entry_name');
    $data['entry_limit'] = $this->language->get('entry_limit');
    $data['entry_status'] = $this->language->get('entry_status');
    $data['button_save'] = $this->language->get('button_save');
    $data['button_cancel'] = $this->language->get('button_cancel');
    if (isset($this->error['warning'])) {
    $data['error_warning'] = $this->error['warning'];
    } else {
    $data['error_warning'] = '';
    }
    if (isset($this->error['name'])) {
    $data['error_name'] = $this->error['name'];
    } else {
    $data['error_name'] = '';
    }
    $data['breadcrumbs'] = array();
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('text_home'),
    'href' => $this->url->link( 'common/dashboard', 'token=' . $this->session->data['token'], 'SSL' )
    );
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('text_module'),
    'href' => $this->url->link( 'extension/module', 'token=' . $this->session->data['token'], 'SSL' )
    );
    if (!isset($this->request->get['module_id'])) {
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('heading_title'),
    'href' => $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'], 'SSL' )
    );
    } else {
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('heading_title'),
    'href' => $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL' )
    );
    }
    if (!isset($this->request->get['module_id'])) {
    $data['action'] = $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'], 'SSL' );
    } else {
    $data['action'] = $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL' );
    }
    $data['cancel'] = $this->url->link( 'extension/module', 'token=' . $this->session->data['token'], 'SSL' );
    if (isset( $this->request->get['module_id']) && ( $this->request->server['REQUEST_METHOD'] != 'POST' ) ) {
    $module_info = $this->model_extension_module->getModule( $this->request->get['module_id'] );
    }
    if (isset($this->request->post['name'])) {
    $data['name'] = $this->request->post['name'];
    } elseif (!empty($module_info)) {
    $data['name'] = $module_info['name'];
    } else {
    $data['name'] = '';
    }
    if (isset($this->request->post['limit'])) {
    $data['limit'] = $this->request->post['limit'];
    } elseif (!empty($module_info)) {
    $data['limit'] = $module_info['limit'];
    } else {
    $data['limit'] = 5;
    }
    if (isset($this->request->post['status'])) {
    $data['status'] = $this->request->post['status'];
    } elseif (!empty($module_info)) {
    $data['status'] = $module_info['status'];
    } else {
    $data['status'] = '';
    }
    $data['header'] = $this->load->controller('common/header');
    $data['column_left'] = $this->load->controller('common/column_left');
    $data['footer'] = $this->load->controller('common/footer');
    $this->response->setOutput( $this->load->view( 'module/recent_products.tpl', $data ) );
    }
    protected function validate() {
    if ( !$this->user->hasPermission( 'modify', 'module/recent_products' ) ) {
    $this->error['warning'] = $this->language->get('error_permission');
    }
    if ((utf8_strlen($this->request->post['name']) < 3) || (utf8_strlen($this->request->post['name']) > 64)) {
    $this->error['name'] = $this->language->get('error_name');
    }
    return !$this->error;
    }
    }
    В коде создается новый класс для пользовательского плагина, который является дочерним для базового класса Controller. Судя по коду, название класса должно дублировать структуру папок к этому файлу. Путь controller/module/recent_products.php превращается в ControllerModuleRecentProducts (опускаем слэши, а первые буквы записываем в верхнем регистре).

    Далее идет метод по умолчанию index, который вызывается сразу после загрузки плагина на стороне front-end’а. В этом методе прописана основная логика плагина.

    В контексте текущего приложения сокращение $this->load->language загружает соответствующий файл языка. В нашем случае загружается файл, заданный в предыдущем разделе. Синтаксис довольно прост – нужно передать название плагина с префиксом module/. Переменные языка доступны через метод $this->language->get. Далее задается заголовок страницы при помощи метода setTitle объекта document.

    Идем дальше, сокращение $this->load->model используется для загрузки модели модуля. Модель представляет собой класс с методами, с помощью которых можно хранить параметры модуля.

    Далее идет важный кусок кода, который проверяет, чтобы данные были поданы в POST формате. Если все правильно, по условию сохраняются настройки модуля.

    Код:
    if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {
    if (!isset($this->request->get['module_id'])) {
    $this->model_extension_module->addModule( 'recent_products', $this->request->post );
    } else {
    $this->model_extension_module->editModule( $this->request->get['module_id'], $this->request->post );
    }
    $this->session->data['success'] = $this->language->get('text_success');
    $this->response->redirect( $this->url->link( 'extension/module', 'token=' . $this->session->data['token'], 'SSL' ) );
    }
    Далее мы записываем языковые лейблы типа heading_title и text_edit в массив $data, чтобы потом использовать их в файле шаблона. Следом идет кусок кода, отвечающий за создание хлебных крошек на странице настроек.

    Код:
    $data['breadcrumbs'] = array();
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('text_home'),
    'href' => $this->url->link( 'common/dashboard', 'token=' . $this->session->data['token'], 'SSL' )
    );
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('text_module'),
    'href' => $this->url->link( 'extension/module', 'token=' . $this->session->data['token'], 'SSL' )
    );
    if (!isset($this->request->get['module_id'])) {
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('heading_title'),
    'href' => $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'], 'SSL' )
    );
    } else {
    $data['breadcrumbs'][] = array(
    'text' => $this->language->get('heading_title'),
    'href' => $this->url->link( 'module/recent_products', 'token=' . $this->session->data['token'] . '&module_id=' . $this->request->get['module_id'], 'SSL' )
    );
    }
    Если бы модуль уже был настроен и находился в режиме редактирования, следующий кусок кода выставил бы настройки по умолчанию.

    Код:
    if ( isset( $this->request->get['module_id'] ) && ( $this->request->server['REQUEST_METHOD'] != 'POST' ) ) {
    $module_info = $this->model_extension_module->getModule( $this->request->get['module_id'] );
    }
    И наконец, мы загружаем такие элементы, как хедер, футер и левый сайдбар. За это отвечает сокращение $this->load->view, которое загружает сам файл представления recent_products.tpl и показывает форму настроек.

    В файле контроллера нужно запомнить пару важных моментов. В файле можно часто встретить запись типа $this->load->ELEMENT, где ELEMENT может быть представление, модель или язык. Этот вызов загружает соответствующий вид, модель и язык компонентов.

    Следующий и последний файл на сегодня – шаблон вида admin/view/template/module/recent_products.tpl. Давайте создадим его!

    Код:
    <!-- admin/view/template/module/recent_products.tpl -->
    <?php echo $header; ?><?php echo $column_left; ?>
    <div id="content">
      <div class="page-header">
    <div class="container-fluid">
    <div class="pull-right">
    <button type="submit" form="form-recent-products" data-toggle="tooltip" title="<?php echo $button_save; ?>" class="btn btn-primary"><i class="fa fa-save"></i></button>
    <a href="<?php echo $cancel; ?>" data-toggle="tooltip" title="<?php echo $button_cancel; ?>" class="btn btn-default"><i class="fa fa-reply"></i></a></div>
    <h1><?php echo $heading_title; ?></h1>
    <ul class="breadcrumb">
    <?php foreach ($breadcrumbs as $breadcrumb) { ?>
    <li><a href="<?php echo $breadcrumb['href']; ?>"><?php echo $breadcrumb['text']; ?></a></li>
    <?php } ?>
    </ul>
    </div>
      </div>
      <div class="container-fluid">
    <?php if ($error_warning) { ?>
    <div class="alert alert-danger"><i class="fa fa-exclamation-circle"></i> <?php echo $error_warning; ?>
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    </div>
    <?php } ?>
    <div class="panel panel-default">
    <div class="panel-heading">
    <h3 class="panel-title"><i class="fa fa-pencil"></i> <?php echo $text_edit; ?></h3>
    </div>
    <div class="panel-body">
    <form action="<?php echo $action; ?>" method="post" enctype="multipart/form-data" id="form-recent-products" class="form-horizontal">
    <div class="form-group">
    <label class="col-sm-2 control-label" for="input-name"><?php echo $entry_name; ?></label>
    <div class="col-sm-10">
    <input type="text" name="name" value="<?php echo $name; ?>" placeholder="<?php echo $entry_name; ?>" id="input-name" class="form-control" />
    <?php if ($error_name) { ?>
    <div class="text-danger"><?php echo $error_name; ?></div>
    <?php } ?>
    </div>
    </div>
    <div class="form-group">
    <label class="col-sm-2 control-label" for="input-limit"><?php echo $entry_limit; ?></label>
    <div class="col-sm-10">
    <input type="text" name="limit" value="<?php echo $limit; ?>" placeholder="<?php echo $entry_limit; ?>" id="input-limit" class="form-control" />
    </div>
    </div>
    <div class="form-group">
    <label class="col-sm-2 control-label" for="input-status"><?php echo $entry_status; ?></label>
    <div class="col-sm-10">
    <select name="status" id="input-status" class="form-control">
    <?php if ($status) { ?>
    <option value="1" selected="selected"><?php echo $text_enabled; ?></option>
    <option value="0"><?php echo $text_disabled; ?></option>
    <?php } else { ?>
    <option value="1"><?php echo $text_enabled; ?></option>
    <option value="0" selected="selected"><?php echo $text_disabled; ?></option>
    <?php } ?>
    </select>
    </div>
    </div>
    </form>
    </div>
    </div>
      </div>
    </div>
    <?php echo $footer; ?>
    Пользователи с хорошим зрением уже заметили, что в файле просто отображаются переменные из контроллера. В остальном это простой XHTML код с формой настроек. Самая главная фишка в том, что код полностью адаптивный прямо из коробки. С созданием файлов для нашего back-end плагина закончили.

    Включаем плагин
    Перейдите в панель администратора OpenCart в Extensions > Modules. В списке должен быть плагин Recent Products. Для установки модуля кликните на +, как показано на скриншоте ниже.

    1.png

    После установки появится иконка редактирования. Кликните по ней для открытия формы настроек модуля.

    2.png

    В форме конфигурации можно задать недавние продукты, которые должны показываться в блоке на стороне front-end’а. Не забудьте установить поле status в Enabled! Сохраните модуль, он должен выглядеть вот так.

    3.png

    Появилась новая строка Recent Products > My Recent Block Plugin. То есть модуль можно дублировать под разные страницы! Почти закончили! Мы создали полноценный back-end плагин в OpenCart. В следующей части мы разберем front-end сторону вопроса, которая отвечает за показ блока с товарами!

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

    В следующей части мы завершим разработку модуля и создадим front-end составляющую, которая будет показывать список товаров. Свои вопросы и предложения пишите в комментариях.
     
  2. Offline

    DaSS Пользователь

    Сообщения:
    57
    Симпатии:
    39
    Репутация:
    23
    Здравствуйте. Подскажите в чём ошибка.
    Пытаюсь продублировать модуль доставки по городу (citylink). Файлы продублировал, модуль появился, но не работает сохранение.
    Листинг файла контроллера
    PHP:
    <?php
    class ControllerExtensionShippingcitylink1 extends Controller {
        private 
    $error = array();

        public function 
    index() {
            
    $this->load->language('extension/shipping/citylink1');

            
    $this->document->setTitle($this->language->get('heading_title'));

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

            if ((
    $this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {
                
    $this->model_setting_setting->editSetting('citylink1'$this->request->post);

                
    $this->session->data['success'] = $this->language->get('text_success');

                
    $this->response->redirect($this->url->link('extension/extension''token=' $this->session->data['token'] . '&type=shipping'true));
            }

            
    $data['heading_title'] = $this->language->get('heading_title');
           
            
    $data['text_edit'] = $this->language->get('text_edit');
            
    $data['text_enabled'] = $this->language->get('text_enabled');
            
    $data['text_disabled'] = $this->language->get('text_disabled');
            
    $data['text_all_zones'] = $this->language->get('text_all_zones');
            
    $data['text_none'] = $this->language->get('text_none');

            
    $data['entry_rate'] = $this->language->get('entry_rate');
            
    $data['entry_tax_class'] = $this->language->get('entry_tax_class');
            
    $data['entry_geo_zone'] = $this->language->get('entry_geo_zone');
            
    $data['entry_status'] = $this->language->get('entry_status');
            
    $data['entry_sort_order'] = $this->language->get('entry_sort_order');

            
    $data['help_rate'] = $this->language->get('help_rate');

            
    $data['button_save'] = $this->language->get('button_save');
            
    $data['button_cancel'] = $this->language->get('button_cancel');

            if (isset(
    $this->error['warning'])) {
                
    $data['error_warning'] = $this->error['warning'];
            } else {
                
    $data['error_warning'] = '';
            }

            
    $data['breadcrumbs'] = array();

            
    $data['breadcrumbs'][] = array(
                
    'text' => $this->language->get('text_home'),
                
    'href' => $this->url->link('common/dashboard''token=' $this->session->data['token'], true)
            );

            
    $data['breadcrumbs'][] = array(
                
    'text' => $this->language->get('text_extension'),
                
    'href' => $this->url->link('extension/extension''token=' $this->session->data['token'] . '&type=shipping'true)
            );

            
    $data['breadcrumbs'][] = array(
                
    'text' => $this->language->get('heading_title'),
                
    'href' => $this->url->link('extension/shipping/citylink1''token=' $this->session->data['token'], true)
            );

            
    $data['action'] = $this->url->link('extension/shipping/citylink1''token=' $this->session->data['token'], true);

            
    $data['cancel'] = $this->url->link('extension/extension''token=' $this->session->data['token'] . '&type=shipping'true);

            if (isset(
    $this->request->post['citylink1_rate'])) {
                
    $data['citylink1_rate'] = $this->request->post['citylink1_rate'];
            } elseif (
    $this->config->get('citylink1_rate')) {
                
    $data['citylink1_rate'] = $this->config->get('citylink1_rate');
            } else {
                
    $data['citylink1_rate'] = '10:11.6,15:14.1,20:16.60,25:19.1,30:21.6,35:24.1,40:26.6,45:29.1,50:31.6,55:34.1,60:36.6,65:39.1,70:41.6,75:44.1,80:46.6,100:56.6,125:69.1,150:81.6,200:106.6';
            }

            if (isset(
    $this->request->post['citylink1_tax_class_id'])) {
                
    $data['citylink1_tax_class_id'] = $this->request->post['citylink1_tax_class_id'];
            } else {
                
    $data['citylink1_tax_class_id'] = $this->config->get('citylink1_tax_class_id');
            }

            
    $this->load->model('localisation/tax_class');

            
    $data['tax_classes'] = $this->model_localisation_tax_class->getTaxClasses();

            if (isset(
    $this->request->post['citylink1_geo_zone_id'])) {
                
    $data['citylink1_geo_zone_id'] = $this->request->post['citylink1_geo_zone_id'];
            } else {
                
    $data['citylink1_geo_zone_id'] = $this->config->get('citylink1_geo_zone_id');
            }

            
    $this->load->model('localisation/geo_zone');

            
    $data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones();

            if (isset(
    $this->request->post['citylink1_status'])) {
                
    $data['citylink1_status'] = $this->request->post['citylink1_status'];
            } else {
                
    $data['citylink1_status'] = $this->config->get('citylink1_status');
            }

            if (isset(
    $this->request->post['citylink1_sort_order'])) {
                
    $data['citylink1_sort_order'] = $this->request->post['citylink1_sort_order'];
            } else {
                
    $data['citylink1_sort_order'] = $this->config->get('citylink1_sort_order');
            }

            
    $data['header'] = $this->load->controller('common/header');
            
    $data['column_left'] = $this->load->controller('common/column_left');
            
    $data['footer'] = $this->load->controller('common/footer');

            
    $this->response->setOutput($this->load->view('extension/shipping/citylink1'$data));
        }

        protected function 
    validate() {
            if (!
    $this->user->hasPermission('modify''extension/shipping/citylink1')) {
                
    $this->error['warning'] = $this->language->get('error_permission');
            }

            return !
    $this->error;
        }
    }
     
  3. Offline

    DaSS Пользователь

    Сообщения:
    57
    Симпатии:
    39
    Репутация:
    23
    Разобрался. Проблема была с файлом .tpl админки