В данной статье рассмотрим какие классы/библиотеки ответственны за вывод страниц интернет-магазина, в каком порядке это происходит. Начнем с момента, когда конфигурация OpenCart 2+ уже загружена. Про сам процесс загрузки приложения читайте тут.

После загрузки нужных библиотек приложения OpenCart, в файле system\framework.php создается объект класса Front (файл system\engine\front.php)
$controller = new Front($registry);

Далее в файле system\framework.php из файла конфигурации, в цикле, в свойство $pre_action класса Front сохраняется список методов служебных классов, которые нужно выполнить в первую очередь.
if ($config->has('action_pre_action')) {
    foreach ($config->get('action_pre_action') as $value) {
        $controller->addPreAction(new Action($value));
    }
}
и в строке
$controller->dispatch(new Action($config->get('action_router')), new Action($config->get('action_error')));
вызывается метод dispatch(), которому первым аргументом передается Action – объект выполняющий переданный ему метод класса (в данном случае взятый из файла конфигурации) «startup/router».

В OpenCart принято основное или единственное действие контроллера называть «index», что позволяет не указывать потом его название при вызове контроллера. В данном случае вызываться будет метод index контроллера ControllerStartupRouter (файл catalog\controller\startup\router.php)

Вторым аргументом метода dispatch() передается объект Action, который выполнит метод index контроллера из файла error/not_found. Данный контроллер отвечает за вывод страницы с сообщением об ошибке с соответствующим текстом. Таким образом данный контроллер будет вызываться всегда, если при подключении/выполнении контроллера переданного первым аргументом или при подключении списка методов служебных классов возникнет ошибка.

Таким образом, Front Controller выполняет сначала методы служебных объектов, а далее «startup/router».

Тут стоит заметить, что startup/router выполнится только в случае, если в настройках не включено ЧПУ URL, в противном случае выполнится метод, указанный для обработки ЧПУ, например из класса ControllerStartupSeoPro. В таком случае, используется результат работы такого метода, а метод контроллера из startup/router не будет выполняться. Подробнее про ЧПУ в OpenCart читайте в этой статье.

В любом случае, какой бы контроллер не обрабатывал URL, он преобразует URL в вид понятный приложению такого плана:
http://oc1.loc/index.php?route=product/category&path=25_28
и обрабатывает одинаково, поэтому, будем считать, что ЧПУ выключено и в скрипте выполнится ControllerStartupRouter.

Когда выполнение доходит до метода index() ControllerStartupRouter, в коде получаются GET-параметры из текущего URL, а именно – значение параметра route. Значение route представляет из себя метод контроллера, который нужно выполнить (метод отсутствует, если вызывается метод index)
Если параметр route не был передан – в значение route сохраняется контроллер выводящий главную страницу:
if (isset($this->request->get['route']) && $this->request->get['route'] != 'startup/router') {
    $route = $this->request->get['route'];
} else {
    $route = $this->config->get('action_default');
}

Далее в классе ControllerStartupRouter выполняется метод trigger(), вызывающий выполнение всех обработчиков привязанных к данному событию:
$result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data));

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

Переходя по страницам, мы получаем разный URL в адресной строке, соответственно, при выполнении контроллера ControllerStartupRouter, каждый раз, вызывается разное действие (вызывается другой контроллер).



Для примера возьмем контроллер выводящий товары определенной категории:
http://oc1.loc/index.php?route=product/category&path=25_28
Из данной ссылки понятно, что будет вызван контроллер product/category (файл catalog\controller\product\category.php). Метод его не указан, значит будет вызов метода index(). Так же, в адресной строке присутствуют доп.параметры:
path=25_28.
В данном случаем это две категории – родительская и дочерняя. Параметр path используется в коде вызываемого контроллера.
Код данного контроллера вы можете открыть сами (файл catalog\controller\product\category.php). Он стандартный- подключаются языковые файлы, модели, для получения данных из БД:
$this->load->language('product/category');
$this->load->model('catalog/category');

Так же вызываются контроллеры, формирующие разные части страницы, например шапку:
$data['header'] = $this->load->controller('common/header');

