После обычной установки OpenCart, URL, при переходах по страницам интернет-магазина, например на страницу категории(рубрики) Мониторы, имеет такой вид:
http://oc1.loc/index.php?route=product/category&path=25_28

Приложение передает в адресной строке все нужные для выполнения и открытия указанной строки данные – файл index.php, который выполняется первым и является «точкой входа», далее в параметре route указывается метод контроллера, который нужно выполнить (если название метода отсутствует – выполняется метод по-умолчанию «index»). После route передаются другие данные, в нашем случае это идентификатор родительской и дочерней категории.
Данный вид URL плохо воспринимается поисковыми системами, кроме того человеку тоже трудно догадаться что именно должно отобразиться на странице.

В результате для OpenCart и прочих фреймворков и CMS разработаны решения, преобразующие обычный вид URL в ЧПУ (человеко-понятный). При этом, данная строка URL примет, например, такой вид:
http://oc1.loc/component/monitor/
где будет отсутствовать точка входа, параметр route и тд.

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

Я использую ocStore - русскоязычную версию OpenCart с некоторыми улучшениями, такими как модуль ЧПУ - SeoPro. Данный модуль заменяет стандартный функционал формирования ЧПУ в движке OpenCart, прежде всего убирает дубли страниц и др. Далее буду описывать работу ЧПУ на основе этого модуля, хотя принцип работы общий. Если вы используете обычную версию OpenCart, а не ocStore - советую скачать бесплатный модуль SeoPro.

В админ-панели, ЧПУ можно включить НАСТРОЙКИ - Мой Магазин - редактировать - Сервер
  • Включить SEO URL – Да.
  • Тип ЧПУ – SeoPro
  • ЧПУ товаров с категориями - Да

Итак, если параметр route в URL отсутствует (например когда выбрано ЧПУ в настройках), то в файле .htaccess выполняется строка
RewriteRule ^([^?]*) index.php?_route_=$1 [L,QSA]
которая изменяет URL на
index.php?_route_=
а после знака равно, подставляются все элементы идущие после названия хоста в URL.

Таким образом, URL
http://oc1.loc/component/monitor/
передаст серверу
http://oc1.loc/index.php?_route_=component/monitor/
где скрипт должен будет перевести полученные данные в нужный для выполнения формат, в данном случае:
http://oc1.loc/index.php?route=product/category&path=25_28
то есть определить, что нужно выполнить метод index контроллера ControllerProductCategory.
Вот о том, как происходит разбор и выполнение действий согласно URL и напишу в данной статье.



В файле system\framework.php создается объект класса Front:
$controller = new Front($registry);
Он сохраняет себе в свойстве addPreAction массив служебных действий, которые должны выполниться автоматически сразу после загрузки всех библиотек. Данный массив хранится в файле system\config\catalog.php или system\config\admin.php для админки. В версии для frontend он содержит такой массив
$_['action_pre_action'] = array(
    'startup/session',
    'startup/startup',
    'startup/error',
    'startup/event',
    'startup/maintenance',
    'startup/'.$seo_type
);
где последним элементом идет 'startup/'.$seo_type содержащий выбранный вариант организации ЧПУ (название нужного контроллера).

Далее в файле system\framework.php происходит вызов метода dispatch() Front Controllerа:
$controller->dispatch(new Action($config->get('action_router')), new Action($config->get('action_error')));
который передает на выполнение действия (контроллеры), первым элементом из которых является ControllerStartupRouter, что передается из массива конфигурации командой $config->get('action_router'). Вторым аргументом передается действие выводящее страницу ошибок, если, например, запрашиваемая страница не была найдена.
В методе dispatch() Front Controllerа происходит поочередный вызов служебных действий, которые были переданы заранее и самым последним должен вызываться контроллер разбирающий стандартный URL содержащий параметр route.
public function dispatch(Action $action, Action $error) {
    $this->error = $error;

    foreach ($this->pre_action as $pre_action) {
        $result = $this->execute($pre_action);

        if ($result instanceof Action) {
            $action = $result;
            break;
        }
    }

    while ($action instanceof Action) {
        $action = $this->execute($action);
    }
}

