Роутинг - начало

 

Всем доброго времени суток. Сегодня мы с Вами начнем делать роутинг системы RS-MINI. Что такое роутинг? В понимании нашей CMS — это алгоритм позволяющий по данным, переданным через адресную строку, найти в таблице rs_map (которую мы создали в предыдущем посте) открываемую страницу. В случае если такая страница была не найдена, выдать пользователю ошибку 404 (пока очень примитивную). Если же такая страница есть в базе данных, то необходимо будет определить какие именно контроллеры подключены к данной странице, собрать их и запустить (это мы будем делать не сегодня, это так, для справки Улыбаюсь)

Для начала нам понадобится сам алгоритм поиска открытой страницы (map.class.php). Так же мы напишем небольшой метод в классе application.class.php выдачи 404 ошибки (как уже говорил очень примитивной)

Давайте начнем.

Реализация класса map.class.php

Нам понадобится метод __call() (про него мы уже говорили, и надеюсь понимаем как он работает Улыбаюсь) и два метода

  • по обработке данных из адресной строки (назвал это метод getPage())
  • и получение страниц из базы данных (наконец-то опробуем наш класс sql.class.php) (назвал этот метод getNode())

Методы само собой задокументированы, но пояснить, в двух словах, логику считаю обязательным. Но для начала давайте я выложу Вам код, а ниже поясню некоторые моменты. (файл лежит в папке /rs-mini/core/ и называется map.class.php)

<?php

/*
 * @package RS-MINI
 * @copyright (c) 2015 Alexey Glumov aka Rio-Shaman (support@rio-shaman.ru)
 * @license GNU General Public License version 2; see LICENSE.txt
 *
 */
 
namespace core;

if(!defined('RS-MINI')) die();

/*
 * map v 1.0
 *
 * класс работает с структурой сайта (роутинг)
 *
 */

class map
{   
    /*
     * хранит значение посланные в метод __call()
     *
     * @var    - массив
     * @access - private
     */
    
    private $call = array();
    
    /*
     * массив c страницами
     *
     * @var    - array
     * @access - public
     */
    
    public $pageList;
    
    /*
     * метод позволяем вызвать несуществующие методы класса
     *
     * @access - public
     * @return - сам себя или сохраненное значение (все зависит от
     *           того set или get метод был вызван)
     *
     * @param string name      - имя вызываемого метода
     * @param array  arguments - присланные аргументы в вызываемый метод
     */
    
    public function __call($name, $arguments)
    {
        $name       = mb_strtolower($name);
        
        $action     = mb_substr($name, 0, 3);
        $argument   = mb_substr($name, 3, mb_strlen($name)-1);
        
        switch($action) {
            case 'get':
                return (isset($this->call[$argument])) ? $this->call[$argument] : null;
                break;
            case 'set':
                $this->call[$argument] = $arguments[0];

                return $this;
                break;
        }
    }
    
    /*
     * получаем шаблон, все контроллеры и их параметры открываемой страницы
     *
     * @access - public
     * @return - массив с данными или false если ничего не найдено
     */
     
    public function getPage()
    {
        // получаем адрес страницы
        $uri = request::getHttpServer('REQUEST_URI')->toUri();
        
        // если адрес равен null выкидываем false
        if (is_null($uri))
            return FALSE;

        // просто бьем строку на массив
        $uri = explode('/', $uri);

        // удаляем последний элемент массива, так как он всегда пустой
        unset($uri[(count($uri) - 1)]);
            
        // получаем все открываемые узлы
        // родитель по умолчанию NULL (главная страница)
        $parent = NULL;
        foreach ($uri as $key => $alias) {
            // получаем страницу
            if (!$page = $this->setAlias($alias)->setParent($parent)->getNode())
                return FALSE;
            
            // запоминаем ID страницы, это наш родитель в следующей итерации
            $parent = $page['map_id'];

            // создаем объект страницы
            $this->pageList[] = new page($page);
        }
        
        return TRUE;
    }