Все полученные данные сохраняются в массив $data.
Если категория указанная в URL найдена, выполняется строка
$this->response->setOutput($this->load->view('product/category', $data));
иначе так же формируется массив $data, куда сохраняются сообщения связанные с тем, что категория не найдена и выполняется строка
$this->response->setOutput($this->load->view('error/not_found', $data));

Видим, что строки почти одинаковы, отличается только файл-представление, который будет выполняться. Аналогичные строки есть и в других контроллерах, ответственных за вывод определенной страницы.

Рассмотрим что будет происходить на примере первой строки.
Прежде всего выполнится
$this->load->view('product/category', $data)

При выполнении $this->load получим объект загрузчика - class Loader (файл system\engine\loader.php), у которого производится вызов метода view с указанными аргументами. Каким образом в контроллере мы получаем объект класса Loader читать в статье «Описание процесса загрузки приложения.» Что представляет из себя метод view() можно посмотреть в файле system\engine\loader.php

А именно:
1.
Выполняются методы классов указанные в обработчиках, прикрепленные к данному событию.
Вернув какой-то результат выполнения методов привязанных к событию, можно отменить подключение файла-вида и вывести данный результат на экран вместо него.

2.
Создает объект класса Template:
$template = new Template($this->registry->get('config')->get('template_type'));
который выбирает тип шаблона указанный в конфигурации и сразу создает объект класса этого шаблона. По-умолчанию это класс Template\php из файла system\library\template\php.php
- далее, в цикле, объекту Template\PHP передаются данные из массива $data (из контроллера):
public function set($key, $value) {
    $this->adaptor->set($key, $value);
}

Таким образом, все данные сохраняются в свойстве $data объекта Template\PHP (файл system\library\template\php.php).
Далее в классе Loader выполняется строка
$output = $template->render($route . '.tpl');
согласно которой, выполняется метод render() объекта Template, в который передается файл-представление полученное из контроллера, а именно:
… ->view('product/category'…

Данный метод возвращает результат выполнения
$this->adaptor->render($template);
где уже вызывается одноименный метод render() созданного ранее объекта Template\PHP. Рассмотрим его подробнее:
public function render($template) {
    $file = DIR_TEMPLATE . $template;
    if (is_file($file)) {
        extract($this->data);
        require($file);
        return ob_get_clean();
    }
    trigger_error('Error: Could not load template ' . $file . '!');
    exit();
}

В аргументе метода получаем шаблон. В нашем примере, это default/template/extension/module/category.tpl
Далее подставляется спереди значение константы, в которой хранится путь к файлам видов:
…/catalog/view/theme/
Потом проверяется наличие файла по указанному пути. Если файл найден, то вызывается функция extract() извлекающая данные из массива $data. Именно благодаря этому, в файле-представлении мы можем обратиться напрямую к его значениям, т.к. ключи данного массива становятся названиями переменных, а значения ключей – значениями переменных.
Далее файл вид подключается, вывод содержимого происходит в буфер обмена, а не на экран, поэтому вызовом строки
return ob_get_clean();
мы возвращаем результат того, что должно отобразиться на экран.

Выполнение кода возвращается к файлу загрузчика и в переменную $output сохраняется результат вывода файла вида:
$output = $template->render($route . '.tpl');

3.
Снова вызываются обработчики событий, привязанных к данному действию. Теперь им
дополнительно передается аргументом переменная $output, для того, чтобы в своих методах можно было обработать результат выполнения файла вида.


Наконец, результат вывода файла-представления, сохраненный в переменной $output передается назад в контроллер и подставляется в метод setOutput():
$this->response->setOutput($this->load->view('product/category', $data));

Данный метод вызывается у объекта Response. В нем переданный результат сохраняется в свойство $output.

Выполнение кода возвращается к файлу, с которого мы начинали обзор - system\framework.php, где сначала устанавливается уровень сжатия содержимого страницы
$response->setCompression($config->get('config_compression'));

Далее вызывается метод output() объекта Response
$response->output();
в котором отсылаются заголовки браузеру и выводится контент страницы строкой
echo $output;

На этом скрипт завершает свою работу.