Модули в Opencart: Основы

Вводная статья по написанию простых модулей под Opencart

Данная статья не рассматривает OCmod/VQmod, здесь мы поговорим об основах и напишем несложный модуль.

Движок Opencart — это довольно успешный opensource проект на ниве e-commerce решений. Думаю, успех его достигнут, не в последнюю очередь, благодаря экстремально простой архитектуре, заложенной в движок ещё в первых версиях.

По сути, мы имеем MVC+L архитектуру — это уже, практически стандартный, в индустрии Model-View-Controller и Language (не знаю почему, но систему перевода выделяют в отдельную букву).

 

Движок предоставляет два отдельных интерфейса — admin и catalog, соответственно бэкенд/фронтенд сайта (по старым до_javascript_frontend_development понятиям), т.е. это два отдельных приложения в одном, и большую часть всех манипуляций вы, как разработчик, будет проводить именно там.

 

Каталог system в корневой директории содержит все вспомогательные элементы Opencart, его базовый код, а также набор библиотек (system/library), куда вы можете добавлять необходимые для работы библиотеки.

Файловая структура Opencart 2

 

Обратите внимание

На данный момент, в дикой природе существуют и функционируют три основные версии Opencart — 1.5.6, 2.3, 3.0.2.0.

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

 

Реестр Opencart (Registry)

 

Реестр в Opencart, это основа основ всего в этом движке, он представляет собой реализацию Dependency Injection паттерна и агрегирует в себе всё, что вы используете стандартными способами. Мы детально рассмотрим внутреннюю кухню Opencart в одной из следующих статей, а пока просто примем тот факт, что вокруг registry построено всё в Opencart — от загрузчика до контроллеров.

 

Проксирование (Proxy)

 

Прокси устроен достаточно просто — все методы проксируемого объекта копируются в объект прокси. В классе реализованы магические методы __set, __get, __call, определяющие поведение объекта (отличий от стандартного поведения фактически никакого), сама концепция прокси, вероятно, внедрена исключительно для поддержки моделями event handler системы (в Opencart 1.5 этой концепции не было, как и Proxy).

Opencart использует проксирование ТОЛЬКО для моделей. 

Танцы с Proxy

Загрузчик Opencart передаёт в Proxy ТОЛЬКО методы!

Это может сыграть с вами злую шутку, если вы хотите использовать предопределённые переменные класса или объекта, через self/$this.

Поскольку проксированный объект использует __get/__set завязанные на Registry, вы не сможете получить доступ к своим изначальным переменным в классе нигде, за исключением конструктора!

 

Загрузчик классов

 

Opencart использует собственный загрузчик классов — хоть это и возможно, но не рекомендуется прямое подключение PHP файлов (include/require), что налагает свои требования по именованию классов.

 

Этот загрузчик устроен довольно просто — он предоставляет несколько методов для реализации разного поведения, при загрузке разных типов классов, например метод controller попытается выполнить метод контроллера по переданному роуту, словно его запрашивает пользователь:

<?php
// выполнит метод index у контроллера example и вернет результат
$this->load->controller('extension/module/example/index');

 

А вот при загрузке модели — она просто будет проксирована и добавлена в Registry:

<?php 
$this->load->model('extension/module/example');
// теперь проксированная модель доступна к использованию:
$this->model_extension_module_example->hello();

 