    /*
     * метод получения узла определенной ветки
     *
     * @access - private
     * @return - массив с структурой сайта или false если ничего не найдено
     *
     * @param integer getParent() - родитель предшествующего узла
     * @param string  getAlias()  - алиас узла
     */
     
    private function getNode()
    {
        // с начало пытаемся получить страницу как статичную
        $sql = ''
            . 'SELECT'
                . ' m.map_id            AS map_id,'
                . ' m.map_parent        AS map_parent,'
                . ' m.map_alias         AS map_alias,'
                . ' m.map_name          AS map_name,'
                . ' m.map_view          AS map_view,'
                . ' m.map_system        AS map_system,'
                . ' m.map_type          AS map_type'
            . ' FROM'
                . ' rs_map              AS m'
            . ' WHERE'
                . '     m.map_parent    ' . ( (is_null($this->getParent())) ? 'IS NULL' : '= \'' . $this->getParent() . '\'' )
                . ' AND m.map_alias     ' . ( ($this->getAlias() == '')     ? 'IS NULL' : '= \'' . $this->getAlias() . '\'' )
                . ' AND m.map_type      = \'static\''
        ;
        
        // если выборка дала результат
        if ($resultList = sql::query($sql)->fetch()) {
            // запоминаем алиас динамичного узла.
            // так как узел статичный, то мы записываем туда NULL
            $resultList[0]['dynamic_alias'] = NULL;
            // возвращаем результат
            return $resultList[0];            
        }
        
        // если статичный узел был не найден, пытаемся найти динамичный
        $sql = ''
            . 'SELECT'
                . ' m.map_id            AS map_id,'
                . ' m.map_parent        AS map_parent,'
                . ' m.map_alias         AS map_alias,'
                . ' m.map_name          AS map_name,'
                . ' m.map_view          AS map_view,'
                . ' m.map_system        AS map_system,'
                . ' m.map_type          AS map_type'
            . ' FROM'
                . ' rs_map              AS m'
            . ' WHERE'
                . '     m.map_parent    ' . ( (is_null($this->getParent())) ? 'IS NULL' : '= \'' . $this->getParent() . '\'' )
                . ' AND m.map_type      = \'dynamic\''
        ;
        
        // если выборка не дала результата
        if (!$resultList = sql::query($sql)->fetch())
            // вертаем false
            return FALSE;
            
        // запоминаем динамичный алиас (реально записанный в БД)
        $resultList[0]['dynamic_alias'] = $resultList[0]['map_alias'];
        // перезаписываем псевдоним на тот что в адресной строке
        $resultList[0]['map_alias']     = $this->getAlias();
            
        // возвращаем результат
        return $resultList[0];        
    }
}

Метод getPage()

Давайте начнем с метода getPage(). В общем-то портянка кода не имеет ничего экстраординарного. Давайте разберем на пример открытия страницы /about/feedback/. При этом заведома уточним что все эти страницы есть в базе и у

  • главной странице id равен 1
  • у about id равне 2
  • а у feedback — 3

Приступим. Первая строчка кода получит нам вот такие данные (запишется в переменную uri):

/about/feedback/

Вы могли обратить внимания что эти данные получаются через класс request метод toUri(). Если помните там были проверки на корректность урла, поэтому беспокоится о каких либо переданных опасных символах тут нам не приходится Улыбаюсь если было передано что-то запрещенное, то метод toUri() вернет нам NULL, а если в переменно uri будет NULL, то метод getPage() закончит свою работу вернув false (а этот результат спровоцирует показ 404-ой ошибки)

Далее значение в переменно uri бьется на массив, тем самым мы получаем вот такие данные

array(4) {
    [0] => string(0) ""
    [1] => string(5) "about"
    [2] => string(8) "feedback"
    [3] => string(0) ""
}

Как видите в начале пустой элемент массива и в конце. Это из-за того что и в начале и в конце есть слеши. От последнего элемента массива мы принудительно избавляемся, так как наши урлы по задумке всегда заканчиваются на слеш, а значит последний элемент массива всегда пуст. Получаем вот такой массив

