Створення модуля: покроково
Що ми створимо#
У цьому посібнику ми з нуля напишемо модуль «Привітальний блок» (hello_block) — блок із заголовком і текстом, який можна розмістити в будь-якій позиції макета (ліва чи права колонка, верх або низ сторінки). На відміну від прикладу з розділу Доповнення, де модуль має лише глобальний статус і працює через події, тут модуль:
- має кілька екземплярів (наприклад, один блок для головної, інший — для категорій);
- зберігає налаштування кожного екземпляра в таблиці
oc_module; - підтримує кілька мов вітрини;
- виводиться на вітрині через систему макетів.
Саме так улаштовані стандартні модулі ocStore на кшталт «HTML контент», «Банер» чи «Рекомендовані».
Ознайомтеся з розділами Архітектура MVC-L (маршрути, завантажувач, шаблони) та Доповнення (типи доповнень, структура пакета). Тут ми не повторюємо теорію, а зосереджуємося на коді.
Структура файлів#
hello_block/
└── upload/
├── admin/
│ ├── controller/extension/module/hello_block.php
│ ├── language/uk-ua/extension/module/hello_block.php
│ ├── language/en-gb/extension/module/hello_block.php
│ └── view/template/extension/module/hello_block.twig
└── catalog/
├── controller/extension/module/hello_block.php
├── language/uk-ua/extension/module/hello_block.php
├── language/en-gb/extension/module/hello_block.php
└── view/theme/default/template/extension/module/hello_block.twig
Зверніть увагу на кілька правил:
- Код модуля (
hello_block) — це назва файлів. Пишіть його латиницею в нижньому регістрі, слова розділяйте_. - Маршрут модуля в адмінці та на вітрині —
extension/module/hello_block. - Клас контролера:
ControllerExtensionModuleHelloBlock(у адмінці й на вітрині — однаково, бо це різні застосунки). - Шаблон вітрини кладіть у тему
default: якщо в активній темі немає власного файлу, ocStore автоматично візьме його зdefault.
Крок 1. Контролер адмінки#
Файл: upload/admin/controller/extension/module/hello_block.php
Контролер показує форму налаштувань екземпляра, перевіряє дані та зберігає їх через модель setting/module.
<?php
class ControllerExtensionModuleHelloBlock extends Controller {
private $error = array();
public function index() {
$this->load->language('extension/module/hello_block');
$this->document->setTitle($this->language->get('heading_title'));
$this->load->model('setting/module');
// Збереження форми
if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {
if (!isset($this->request->get['module_id'])) {
// Новий екземпляр модуля
$this->model_setting_module->addModule('hello_block', $this->request->post);
} else {
// Редагування наявного екземпляра
$this->model_setting_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('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true));
}
// Помилки
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'] = '';
}
// Посилання
$url = '';
if (isset($this->request->get['module_id'])) {
$url .= '&module_id=' . (int)$this->request->get['module_id'];
}
$data['breadcrumbs'] = array();
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/module/hello_block', 'user_token=' . $this->session->data['user_token'] . $url, true)
);
$data['action'] = $this->url->link('extension/module/hello_block', 'user_token=' . $this->session->data['user_token'] . $url, true);
$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true);
// Дані наявного екземпляра
if (isset($this->request->get['module_id']) && ($this->request->server['REQUEST_METHOD'] != 'POST')) {
$module_info = $this->model_setting_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['module_description'])) {
$data['module_description'] = $this->request->post['module_description'];
} elseif (!empty($module_info['module_description'])) {
$data['module_description'] = $module_info['module_description'];
} else {
$data['module_description'] = array();
}
if (isset($this->request->post['status'])) {
$data['status'] = $this->request->post['status'];
} elseif (!empty($module_info)) {
$data['status'] = $module_info['status'];
} else {
$data['status'] = '';
}
// Мови для вкладок форми
$this->load->model('localisation/language');
$data['languages'] = $this->model_localisation_language->getLanguages();
$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/hello_block', $data));
}
protected function validate() {
if (!$this->user->hasPermission('modify', 'extension/module/hello_block')) {
$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;
}
}
Що тут важливо:
addModule($code, $data)іeditModule($module_id, $data)зберігають увесь масив$this->request->postу JSON-поліsettingтаблиціoc_module. Полеnameзаписується ще й в окрему колонку — це назва екземпляра, яку видно в списку модулів і в макетах.- Поле
statusобов'язкове для модулів з екземплярами: вітрина показує екземпляр, лише якщо в його налаштуванняхstatusдорівнює1. - Методи
install()таuninstall()тут не потрібні: під час активації/деактивації ocStore сам додає модуль у таблицюoc_extension, видає й прибирає права, а під час деактивації видаляє всі екземпляри модуля та їх розміщення в макетах. Додавайте ці методи, лише якщо модулю потрібні власні таблиці чи події.
Крок 2. Мовні файли адмінки#
Файл: upload/admin/language/uk-ua/extension/module/hello_block.php
<?php
// Heading
$_['heading_title'] = 'Привітальний блок';
// Text
$_['text_extension'] = 'Доповнення';
$_['text_success'] = 'Налаштування модуля збережено!';
$_['text_edit'] = 'Редагування модуля';
// Entry
$_['entry_name'] = 'Назва модуля';
$_['entry_title'] = 'Заголовок';
$_['entry_text'] = 'Текст';
$_['entry_status'] = 'Статус';
// Error
$_['error_permission'] = 'У вас недостатньо прав для зміни модуля «Привітальний блок»!';
$_['error_name'] = 'Назва модуля має містити від 3 до 64 символів!';
Файл: upload/admin/language/en-gb/extension/module/hello_block.php
<?php
// Heading
$_['heading_title'] = 'Hello block';
// Text
$_['text_extension'] = 'Extensions';
$_['text_success'] = 'Success: You have modified Hello block module!';
$_['text_edit'] = 'Edit Hello block module';
// Entry
$_['entry_name'] = 'Module name';
$_['entry_title'] = 'Title';
$_['entry_text'] = 'Text';
$_['entry_status'] = 'Status';
// Error
$_['error_permission'] = 'Warning: You do not have permission to modify Hello block module!';
$_['error_name'] = 'Module name must be between 3 and 64 characters!';
Загальні рядки (text_home, text_enabled, text_disabled, button_save, button_cancel) уже є в головному мовному файлі адмінки, тож оголошувати їх повторно не потрібно. Усі завантажені мовні рядки автоматично доступні в шаблоні.
Крок 3. Шаблон адмінки#
Файл: upload/admin/view/template/extension/module/hello_block.twig
Адмінка ocStore 3 використовує Bootstrap 3 і Font Awesome 4.
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="pull-right">
<button type="submit" form="form-module" data-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fa fa-save"></i></button>
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i class="fa fa-reply"></i></a></div>
<h1>{{ heading_title }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
{% if error_warning %}
<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> {{ error_warning }}
<button type="button" class="close" data-dismiss="alert">×</button>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
</div>
<div class="panel-body">
<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-module" class="form-horizontal">
<div class="form-group required">
<label class="col-sm-2 control-label" for="input-name">{{ entry_name }}</label>
<div class="col-sm-10">
<input type="text" name="name" value="{{ name }}" placeholder="{{ entry_name }}" id="input-name" class="form-control" />
{% if error_name %}
<div class="text-danger">{{ error_name }}</div>
{% endif %}
</div>
</div>
<ul class="nav nav-tabs" id="language">
{% for language in languages %}
<li><a href="#language{{ language.language_id }}" data-toggle="tab"><img src="language/{{ language.code }}/{{ language.code }}.png" title="{{ language.name }}" /> {{ language.name }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for language in languages %}
<div class="tab-pane" id="language{{ language.language_id }}">
<div class="form-group">
<label class="col-sm-2 control-label" for="input-title{{ language.language_id }}">{{ entry_title }}</label>
<div class="col-sm-10">
<input type="text" name="module_description[{{ language.language_id }}][title]" value="{{ module_description[language.language_id] ? module_description[language.language_id].title }}" placeholder="{{ entry_title }}" id="input-title{{ language.language_id }}" class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-text{{ language.language_id }}">{{ entry_text }}</label>
<div class="col-sm-10">
<textarea name="module_description[{{ language.language_id }}][text]" rows="5" placeholder="{{ entry_text }}" id="input-text{{ language.language_id }}" class="form-control">{{ module_description[language.language_id] ? module_description[language.language_id].text }}</textarea>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-status">{{ entry_status }}</label>
<div class="col-sm-10">
<select name="status" id="input-status" class="form-control">
{% if status %}
<option value="1" selected="selected">{{ text_enabled }}</option>
<option value="0">{{ text_disabled }}</option>
{% else %}
<option value="1">{{ text_enabled }}</option>
<option value="0" selected="selected">{{ text_disabled }}</option>
{% endif %}
</select>
</div>
</div>
</form>
</div>
</div>
</div>
<script type="text/javascript"><!--
$('#language a:first').tab('show');
//--></script></div>
{{ footer }}
Тексти зберігаються в масиві module_description, де ключ — language_id мови. Так само зберігає дані стандартний модуль «HTML контент».
Крок 4. Контролер вітрини#
Файл: upload/catalog/controller/extension/module/hello_block.php
Коли макет сторінки містить екземпляр модуля, ocStore викликає метод index() і передає йому масив налаштувань цього екземпляра (той самий, що зберегла форма в адмінці).
<?php
class ControllerExtensionModuleHelloBlock extends Controller {
public function index($setting) {
$language_id = (int)$this->config->get('config_language_id');
// Немає тексту для поточної мови — нічого не виводимо
if (empty($setting['module_description'][$language_id]['text'])) {
return '';
}
$this->load->language('extension/module/hello_block');
$description = $setting['module_description'][$language_id];
if (!empty($description['title'])) {
$data['title'] = $description['title'];
} else {
$data['title'] = $this->language->get('heading_title');
}
$data['text'] = nl2br($description['text']);
return $this->load->view('extension/module/hello_block', $data);
}
}
- Метод повертає HTML (
return), а не виводить його через$this->response->setOutput(): результат вставляє в сторінку контролер позиції (common/column_left,common/content_topтощо). - Дані форми потрапили в базу через
$this->request->post, тобто вже пропущені черезhtmlspecialchars(). Тому текст можна безпечно виводити в шаблоні як є.
Крок 5. Мовні файли вітрини#
Файл: upload/catalog/language/uk-ua/extension/module/hello_block.php
<?php
// Heading
$_['heading_title'] = 'Вітаємо!';
Файл: upload/catalog/language/en-gb/extension/module/hello_block.php
<?php
// Heading
$_['heading_title'] = 'Welcome!';
Цей рядок використовується як заголовок за замовчуванням, якщо для екземпляра не вказано власного.
Крок 6. Шаблон вітрини#
Файл: upload/catalog/view/theme/default/template/extension/module/hello_block.twig
<div class="hello-block">
{% if title %}
<h3>{{ title }}</h3>
{% endif %}
<p>{{ text }}</p>
</div>
Якщо ваша тема потребує іншої розмітки, створіть власну копію файлу за шляхом catalog/view/theme/<ваша_тема>/template/extension/module/hello_block.twig — вона матиме пріоритет над файлом з default.
Крок 7. Пакування#
- Переконайтеся, що структура файлів відповідає наведеній вище.
- Заархівуйте вміст теки
hello_block/(тобто саму текуupload/) у ZIP з назвоюhello_block.ocmod.zip. Відкривши архів, ви маєте одразу побачитиupload/. - Назва архіву обов'язково має закінчуватися на
.ocmod.zip, інакше інсталятор його не прийме.
Модулю не потрібен install.xml, бо він не змінює файлів ядра. Якщо колись знадобиться — див. Система модифікацій OCMOD.
Під час розробки зручніше не перепаковувати архів після кожної правки, а одразу скопіювати файли з upload/ у відповідні теки тестового магазину через FTP. ZIP знадобиться для фінальної перевірки й поширення.
Крок 8. Встановлення та перевірка#
- Завантажте пакет. Відкрийте Доповнення → Установка доповнень, натисніть Завантажити й виберіть
hello_block.ocmod.zip. Дочекайтеся завершення. - Активуйте модуль. Перейдіть у Доповнення → Доповнення, у списку «Оберіть тип доповнення» виберіть Модулі, знайдіть «Привітальний блок» і натисніть зелену кнопку Активація (
+). - Створіть екземпляр. Натисніть синю кнопку навпроти модуля (Редагувати з олівцем; коли екземпляри вже існують, на її місці кнопка Додати з
+), заповніть назву (наприклад, «Привітання на головній»), заголовок і текст для кожної мови, встановіть статус «Включено» та збережіть. Екземпляр з'явиться в списку під назвою модуля. Повторіть, якщо потрібно кілька різних блоків. - Розмістіть модуль у макеті. Відкрийте Дизайн → Макети, відредагуйте потрібний макет (наприклад, «Головна»), у бажаній позиції («Ліва колонка», «Верх сторінки» тощо) виберіть у списку екземпляр «Привітання на головній» (він згрупований під назвою модуля «Привітальний блок»), натисніть
+і збережіть. Докладніше — у розділі Макети. - Перевірте вітрину. Відкрийте сторінку, до якої застосовано макет. Якщо змін не видно, очистіть кеш шаблонів: Панель стану → кнопка з шестернею («Налаштування розробника») → Шаблон.
Права доступу#
Під час активації ocStore автоматично додає дозволи на перегляд (access) і редагування (modify) маршруту extension/module/hello_block лише групі поточного користувача. Іншим групам адміністраторів доступ треба надати вручну:
- Відкрийте Система → Користувачі → Групи користувачів і відредагуйте групу.
- Позначте
extension/module/hello_blockу списках Дозволено перегляд і Дозволено редагування. - Збережіть.
У тій самій формі є список Приховати доповнення — позначені там доповнення не показуються цій групі в списку доповнень.
Без дозволу на перегляд адмінка покаже сторінку «Доступ заборонений», без дозволу на зміну — помилку error_permission під час збереження форми. Під час деактивації модуля ці дозволи прибираються автоматично.
Типові проблеми#
Модуля немає в списку «Модулі»
Перевірте шлях і назву файлу контролера адмінки: admin/controller/extension/module/hello_block.php. ocStore будує список модулів саме за файлами в цій теці. Також переконайтеся, що модуль не приховано в налаштуваннях прав групи користувачів.
Після збереження форми з'являється порожня сторінка або помилка «Could not call»
Найчастіше це помилка в назві класу. Для файлу extension/module/hello_block.php клас має називатися ControllerExtensionModuleHelloBlock. Подробиці помилки дивіться в Система → Обслуговування → Журнал помилок.
Модуль розміщено в макеті, але на вітрині його не видно
Перевірте, що: статус екземпляра — «Включено»; для поточної мови вітрини заповнено поле «Текст»; макет справді застосовується до відкритої сторінки (маршрут у налаштуваннях макета); шаблон лежить у catalog/view/theme/default/template/extension/module/. Після змін очистіть кеш шаблонів.
На вітрині замість тексту видно ключі на кшталт heading_title
Мовний файл вітрини не знайдено. Перевірте шлях catalog/language/<код мови>/extension/module/hello_block.php і що контролер викликає $this->load->language('extension/module/hello_block').
Що далі#
- Додайте модель
catalog/model/extension/module/hello_block.php(класModelExtensionModuleHelloBlock), якщо модулю потрібні дані з бази — наприклад, список товарів. - Підключіть власні стилі: покладіть CSS у
catalog/view/theme/default/stylesheet/і додайте його в контролері через$this->document->addStyle(). - Вивчіть код стандартних модулів
html,banner,featuredуadmin/controller/extension/module/іcatalog/controller/extension/module/— вони побудовані за тією самою схемою. - Дізнайтеся, як реагувати на дії в магазині, у розділі Події.