view — попытается отрендерить представление, language загружает языковой пакет, library — загружает и инициализирует библиотеку подобно модели, но без префиксов (system/library/*), helper подключит скрипт с вспомогательными функциями (system/helper)

 

Директории модуля

 

Директории модуля полностью повторяют директории сайта — Opencart просто поместит ваши файлы внутрь себя. Это означает что вы можете устанавливать ocmod.zip файлы через установщик, или просто копировать файлы модулей в соответствующие директории сайта.

 

К примеру, давайте напишем простенький модуль "example". Для этого нам необходимо создать следующую иерархию каталогов:

upload/admin/controller/extension/module
upload/admin/view/template/extension/module
upload/admin/language/ru-ru/extension/module
upload/catalog/controller/extension/module
upload/catalog/view/theme/default/extension/module

 

Наш модуль не будет делать ничего особенного, всего лишь выводить текст "Hello from example module" в том месте, куда вы его вставите в настройках шаблонов (дизайн → макеты).

Обратите внимание!

Если вы собираетесь распространять модуль в виде ocmod.zip файла — корневым каталогом в архиве должен быть upload!

 

Бэкенд переводы

 

Переводы Opencart хранятся в каталоге languages, они довольно примитивны и представляют собой PHP файлы содержащие массив $_. В этом массиве прописываются ключи-значения, и, собственно, всё!

В каждой языковой директории имеется файл с названием кода языка (например, languages/ru-ru/ru-ru.php), в котором содержатся общесистемные переводы, а также региональные настройки.

Обратите внимание!

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

 

Каждый модуль Opencart должен обладать одноимённым файлом переводов, в котором необходимо наличие секции heading_title, значение этой секции используется для вывода названия модуля в списке модулей:

<?php
// admin/languages/ru-ru/extension/module/example.php
$_['heading_title'] = 'Пример модуля';

$_['entry_status'] = 'Статус';

Обратите внимание!

В случае отсутствия этой секции, вы рискуете получить дублирующиеся имена модулей в списке, а в случае отсутствия языкового файла модуля — ошибку PHP!

 

Бэкенд контроллер

 

Настало время разработки контроллера для админки. Для этого создадим файл example.php в директории admin/controller/extension/module:

<?php

class ControllerExtensionModuleExample extends Controller {

}

 

Обратите внимание на префикс ControllerExtensionModule в названии нашего класса — Controller даёт загрузчику понять что он имеет дело с контроллером, ExtensionModule — что класс лежит в директории extension/module.

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

 

Теперь давайте создадим свой action с названием index. Этот метод можно считать входной функцией модуля (как функция main в C):

<?php

class ControllerExtensionModuleExample extends Controller {
  // index - выполняется по умолчанию если в url не указан конкретный action!
  public function index () {
    // массив переменных для представления
    $data = array();
    // загружаем языковой пакет
    $this->load->language('extension/module/example');
    // устанавливаем заголовок окна
    $this->document->setTitle($this->language->get('heading_title'));

    // загружаем модель setting
    $this->load->model('setting/setting');

    // если от пользователя пришёл POST запрос
    if ($this->request->server['REQUEST_METHOD'] == 'POST') {
      // заполняем настройки модуля из него
      $this->model_setting_setting->editSetting('example', $this->request->post);
      // и редиректим пользователя к списку модулей
      $this->response->redirect($this->url->link('extension/extension', 'token=' . $this->session->data['token'] . '&type=module', true));
    }

    // устанавливаем переменные представления
    $data['heading_title'] = $this->language->get('heading_title');
    // эти переводы уже подгружены Opencart из ru-ru.php:
    $data['text_enabled'] = $this->language->get('text_enabled');
    $data['text_disabled'] = $this->language->get('text_disabled');
    $data['entry_status'] = $this->language->get('entry_status');

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

    // устанавливаем текущий статус модуля
    if (isset($this->request->post['example_status'])) {
      $data['example_status'] = $this->request->post['example_status'];
    }
    else {
      $data['example_status'] = $this->config->get('example_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('extension/module/example.tpl', $data));
  }

}

 

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

 

Эти контроллеры отличаются тем, что не выводят результат рендеринга в Response, а просто возвращают подготовленный view (return $this->load->view...). Чуть позже мы будем использовать эту концепцию при разработке фронтенд контроллера.

 

Бэкенд представление

 

Opencart не использовал внешних шаблонизаторов вплоть до третьей версии (там прикрутили twig), и по сути шаблоны, это такая мешанина html + php. У такой связки есть только один минус — она позволяет говнокодить и использовать php в шаблоне не по назначению.

Давайте создадим шаблон для нашего модуля — view/template/extension/module/example.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-example" class="btn btn-primary">
          <i class="fa fa-save"></i>
        </button>
      </div>
      <h1><?php echo $heading_title; ?></h1>
    </div>
  </div>
  <div class="container-fluid">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><?php echo $heading_title; ?></h3>
      </div>
      <div class="panel-body">
        <form id="form-example" action="<?php echo $action; ?>" method="post" enctype="multipart/form-data">
          <div class="form-group">
            <label class="col-sm-2 control-label" for="example_status"><?php echo $entry_status; ?></label>
            <div class="col-sm-10">
              <select name="example_status" id="example_status" class="form-control">
              <?php if ($example_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 endif; ?>
              </select>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
<?php echo $footer; ?>

 

Отлично!

Административная часть модуля готова, давайте протестируем её. Для этого можно пойти двумя путями — либо просто скопировать содержимое папки upload в корень установленного Opencart, либо упаковать всю папку upload в ZIP архив с названием <имя_модуля>.ocmod.zip и установить модуль штатным установщиком Opencart.

 

После установки нашего, бесполезного пока что, модуля, нужно его включить:

 

 

Как только мы включили модуль, он становится доступен для выбора в настройках макетов. Теперь мы с лёгкостью можем добавить его на сайт:

 

 

Однако, вы не увидите никаких изменений на странице!

Это значит — настало время сделать фронтенд часть модуля.

 

Концептуальной разницы между admin/catalog частями модуля почти нет, стоит только упомянуть, что в панели администрирования нет такого понятия как "тема" — там все представления располагаются сразу в папке template. Фронтендная часть модуля подразумевает такую структуру файлов представления: view/theme/default/template/extension/module.

 

Обратите внимание!

Всегда создавайте шаблоны модуля в теме default! Это позволит использовать ваши шаблоны по умолчанию, а также мы посмотрим как дать возможность кастомизации этих шаблонов в темах.

 

Фронтенд контроллер

 

Помните, я упоминал контроллеры, которые возвращают отрендеренные шаблоны, а не устанавливают их в объект Response?

 Для фронтенда мы воспользуемся именно таким способом вывода содержимого − catalog/controller/extension/module/example.php:

<?php

class ControllerExtensionModuleExample extends Controller {

  public function index () {
    $view = 'extension/module/example.tpl';
    // проверяем - существует ли кастомный шаблон в текущей теме
    if (file_exists(DIR_TEMPLATE . $this->config->get('config_template') . '/template/' . $view)) {
      $view = $this->config->get('config_template') . '/template/' . $view;
    }

    return $this->load->view($view);
  }

}

 

При выводе модуля, прикреплённого в макете, Opencart вызывает индексный экшн (метод index). В нём мы проверяем — существует-ли кастомное представление в текущей теме сайта ($this->config->get('config_template')), и если существует то оно и будет загружено, если нет, то будет использовано представление из темы default.

А вот и наше стандартное представление − catalog/view/theme/default/template/extension/module/example.tpl:

<h1>Hello from example module!</h1>

 

Когда вы запакуете и установите обновлённую версию нашего модуля, то получите что−то в таком духе:

 

 

Подведём итог

 

Мы разработали абсолютно бесполезный, но собственный модуль для Opencart 2. Рассмотрели базовые принципы разработки и некоторые подводные камни.

В следующей статье я планирую рассказать о системе OCmod/VQmod, показать что это, зачем и как использовать.

 

Помните — Opencart это свободное программное обеспечение, поэтому вы можете самостоятельно изучать и модифицировать его в своих целях.