array(3) {
    [0] => string(0) ""
    [1] => string(5) "about"
    [2] => string(8) "feedback"
}

Далее мы назначаем родителя страницы по умолчанию. У главной страницы ведь нет родителя, и в базе данных поле map_parent будет иметь значение NULL, стало быть стартовое значение родителя должно быть тоже NULL.

Теперь запускаем цикл, и листаем псевдонимы страниц переданные нам через адресную строку.

В нашем примере первый псевдоним будет пустота (смотрите первый элемент массива uri) У главной странице псевдонима тоже нет, так что передовая в метод getNode() (о нем чуть ниже) родителя с значением NULL и псевдоним с значением «пусто» мы должны получить главную страницу сайта. После успешно запроса мы сохраняем в переменной parent id только что полученной страницы и поднимем объект страницы (класс page.class.php) в свойстве pageList

В следующей итерации псевдоним уже будет равен about а id родителя единицы. Такая страница у нас тоже имеется, поэтому результат будет утвердительным. Сохраняться новые данные (родитель теперь должен равняться id страницы about, то есть двум), и запустится последняя итерация с псевдонимом feedback.

Если хотя бы в одной из итераций страница не будет найдена, это приведет к 404-ой ошибке.

Метод getNode()

Этот метод получает из базы данных страницу с переданными параметрами. Тут имеются два скуль запроса, почему именно два? Объясняется это тем, что у нас имеются два вида старниц (мы о них уже говорили)

  • статичные
  • и динамичные

По приоритету запрашивается с начало статичная страница, используя все переданные параметры (и родитель и псевдоним). Если запрос был удачным то метод возвращает выборку в метод getPage() но перед этим создает еще один элемент массива — dynamic_alias. При первом удачном запросе этот элемент будет содержать NULL.

Если же запрос прошел не удачно, система попробует найти первую попавшуюся динамичную страницу. При этом будет использован лишь родитель, опуская при этом значение псевдонима. При успешном втором запросе, в элемент массива map_alias бедет передан запрашиваемый псевдоним (взятый из адресной строки) а в элемент dynamic_alias будет сохранен реальный псевдоним. Очень сложно объяснить зачем это все нужно, но я все же попробую.

Роль dynamic_alias в системе

Я уже не раз говорил что динамичные страницы понадобятся нам в основном для реализации редакторов. А именно что бы по адресу /post/123/edit/ нам открывалась статья чей id равен 123. Как контроллер узнает что ему нужно вытащить именно статью с таким id, а не с другим? Он должен запросить значение 123 из адресной строки. Вот тут возникает технический вопрос, а как именно он это сделает?

Как раз на этот случай и необходим этот самый элемент массива dynamic_alias. Значение хранящиеся тут будет передано в объект класса page.class.php, а уже после в параметры контроллера который должен отработать на странице edit. То есть контроллеру будут заранее известны все динамические страницы которые есть в адресной строке (в нашем примере такая страница только одна)

Запрашивать id из строки контроллер сможет по реальному псевдониму динамичной страницы. То есть если в базе есть три вложенных узла

  • post — статичный
  • post_id — динамичный
  • edit — статичный

а в адресной строке запрашивалась страницы /post/123/edit/ то под ключом post_id в спец массиве будет лежать значение 123. Надеюсь понятно о чем я (ну если не понятно, мы еще вернемся к этому вопросу)

Подключаем и вызываем роутер

Ну что же, нам осталось подключить новый класс к приложению, и вызвать из него (из класса map.class.php) метод getPage(). Так же нам понадобится метод вывода 404-ой ошибки который будет содержать всего пару строк кода. Это временный метод, позднее мы будем подгружать специально подготовленную страничку. В общем вот новый код файла application.class.php (файл если не помните лежит в папке /rs-mini/core/)

<?php

/*
 * @package RS-MINI
 * @copyright (c) 2015 Alexey Glumov aka Rio-Shaman (support@rio-shaman.ru)
 * @license GNU General Public License version 2; see LICENSE.txt
 *
 */

namespace core;

if(!defined('RS-MINI')) die();

