Архітектура 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.

  1. index.php визначає версію (VERSION), підключає config.php (якщо в ньому немає DIR_APPLICATION, перенаправляє на інсталятор) і запускає system/startup.php.
  2. system/startup.php перевіряє версію PHP, нормалізує змінні сервера, визначає HTTPS, підключає автозавантажувач Composer (storage/vendor/autoload.php) і автозавантажувач бібліотек, файли рушія (system/engine/*) і помічники. Потім викликає start('catalog').
  3. 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.
  4. Pre-actions вітрини (catalog/controller/startup/):
    • startup/session — сесія;
    • startup/startup — визначає магазин, завантажує налаштування з таблиці oc_setting, мову, валюту, кошик, клієнта тощо;
    • startup/error — обробник помилок;
    • startup/event — реєструє події з таблиці oc_event (див. Події);
    • startup/maintenance — режим обслуговування;
    • startup/seo_url — розбір SEO-адрес.
  5. Маршрутизатор (Router) запускає дію startup/router, яка бере route із запиту (або маршрут за замовчуванням common/home), викликає події controller/…/before, виконує контролер і події controller/…/after. Якщо контролер не знайдено, виконується error/not_found.
  6. Контролер ControllerProductProduct::index() завантажує мови й моделі, отримує дані товару, підключає дочірні контролери (common/header, common/footer, колонки) і викликає $this->load->view('product/product', $data).
  7. Представлення: перед рендерингом спрацьовують системні події: event/theme визначає теку активної теми (і перевизначення з Редактора шаблонів), event/language додає в дані шаблону всі завантажені мовні рядки. Шаблон рендериться Twig.
  8. Контролер передає 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.

Що далі#

Сторінку перекладено й адаптовано для ocStore з офіційної документації OpenCart. Ліцензія — GNU GPL v3.