Простейший компонент
Внимание: в статье используется устаревшая структура компонента. Рекомендуется для начала изучить статью структура компонентов и в дальнейшем учитывать её рекомендации.
Сейчас мы попытаемся создать компонент "Новости". Для начала он будет очень простым - даже без базы данных. Наша задача - создать компонент, который будет обрабатывать следующие запросы:
- /news/ - вывод списка новостей со ссылками на каждую новость
- /news/<ID>/ - вывод одной новости, где <ID> - идентификатор конкретного новостного сообщения.
Сами новости пусть пока будут храниться в виде массива внутри контроллера.
Итак, в каталоге app/components создаем файл модуля News.php. А нем будет два класса: собственно класс модуля, а также класс, реализующий роутер.
<?php class Component_News implements Core_ModuleInterface { public static function initialize() { CMS::add_component('News', new Component_News_Router); } } class Component_News_Router extends CMS_Router { public function route($request) { return false; } }
Вот этот маленький PHP-файл - уже является настоящим компонентом. Осталось включить его в index.php:
Core::load('CMS'); Core::load('Component.News'); CMS::Run();
Очень важно добавить его до вызова CMS::Run(), не в коем случае после! Однако, ничего на нашем сайте нового не появилось, поскольку функция route всегда возвращает false. Т.е. компонент, хоть и есть, но на все запросы отвечает отказом.
Component_News может содержать статический метод initialize(), который будет вызван после его загрузки. В данном случае в нем происходит регистрация компонента: в метод add_component() передается его название и экземпляр роутера, который будет обрабатывать для него запросы. Это необходимо сделать, иначе маршрутизация не будет работать.
Строим функцию route
Функция route в качестве аргумента принимает объект HTTP-запроса. Нас из него интересует только REQUEST_URI, который мы разберем с помощью регулярных выражений и, встретив нужный запрос, вернем информацию о контроллере:
class Component_News_Router extends CMS_Router { public function route($request) { // получаем URI $uri = $request->uri; // если передан адрес списка новостей, // то надо запустить Component.News.Controller::view_list if ($uri=='/news/') { return array( 'controller' => 'Component.News.Controller', 'action' => 'view_list', ); } return false; } }
Вот теперь роутер стал узнавать адрес /news/, однако для работы нам не хватает контроллера. Создаем каталог app/components/News, а в нем - файл Controller.php:
<?php class Component_News_Controller extends CMS_Controller implements Core_ModuleInterface { static $news = array( 1 => array( 'date' => '28.07.1914', 'title' => 'Первая мировая', 'content' => 'Началась Первая мировая война. Такие дела...', ), 2 => array( 'date' => '11.11.1918', 'title' => 'Компьенское перемирие', 'content' => 'Первая мировая война закончилась. Все радуются.', ), ); public function view_list() { return $this->render('list',array( 'rows' => self::$news, )); } }
Теперь у нас есть и контроллер с одним единственный экшеном - view_list, который берет список записей (пока только из переменной - члена класса) и передает ее в шаблон. Здесь остановимся поподробнее.
Каждый экшен обязательно должен вернуть какое-либо значение. Они могут быть следующими:
- Произвольное строковое значение - содержимое строки будет отправлено как HTTP-страница с кодом 200.
- $this->render('<Имя шаблона>',$params) - будет вызван шаблон, и ему будут передан массив параметров, которые внутри шаблона будут доступны как обычные переменные. Полученный контент будет отображен внутри лейаута.
- $this->page_not_found() - будет возвращен отклик "404 Page not found".
- $this->redirect_to($url) - редирект с кодом 302
- $this->moved_permanently_to($url) - редирект с кодом 301
- Произвольный объект HTTP-отклика.
В нашем случае используется функция render с вызовом шаблона list, поэтому нам нужно создать также и сам файл шаблона. Шаблоны компонента лежат в каталоге views. Создаем каталог app/components/News/views, в нем создаем файл list.phtml
<h1>Новости</h1> <?php foreach($rows as $id => $row) { ?> <div class="news_item"> <div class="date"><?= $row['date'] ?></div> <a class="title" href="/news/<?= $id ?>/"><?= $row['title'] ?></a> </div> <?php } ?>
Вот теперь все должно заработать. Обратите внимание, что параметр rows, который мы передали шаблон, можно использовать в нем как обычную переменную.
Экшены с параметрами
Теперь наша задача - сделать показ одной новости. Для этого нам необходимо передавать контроллеру идентификатор записи, которую следует отобразить.
Модифицируем роутер следующим образом:
public function route($request) { // получаем URI $uri = $request->uri; // если передан адрес списка новостей, // то надо запустить Component.News.Controller::view_list if ($uri=='/news/') { return array( 'controller' => 'Component.News.Controller', 'action' => 'view_list', ); } // если передан адрес показа одной новости, // то надо запустить Component.News.Controller::view_item if (preg_match('{^/news/(\d+)/$}',$uri,$m)) { return array( $m[1], 'controller' => 'Component.News.Controller', 'action' => 'view_item', ); } return false; }
Обратите внимание, что для показа одной новости мы кроме двух именованных элеменов (controller, action) массива возвращаем также и один неименованный - значение подстроки из регулярного выражения, в которой находится идентификатор новости. Все неименованные элементы таких массивов будут передаваться в экшен контроллера в качестве аргументов.
Теперь добавим в контроллер новый экшен:
public function view_item($id) { // Если такой новости не существует, // то вернем 404 if (!isset(self::$news[$id])) { return $this->page_not_found(); } return $this->render('item',array( 'item' => self::$news[$id], )); }
Этот экшен подобен предыдущему, только в нем вызывается другой шаблон (и с другими данными) и перед этим осуществляется проверка на существование такой новости. Содержимое шаблона (item.phtml) может быть, например, таким:
<h1><?= $item['title'] ?></h1> <div class="news-content"><?= $item['content'] ?></div>
Упрощаем роутер
Созданный нами роутер, по сути, выполняет одну функцию: сравнивает поступивший REQUEST_URI с шаблонной строкой и, если сравнение успешно, возвращает массив с информацией о контроллере/экшене/аргументах. Очевидно, что в большинстве случаев по этой же схеме будут работать и большинство других роутеров, поэтому для осуществления этих стандартных действий в ТАО существует стандартный же механизм.
Перепишем роутер следующим образом:
class Component_News_Router extends CMS_Router { public function controllers() { return array( 'Component.News.Controller' => array( 'path' => '/news/', 'rules' => array( '{^$}' => array('action' => 'view_list'), '{^(\d+)/$}' => array('{1}','action' => 'view_item'), ), ), ); } }
Не смотря на то, что выглядит он совершенно иначе, выполняет он абсолютно те же самые действия. Функции route в нем уже нет, поскольку используется та, которая определена в родительском классе CMS_Router - она работает на основании информации, возвращаемой функцией controllers. В примере - информация только об одном контроллере, однако в этом массиве их может быть и несколько (столько, сколько их есть в компоненте). Полагаю, нет смысла подробно останавливаться на формате массива - он довольно прост и очевиден. Достаточно разве что обратить внимание на строку вида {1}: в возвращаемой функцией route информации значение этой строки замениться на содержимое первой подстроки регулярного выражения.
Разумеется, с помощью такой упрощенной записи можно реализовать только самые примитивные ситуации. Однако иногда встречаются задачи, когда для маршрутизации необходимо учитывать не только REQUEST_URI, но и кучу других факторов: домены, IP-адреса, с которых идет запрос, содержимое базы данных, права пользователей. Очевидно, что в таких ситуациях не обойтись без написания функции route, порою с довольно сложным алгоритмом.
Итоги
Окончательный код полученного компонента вы можете скачать по этой ссылке. Теперь наша задача - научиться работать с базой данных.