/*
 * application v 1.0
 *
 * Класс стартует систему
 *
 */

class application
{
    /*
     * объект с картой сайта
     *
     * @var         - object
     * @access      - private
     */
    
    private static $map;

    /*
     * объект дома
     *
     * @var         - object
     * @access      - private
     */
    
    private static $dom;

    /*
     * главный метод. запуск системы
     *
     * @access  - public
     *
     */
     
    public static function run()
    {
        // поднимаем некоторые классы ядра
        // объект по работе с структурой сайта
        self::$map = new map();
        // объект прослойка между контроллерами и вьюшкой
        self::$dom = new dom();
        
        // стартуем подключение к БД
        sql::setConnect();
        
        // получаем открываемую страницу
        if (!self::$map->getPage())
            self::notFound();
        
        // тест работы роутинга
        // просматриваем массив объектов страниц
        ?><xmp><?
        var_dump(self::$map->pageList);
        ?></xmp><?
    }
    
    /*
     * метод выводит 404 ошибку
     *
     * @access - private
     */

    private static function notFound()
    {
        header('HTTP/1.0 404 Not Fount');
        die('404 error');
    }
}

В методе run() я разместил небольшой тест, он позволит увидеть массив с поднятыми объектами класса page.class.php.

Сейчас если запустить главную страницу нашего сайта, мы увидим ошибку 404. Почему так происходит? Все очень просто, у нас в базе данных нет ни одной созданной страницы, поэтому для того что бы протестировать нашу проделанную работу, необходимо создать хотя бы пару узлов.

Добавляем несколько страниц в базу данных

Давайте протестируем оба типа страниц (я про статичные и динамичные типы). Для этого создадим главную (статичную) и вложим внутрь динамичную страницу. Вот sql запрос для этого:

INSERT INTO `rs_map`
    (
        `map_id`,
        `map_parent`,
        `map_alias`,
        `map_name`,
        `map_view`,
        `map_system`,
        `map_type`
    )
VALUES
    (
        NULL,
        NULL,
        NULL,
        'Главная',
        '',
        'true',
        'static'
    ),
    (
        NULL,
        '1',
        'test_alias',
        'Тест',
        '',
        'true',
        'dynamic'
    )
;

Теперь, если мы запустим главную страницу, то увидим вот такую картинку: (один поднятый объект класса page.class.php)

главная страница

Если же перейти вот по такому http://mini.test.ru/123/, или такому адресу http://mini.test.ru/chtonibud/ (тут без разницы, ведь страница динамическая) то мы увидим уже два созданных объекта page, это говорит нам о том, что открыто две страницы.вложенная динамическая страница

Разницы между этими объектами пока никакой, но это пока Улыбаюсь

Заключение

Надеюсь материал не очень сложный для восприятия. В конце статьи Вы можете скачать проделанную работу вместе с дампом базы данных.

Если у Вас возникли какие либо вопросы, то задавайте их в комментариях, постараюсь на них ответить

Всего Вам наилучшего, на сегодня все!

Прикрепленные файлы

 

Возможно Вам будут интересны следующие заметки

Комментарии (10)

Ваше имя *
Сайт
Ваш E-mail *
Ваше сообщение *
 
