Архітектура MVC-L
Огляд#
ocStore 3.0.4.1, як і OpenCart 3.0, побудований за шаблоном MVC-L — Model-View-Controller-Language:
- Model (модель) — робота з даними: запити до бази даних, обчислення, звернення до зовнішніх сервісів.
- View (представлення) — шаблони Twig, що формують HTML.
- Controller (контролер) — логіка сторінки: отримує запит, викликає моделі, готує дані й передає їх у шаблон.
- Language (мова) — мовні файли з усіма текстами інтерфейсу, окремо для кожної мови.
Магазин складається з двох окремих застосунків — вітрини (catalog/) та адмінки (admin/), — які використовують спільне ядро в теці system/.
Структура тек#
/ ← корінь сайту
├── admin/ ← адмінка
│ ├── controller/
│ ├── language/ ← uk-ua/, en-gb/ …
│ ├── model/
│ ├── view/
│ │ ├── image/
│ │ ├── javascript/
│ │ ├── stylesheet/
│ │ └── template/ ← шаблони адмінки (.twig)
│ ├── config.php
│ └── index.php
├── catalog/ ← вітрина
│ ├── controller/
│ ├── language/
│ ├── model/
│ └── view/
│ ├── javascript/
│ └── theme/
│ └── default/ ← тема: template/, stylesheet/, image/
├── image/ ← зображення: catalog/ (оригінали), cache/ (ресайзи)
├── install/ ← інсталятор (видаляється після встановлення)
├── system/ ← спільне ядро
│ ├── config/ ← конфігурація застосунків (default.php, catalog.php, admin.php …)
│ ├── engine/ ← рушій MVC-L
│ ├── helper/ ← допоміжні функції
│ ├── library/ ← бібліотеки (БД, кошик, сесія, пошта …)
│ ├── storage/ ← сховище (рекомендовано перенести за межі кореня сайту)
│ ├── framework.php
│ ├── startup.php
│ └── modification.xml
├── config.php
├── index.php
└── .htaccess.txt
Сховище storage#
Тека storage/ містить службові дані, які не повинні бути доступні з браузера:
| Тека | Призначення |
|---|---|
cache/ |
Файловий кеш даних і скомпільованих шаблонів Twig (cache/template/). |
download/ |
Файли для завантаження (цифрові товари). |
logs/ |
Журнали: помилок PHP і ocmod.log. |
modification/ |
Кеш модифікацій OCMOD (див. Система модифікацій OCMOD). |
session/ |
Файли сесій (якщо використовується файловий обробник). |
upload/ |
Файли, які завантажили покупці, і тимчасові файли інсталятора доповнень. |
vendor/ |
Сторонні бібліотеки Composer, зокрема шаблонізатор Twig. |
Розташування тек задають константи у config.php та admin/config.php: DIR_STORAGE, DIR_CACHE, DIR_DOWNLOAD, DIR_LOGS, DIR_MODIFICATION, DIR_SESSION, DIR_UPLOAD. Як перенести сховище — див. Встановлення.
Основні константи#
| Константа | Значення |
|---|---|
DIR_APPLICATION |
Тека поточного застосунку (catalog/ або admin/). |
DIR_SYSTEM |
Тека system/. |
DIR_IMAGE |
Тека image/. |
DIR_LANGUAGE |
Тека мовних файлів поточного застосунку. |
DIR_TEMPLATE |
Тека шаблонів: catalog/view/theme/ або admin/view/template/. |
DIR_CONFIG |
Тека system/config/. |
DIR_CATALOG |
Тека catalog/ (визначена лише в адмінці). |
HTTP_SERVER, HTTPS_SERVER |
Адреса поточного застосунку. |
DB_DRIVER, DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_DATABASE, DB_PORT, DB_PREFIX |
Параметри підключення до бази даних. |
Маршрути#
Будь-яку сторінку визначає маршрут (route) — шлях до файлу контролера відносно теки controller/ і, за потреби, назва методу:
| URL | Файл контролера | Клас | Метод |
|---|---|---|---|
index.php?route=product/category |
catalog/controller/product/category.php |
ControllerProductCategory |
index() |
index.php?route=checkout/cart/add |
catalog/controller/checkout/cart.php |
ControllerCheckoutCart |
add() |
index.php?route=extension/module/html |
catalog/controller/extension/module/html.php |
ControllerExtensionModuleHTML |
index() |
Правила:
- Сегменти маршруту розділяються косою рискою
/(на відміну від OpenCart 4, де метод відділяється крапкою чи|). - ocStore шукає файл контролера, відкидаючи останні сегменти маршруту: якщо
checkout/cart/add.phpне існує, останній сегмент (add) вважається назвою методу. - Якщо метод не вказано, викликається
index(). - Назва класу — це
Controller+ шлях без/і_. Регістр літер у назвах класів PHP не важливий, але за домовленістю кожен сегмент пишуть з великої літери:extension/module/test_module→ControllerExtensionModuleTestModule. - Методи, що починаються з
__, викликати через URL не можна. - У маршруті дозволені лише латинські літери, цифри,
_і/— усе інше відкидається.
Коли ввімкнено SEO URL, адреса на кшталт /smartphones/ перетворюється на route=… контролером startup/seo_url (з бібліотекою SeoPro). Детальніше — у розділі SEO URL.
В адмінці до кожного маршруту, окрім сторінки входу, обов'язково додається параметр user_token, а доступ перевіряється за правами групи користувачів.
Шлях запиту#
Розгляньмо, що відбувається, коли покупець відкриває index.php?route=product/product&product_id=42.
index.phpвизначає версію (VERSION), підключаєconfig.php(якщо в ньому немаєDIR_APPLICATION, перенаправляє на інсталятор) і запускаєsystem/startup.php.system/startup.phpперевіряє версію PHP, нормалізує змінні сервера, визначає HTTPS, підключає автозавантажувач Composer (storage/vendor/autoload.php) і автозавантажувач бібліотек, файли рушія (system/engine/*) і помічники. Потім викликаєstart('catalog').system/framework.php:- створює реєстр (
Registry) і завантажує конфігураціюsystem/config/default.phpтаsystem/config/catalog.php; - створює та кладе в реєстр базові об'єкти:
config,log,event,load,request,response,db,session,cache,url(якщо потрібно),language,document; - реєструє системні події з конфігурації (
action_event); - виконує pre-actions — службові контролери з
action_pre_action.
- створює реєстр (
- Pre-actions вітрини (
catalog/controller/startup/):startup/session— сесія;startup/startup— визначає магазин, завантажує налаштування з таблиціoc_setting, мову, валюту, кошик, клієнта тощо;startup/error— обробник помилок;startup/event— реєструє події з таблиціoc_event(див. Події);startup/maintenance— режим обслуговування;startup/seo_url— розбір SEO-адрес.
- Маршрутизатор (
Router) запускає діюstartup/router, яка береrouteіз запиту (або маршрут за замовчуваннямcommon/home), викликає подіїcontroller/…/before, виконує контролер і подіїcontroller/…/after. Якщо контролер не знайдено, виконуєтьсяerror/not_found. - Контролер
ControllerProductProduct::index()завантажує мови й моделі, отримує дані товару, підключає дочірні контролери (common/header,common/footer, колонки) і викликає$this->load->view('product/product', $data). - Представлення: перед рендерингом спрацьовують системні події:
event/themeвизначає теку активної теми (і перевизначення з Редактора шаблонів),event/languageдодає в дані шаблону всі завантажені мовні рядки. Шаблон рендериться Twig. - Контролер передає HTML у
$this->response->setOutput(), аframework.phpнаприкінці викликає$response->output().
Адмінка працює так само (admin/index.php → start('admin')), але з власним набором pre-actions (startup/startup, startup/error, startup/event, startup/sass, startup/login, startup/permission) і маршрутом за замовчуванням common/dashboard.
Рушій (system/engine)#
Реєстр (Registry)#
Реєстр — це контейнер, у якому зберігаються всі спільні об'єкти застосунку. Контролери й моделі мають доступ до них як до властивостей: $this->db фактично означає $this->registry->get('db').
| Метод | Опис |
|---|---|
get($key) |
Повертає об'єкт за ключем або null. |
set($key, $value) |
Записує об'єкт. |
has($key) |
Перевіряє, чи є об'єкт. |
Основні об'єкти реєстру:
| Ключ | Клас | Призначення |
|---|---|---|
config |
Config |
Налаштування ($this->config->get('config_name')). |
db |
DB |
База даних. |
load |
Loader |
Завантажувач компонентів. |
request |
Request |
Дані запиту: get, post, cookie, files, server, request. |
response |
Response |
Відповідь: заголовки, вивід, перенаправлення. |
session |
Session |
Сесія ($this->session->data). |
language |
Language |
Мовні рядки. |
document |
Document |
Заголовок, мета-теги, CSS і JS сторінки. |
url |
Url |
Побудова посилань. |
cache |
Cache |
Кеш. |
log |
Log |
Журнал помилок. |
event |
Event |
Система подій. |
customer, cart, currency, tax, weight, length |
Cart\… |
Вітрина: клієнт, кошик, валюти, податки, одиниці. |
user |
Cart\User |
Адмінка: поточний адміністратор і його права. |
Моделі після завантаження теж потрапляють у реєстр під ключем model_<шлях_через_підкреслення>, наприклад model_catalog_product.
Завантажувач (Loader)#
Доступний як $this->load. Кожен виклик (крім model, library, helper) супроводжується подіями before і after, тож його можна перехопити через систему подій.
| Метод | Що робить |
|---|---|
controller($route, $data = array()) |
Виконує контролер і повертає його результат (зазвичай HTML). Так підключаються common/header, common/footer, модулі. |
model($route) |
Завантажує модель: $this->load->model('catalog/product') → $this->model_catalog_product. |
view($route, $data = array()) |
Рендерить шаблон і повертає HTML. |
language($route, $key = '') |
Завантажує мовний файл і повертає масив рядків. |
config($route) |
Завантажує файл конфігурації з system/config/. |
library($route) |
Підключає бібліотеку з system/library/ і кладе її екземпляр у реєстр. |
helper($route) |
Підключає файл помічника з system/helper/. |
// Контролер вітрини
$this->load->language('product/product');
$this->load->model('catalog/product');
$product_info = $this->model_catalog_product->getProduct($product_id);
$data['header'] = $this->load->controller('common/header');
$this->response->setOutput($this->load->view('product/product', $data));
Контролер#
Абстрактний клас Controller лише зберігає посилання на реєстр і через магічні методи __get/__set дає доступ до його об'єктів. Контролер може:
- виводити сторінку —
$this->response->setOutput($this->load->view(...)); - повертати фрагмент HTML (дочірні контролери, модулі) —
return $this->load->view(...); - повертати JSON для AJAX —
$this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode($json));; - перенаправляти —
$this->response->redirect($url); - повертати новий об'єкт
Action— тоді маршрутизатор виконає вказаний контролер (так працюють pre-actions і перенаправлення наerror/permission).
Модель#
Абстрактний клас Model влаштований так само, як контролер. Клас моделі називається Model + шлях: catalog/model/catalog/product.php → ModelCatalogProduct.
Завантажувач не віддає сам об'єкт моделі, а створює проксі (Proxy): кожен виклик методу моделі проходить через події model/<маршрут>/<метод>/before і after. Наприклад, $this->model_checkout_order->addOrderHistory(...) викликає події model/checkout/order/addOrderHistory/before та .../after.
<?php
class ModelExtensionModuleHelloBlock extends Model {
public function getProducts($limit) {
$query = $this->db->query("SELECT `product_id` FROM `" . DB_PREFIX . "product` WHERE `status` = '1' ORDER BY `date_added` DESC LIMIT " . (int)$limit);
return $query->rows;
}
}
Результат $this->db->query() для SELECT — об'єкт із властивостями row (перший рядок), rows (усі рядки) і num_rows. Завжди екрануйте рядки через $this->db->escape() і приводьте числа до (int)/(float). Детальніше про таблиці — у розділі Схема бази даних.
Представлення#
Шаблони пишуться мовою Twig (версія 3, з каталогу storage/vendor), файли мають розширення .twig.
- Адмінка:
admin/view/template/<маршрут>.twig. - Вітрина:
catalog/view/theme/<тема>/template/<маршрут>.twig. Якщо в активній темі файлу немає, береться файл з темиdefault.
У шаблоні доступні всі ключі масиву $data, переданого у view(), а також усі мовні рядки, завантажені до цього моменту, — їх додає системна подія event/language, тож писати $data['text_x'] = $this->language->get('text_x') для кожного рядка не обов'язково.
Автоекранування в Twig вимкнено (autoescape = false). Дані з $this->request уже пропущені через htmlspecialchars(), але дані, які ви отримуєте з інших джерел, екрануйте самостійно, наприклад фільтром {{ value|e }}.
Детальніше про синтаксис — у розділі Перенесення теми з 2.3 на 3.0.
Мова#
Мовні файли — це PHP-файли з масивом $_:
<?php
// catalog/language/uk-ua/extension/module/hello_block.php
$_['heading_title'] = 'Привітальний блок';
$_['text_empty'] = 'Тут поки нічого немає';
- Шлях:
<застосунок>/language/<код мови>/<маршрут>.php. Коди мов у 3.0 —uk-ua,en-gbтощо. - Під час завантаження спершу підключається файл мови за замовчуванням, потім — поточної мови, тож відсутні в перекладі рядки підставляються з мови за замовчуванням.
- Загальні рядки (кнопки, «Увімкнено»/«Вимкнено», формати дат) лежать у головному файлі мови, наприклад
admin/language/uk-ua/uk-ua.php. $this->language->get('key')повертає рядок або сам ключ, якщо рядка не знайдено, — так легко помітити пропущений переклад.- Мовні рядки можна перевизначати без зміни файлів у Дизайн → Редактор перекладу (докладніше).
Події#
Рушій викликає події під час завантаження контролерів, моделей, шаблонів, мовних файлів і конфігурації. Обробники реєструються в таблиці oc_event і дають змогу змінювати поведінку без правки ядра. Повний опис — у розділі Події.
Конфігурація застосунку#
Файли system/config/ визначають, як запускається кожен застосунок:
| Файл | Призначення |
|---|---|
default.php |
Спільні значення: мова за замовчуванням, кодування, часова зона, рушій шаблонів, кеш, журнал помилок, error_display, маршрути за замовчуванням. |
catalog.php |
Вітрина: БД, сесія, pre-actions, системні події. |
admin.php |
Адмінка: аналогічно, плюс перевірка входу й прав. |
Не редагуйте файли ядра в system/config/ напряму — під час оновлення зміни буде втрачено. Для власних налаштувань доповнення створіть окремий файл у system/config/ і завантажуйте його через $this->load->config('my_config').
Бібліотеки (system/library)#
Бібліотеки — це класи загального призначення. Більшість із них уже створена в framework.php і доступна через реєстр. Інші класи підвантажуються автоматично під час першого звернення (автозавантажувач шукає файл у system/library/ за назвою класу).
| Клас | Основні методи |
|---|---|
Cache |
get($key), set($key, $value), delete($key); драйвери: file, apc, mem, memcached, redis. |
Config |
get($key), set($key, $value), has($key), load($filename). |
DB |
query($sql), escape($value), getLastId(), countAffected(), connected(); драйвери: mysqli, mpdo та ін. |
Document |
setTitle(), setDescription(), setKeywords(), setRobots(), setOgImage(), addLink(), addStyle(), addScript() і відповідні get…(). |
Encryption |
encrypt($key, $value), decrypt($key, $value). |
Image |
resize(), crop(), rotate(), watermark(), save(). |
Language |
get($key), set($key, $value), all(), load($filename). |
Log |
write($message) — запис у журнал. |
Mail |
setTo(), setFrom(), setSender(), setReplyTo(), setSubject(), setText(), setHtml(), addAttachment(), send(). |
Pagination |
render() — HTML пагінації. |
Request |
Властивості get, post, cookie, files, server, request (очищені через htmlspecialchars()). |
Response |
addHeader(), redirect(), setOutput(), getOutput(), setCompression(). |
Session |
start(), getId(), destroy(), масив data. |
Template |
Обгортка над адаптером Twig. |
Url |
link($route, $args = '', $secure = false), addRewrite(). |
Cart\Cart, Cart\Customer, Cart\Currency, Cart\Tax, Cart\Weight, Cart\Length, Cart\User |
Кошик, клієнт, валюти, податки, одиниці виміру, адміністратор. |
Приклад відправлення листа (так само, як це робить ядро):
$mail = new Mail($this->config->get('config_mail_engine'));
$mail->parameter = $this->config->get('config_mail_parameter');
$mail->smtp_hostname = $this->config->get('config_mail_smtp_hostname');
$mail->smtp_username = $this->config->get('config_mail_smtp_username');
$mail->smtp_password = html_entity_decode($this->config->get('config_mail_smtp_password'), ENT_QUOTES, 'UTF-8');
$mail->smtp_port = $this->config->get('config_mail_smtp_port');
$mail->smtp_timeout = $this->config->get('config_mail_smtp_timeout');
$mail->setTo($this->config->get('config_email'));
$mail->setFrom($this->config->get('config_email'));
$mail->setSender(html_entity_decode($this->config->get('config_name'), ENT_QUOTES, 'UTF-8'));
$mail->setSubject('Тема листа');
$mail->setText('Текст листа');
$mail->send();
Власну бібліотеку доповнення покладіть у system/library/ (це одна з тек, куди дозволяє писати інсталятор) і підключіть через $this->load->library('mylib') — екземпляр з'явиться як $this->mylib.
Помічники (system/helper)#
Помічники — це набори звичайних функцій, підключених глобально під час старту:
| Файл | Функції |
|---|---|
general.php |
token($length = 32) — випадковий рядок (для токенів, назв тимчасових файлів). |
utf8.php |
utf8_strlen(), utf8_strpos(), utf8_strrpos(), utf8_substr(), utf8_strtolower(), utf8_strtoupper() — безпечна робота з багатобайтовими рядками (кирилицею). |
if ((utf8_strlen($this->request->post['name']) < 3) || (utf8_strlen($this->request->post['name']) > 64)) {
$this->error['name'] = $this->language->get('error_name');
}
Для перевірки довжини текстів українською завжди використовуйте utf8_strlen(), а не strlen(): остання рахує байти, і слово «Привіт» для неї має довжину 12.
Що далі#
- Створення модуля: покроково — практичний приклад, що поєднує всі компоненти MVC-L.
- Доповнення — типи доповнень і пакування.
- Події — як втручатися в роботу ядра без його зміни.
- Система модифікацій OCMOD — коли подій недостатньо.
- Стандарт кодування.