Но тут есть условие – если одно из действий вернуло результат своего выполнения и результат является объектом класса Action – данный результат перезапишет действие, которое было передано методу dispatch() в качестве первого аргумента (Action $action):
$action = $result;
А выполнение контроллера обрабатывающего URL ЧПУ, как раз возвращает такой результат. Поэтому далее выполнение получает не аргумент $action (передающий на выполнение метод index контроллера startup/router), а контроллер/метод, возвращенный после разбора ЧПУ в контроллере ControllerStartupSeoPro или другом, указанном для работы с ЧПУ.


Рассмотрим стандартный способ создания ЧПУ, когда в настройках выбран вариант «SeoPro».

Вызывается контроллер ControllerStartupSeoPro из файла catalog\controller\startup\seo_pro.php
При создании данного объекта, в конструкторе с помощью строки
$this->cache_data = $this->cache->get('seo_pro');
в свойство $cache_data сохраняется массив всех значений из таблицы БД url_alias. Алиасы представляют собой сокращенные фразы, по которым фреймворк будет искать контроллер/метод, которые нужно выполнить.
Выполняется метод index(), где в строке
if (!isset($this->request->get['_route_'])) {...}
проверяется наличие в GET-параметрах «_route_». Если его нет – выполняется метод validate(), выводящий или главную страницу или страницу ошибок. Если есть – код выполняется дальше. А именно:
$route_ = $route = $this->request->get['_route_'];
- значение _route_ из URL сохраняется в переменную $route_ и одновременно в $route с удалением данного параметра из Request Object.
Строкой
$parts = explode('/', trim(utf8_strtolower($route), '/'));
параметры из _route_ разбиваются по разделителю на массив значений такого плана:
Array
(
    [0] => component
    [1] => monitor
)
и сохраняются в массив $parts.

Далее берется последний элемент из массива полученных GET-параметров и с помощью функции explode() скрипт пытается снова разбить его на массив значений по разделителю (точке). Результат присваивается переменной $last_part:
list($last_part) = explode('.', array_pop($parts));
а потом содержащиеся в ней массивы строк добавляются к тому, что было в переменной $parts до удаления последнего элемента ее массива. В нашем примере с рубрикой component/monitor последние преобразования не изменят начальный массив $parts.

Далее в цикле формируется новый массив $rows, где ключами становятся элементы из параметры GET, а значениями – алиасы, которые соответствуют этим элементам из таблицы url_alias, которые были сохранены конструктором в свойстве $cache_data. Пример:
Array
(
    [0] => Array
        (
            [keyword] => component
            [query] => category_id=25
        )

    [1] => Array
        (
            [keyword] => monitor
            [query] => category_id=28
        )
)

Далее проверяется наличие строки
component/monitor/
в переменной $route
if (isset($this->cache_data['keywords'][$route])){...}
в таблице url_alias. В нашем случае она найдена не будет, иначе значение переменной $rows было бы переписано.

В любом случае, при существовании данных в таблице алиасов, что проверяется в следующем условии:
if (count($rows) == sizeof($parts)) {...}
данные преобразуются. Сначала в цикле в вид
Array
(
    [component] => category_id=25
    [monitor] => category_id=28
)
а потом проверяется, содержит ли данный массив параметр category_id, что значит- страница определенной рубрики. Если содержит, то определяется свойство $this->request->get['path']. В нашем примере оно будет:
25_28
Данный параметр будет использован в контроллере, который будет вызван (catalog\controller\product\category.php).

Далее следует блок проверок, формирующий параметр $this->request->get['route'] который будет состоять из контроллера, метод index() которого должен будет выполняться. В нашем примере это product/category.

Вызывается метод контроллера ControllerStartupSeoPro
$this->validate();
в котором, в случае, если значение в параметре $this->request->get['route'] отсутствует, происходит вывод главной страницы или переадресация.

Если значение имеется, оно передается в объект Action для сохранения и вызова в дальнейшем:
return new Action($this->request->get['route']);

Ну а дальше происходит то, о чем я писал в начале - результат перезапишет вызов метода index() контроллера startup/router, который должен был включить разбор URL без ЧПУ содержащий параметр route.