jimi333, 11 Апреля 2017 г. 21:13 пишет:
Читатель
$resultList[0]['dynamic_alias'] = NULL;
Cannot use a scalar value as an array
Не хочет присваивать значение, как быть?
Алексей, 11 Апреля 2017 г. 21:26 пишет:
Автор
У Вас по какой-то причине в переменной $resultList находится скалярное выражение (число). Хотя строка
sql::query($sql)->fetch()
должна вернуть либо false либо массив. Вы случайно ничего в коде не меняли?
Ответ для пользователя: jimi333
jimi333, 11 Апреля 2017 г. 21:36 пишет:
Читатель
Нет, я специально отдельно вывел
sql::query($sql)->fetch()
там массив со значениями из базы данных, а операция is_array() дала true;
У меня почему то поле в массив не хочет добавляться,
$x2['qwerty'] = '12345';
работает, а
$x2[0]['qwerty'] = '12345';
уже не хочет работать, x2 объявил в начале как массив.
Ответ для пользователя: Алексей
Алексей, 11 Апреля 2017 г. 21:44 пишет:
Автор
Перепишете строчки
// если выборка дала результат
if ($resultList = sql::query($sql)->fetch()) {
    // запоминаем алиас динамичного узла.
    // так как узел статичный, то мы записываем туда NULL
    $resultList[0]['dynamic_alias'] = NULL;
    // возвращаем результат
    return $resultList[0];            
}
вот так
// если выборка дала результат
if ($resultList = sql::query($sql)->fetch()) {
    
    ?><xmp><?
    var_dump($resultList);
    ?></xmp><?
    die();
    
    // запоминаем алиас динамичного узла.
    // так как узел статичный, то мы записываем туда NULL
    $resultList[0]['dynamic_alias'] = NULL;
    // возвращаем результат
    return $resultList[0];            
}
и напишите, что увидите на экране.
Ответ для пользователя: jimi333
jimi333, 11 Апреля 2017 г. 21:54 пишет:
Читатель
array(14) {
  ["map_id"]=>
  int(1)
  [0]=>
  int(1)
  ["map_parent"]=>
  NULL
  [1]=>
  NULL
  ["map_alias"]=>
  NULL
  [2]=>
  NULL
  ["map_name"]=>
  string(14) "Главная"
  [3]=>
  string(14) "Главная"
  ["map_view"]=>
  string(0) ""
  [4]=>
  string(0) ""
  ["map_system"]=>
  string(4) "true"
  [5]=>
  string(4) "true"
  ["map_type"]=>
  string(6) "static"
  [6]=>
  string(6) "static"
}

Warning: Cannot use a scalar value as an array
Ответ для пользователя: Алексей
Алексей, 11 Апреля 2017 г. 22:02 пишет:
Автор

Вы переписывали класс /rs-mini/core/sql.class.php? У Вас явно результат функции mysqli_fetch_array() а не mysqli_fetch_assoc()
К тому же класс sql возвращает многомерный массив следующий структуры:

array (
    0 => array(
        'алиас поля' => 'значение',
        'алиас поля' => 'значение',
        'алиас поля' => 'значение'
    ),
    1 => array(
        'алиас поля' => 'значение',
        'алиас поля' => 'значение',
        'алиас поля' => 'значение'
    )
)

а у Вас

array (
    'алиас поля' => 'значение',
    'алиас поля' => 'значение',
    'алиас поля' => 'значение'
)
Ответ для пользователя: jimi333
jimi333, 11 Апреля 2017 г. 22:07 пишет:
Читатель
Точно, я PDO использовал
Ответ для пользователя: Алексей
jimi333, 12 Апреля 2017 г. 00:36 пишет:
Читатель
Сделал массив как у вас, но тут новая проблема.
На главной странице у меня отражаются 2 поднятых объекта класса page. Вероятно потому что
$uri = request::getHttpServer('REQUEST_URI')->toUri();
выдает
/mini.test.ru/
и получается 2 элемента в массиве. Вроде решилось путем удаления первого элемента.
Но почему то при
mini.test.ru/chtonibud/
меня перекидывает сразу в localhost.
Я скачал ваши исходники и абсолютно ничего не менял, все равно выкидывает в localhost
Ответ для пользователя: Алексей
Алексей, 12 Апреля 2017 г. 10:35 пишет:
Автор
$uri = request::getHttpServer('REQUEST_URI')->toUri();
т.е. у Вас архив с системой доступен по адресу http://localhost/mini.test.ru/?
меня перекидывает сразу в localhost.
что значит перекидывает? В проекте вреде нет редиректов куда либо, только недореализованная ошибка 404.
Ответ для пользователя: jimi333
jimi333, 12 Апреля 2017 г. 23:34 пишет:
Читатель
Путь поменял, кажется работает
Но раньше почему - то перекидывал на уровень выше, хотя редиректа нигда нету, и использовал я ваши файлы
Ответ для пользователя: Алексей