Простейший компонент


Внимание: в статье используется устаревшая структура компонента. Рекомендуется для начала изучить статью структура компонентов и в дальнейшем учитывать её рекомендации.

Сейчас мы попытаемся создать компонент "Новости". Для начала он будет очень простым - даже без базы данных. Наша задача - создать компонент, который будет обрабатывать следующие запросы:

  • /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, порою с довольно сложным алгоритмом.

Итоги

Окончательный код полученного компонента вы можете скачать по этой ссылке. Теперь наша задача - научиться работать с базой данных.

Метки: Азы
18.12.2013
Все статьи