Работа со статическими страницами в Yii

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

А требований, собственно, не так уж и много:

  • поддержка вложенности страниц,
  • возможность управления и редактирования страниц из админки,
  • быстрая, без многочисленных запросов к базе данных, проверка существования страницы по запрашиваемому адресу, а также быстрая генерация URL страницы.

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

Вся наша система будет вращается вокруг простого массива, назовём его картой путей. Каждый элемент массива характеризует отдельную страницу. В качестве индексов массива используются первичные ключи (далее ID) страниц в базе, а в качестве значений — пути до соотвествующих страниц.

Таким образом, задача заключается в написании кода, который должен:

  • в процессе разбора URL производить поиск страницы (её ID) по запрашиваемому пути, и при положительном исходе выдавать страницу пользователю.
  • при создании URL проверять существование элемента с индексом, равным ID страницы, на которую создаётся ссылка, и если такой элемент существует, возвращать путь до этой страницы.
  • разумеется всё должно кешироваться, а кеш при изменениях в иерархии страниц обновляться.

Итак, приступим. Начнём с создания таблицы для хранения страниц. SQL-запрос для этого выглядит следующим образом:

CREATE TABLE IF NOT EXISTS `pages` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `root` int(10) unsigned NOT NULL,
  `lft` int(10) unsigned NOT NULL,
  `rgt` int(10) unsigned NOT NULL,
  `level` int(10) unsigned NOT NULL,
  `parent_id` int(10) unsigned NOT NULL,
  `slug` varchar(127) NOT NULL,
  `layout` varchar(15) DEFAULT NULL,
  `is_published` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `page_title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `meta_title` varchar(255) NOT NULL,
  `meta_description` varchar(255) NOT NULL,
  `meta_keywords` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `root` (`root`),
  KEY `lft` (`lft`),
  KEY `rgt` (`rgt`),
  KEY `level` (`level`)
);

Как видно из запроса, для построения иерархической структуры используется метод хранения деревьев «вложенные множества», поэтому при дописании административной части модуля, будет иметь смысл использовать расширение Nested Set Behavior.

Далее, с помощью Gii, генерируем каркас модуля (назовём его pages), а также модель для работы с только что созданной таблицей (её назовём Page).

Поправим код созданного модуля. Добавим атрибут $cacheId, в котором будет храниться идентификатор для кешированной карты путей.

При инициализации модуля должна происходить проверка, существует ли в кеше карта путей, и если она там отсутствует, должна генерироваться актуальная на момент вызова карта. Для этого дописываем функцию init().

Также добавляем три метода: генерирующий, обновляющий и возвращающий карту путей. Итого, код модуля принимает следующий вид:

class PagesModule extends CWebModule {

	/**
	 * @var string идентификатор, по которому доступна закешированная карта путей
	 */
	public $cacheId = 'pagesPathsMap';

	public function init()
	{
		if (Yii::app()->cache->get($this->cacheId) === false)
			$this->updatePathsMap();

		$this->setImport(array(
			'pages.models.*',
			'pages.components.*',
		));
	}

	/**
	 * Возвращает карту путей из кеша.
	 * @return mixed
	 */
	public function getPathsMap()
	{
		$pathsMap = Yii::app()->cache->get($this->cacheId);
		return $pathsMap === false ? $this->generatePathsMap() : $pathsMap;
	}

	/**
	 * Сохраняет в кеш актуальную на момент вызова карту путей.
	 * @return void
	 */
	public function updatePathsMap()
	{
		Yii::app()->cache->set($this->cacheId, $this->generatePathsMap());
	}

	/**
	 * Генерация карты страниц.
	 * @return array ID узла => путь до узла
	 */
	public function generatePathsMap()
	{
		$nodes = Yii::app()->db->createCommand()
			->select('id, level, slug')
			->from('pages')
			->order('root, lft')
			->queryAll();

		$pathsMap = array();
		$depths = array();

		foreach ($nodes as $node)
		{
			if ($node['level'] > 1)
				$path = $depths[$node['level'] - 1];
			else
				$path = '';

			$path .= $node['slug'];
			$depths[$node['level']] = $path . '/';
			$pathsMap[$node['id']] = $path;
		}

		return $pathsMap;
	}

}

На этом с классом модуля мы закончили, не забудьте дать знать о нём приложению, дописав идентификатор модуля в свойство modules массива конфигурации.

Теперь создадим класс правила PagesUrlRule, унаследованный от CBaseUrlRule. В нём достаточно объявить всего два метода: для создания и для разбора URL. Код метода для создания URL выглядит следующим образом:

public function createUrl($manager, $route, $params, $ampersand)
{
	$pathsMap = Yii::app()->getModule('pages')->getPathsMap();

	if ($route === 'pages/default/view' && isset($params['id'], $pathsMap[$params['id']]))
		return $pathsMap[$params['id']] . $manager->urlSuffix;
	else
		return false;
}

В методе производится проверка существования страницы в карте путей, и при нахождении возвращается путь к ней (не забываем про URL-суффикс! — люблю чтобы адреса оканчивались слешем).

Код метода для разбора URL (здесь наоборот, производится поиск ID страницы по пути к ней):

public function parseUrl($manager, $request, $pathInfo, $rawPathInfo)
{
	$pathsMap = Yii::app()->getModule('pages')->getPathsMap();

	$id = array_search($pathInfo, $pathsMap);

	if ($id === false)
		return false;

	$_GET['id'] = $id;
	return 'pages/default/view';
}

Не забудьте добавить запись со ссылкой на класс правила в конфигурационный массив. Ну и раз уж мы возвращаем здесь ссылку на контроллер default, не лишним будет привести его код.

class DefaultController extends Controller {

	public function actionView($id)
	{
		$page = $this->loadModel($id);

		$this->render('view', array(
			'page' => $page,
		));
	}

	public function loadModel($id)
	{
		$model = Page::model()->published()->findByPk($id);
		if ($model === null)
			throw new CHttpException(404, 'Запрашиваемая страница не существует.');
		return $model;
	}

}

Собственно, всё. Функционал для разбора, создания URL, и вывода страниц посетителю готов. А реализацию функционала управления страницами (он вполне стандартен), если есть желание, можете посмотреть в готовом проекте, который можно загрузить отсюда.

UPD1. Исправлена работа с кешем, добавлены индексы в таблицу, обновлена ссылка на файл с готовым проектом.
  • +4
  • 17.2k
  • 9
Share post

Comments 9

    +3
    Мне кажется, вы делаете популярную ошибку — использование кэша в качестве постоянного хранилища. Кэш — хранилище очень быстрое, но не гарантированное. Кэш не дает никакой гарантии, что то, что вы туда положили, может быть извлечено пусть даже через микросекунду после того, как положили. Для проверки задайте в конфигурации проекта NullCache из Yii как кэш и проверьте — в норме все должно работать нормально (хотя и медленно).

    Да и сама идея хранить статические страницы в БД и доставать оттуда мне кажется, мягко говоря, странной.
      0
      Видимо предполагается их редактирование в админке.
        +2
        Это не исключает возможности сохранения отредактированных страниц на диск и дальнейшей работы с ними как с простейшими статическими страницами.
          0
          На счёт первого пункта — ваша правда, код поправлю в ближайшее время. На счёт хранения страниц в БД — не совсем согласен. Часто, в том числе и людям далёким от вёрстки, требуется самостоятельно вносить изменения в текст страниц, менять в них различные мета-теги и т.д., так что хранение страниц со всеми их дополнительными данными в БД кажется весьма удобным решением. Хотя, если под дальнейшей работой со страницами вы подразумеваете вывод их посетителю, то тогда да — логичным было бы использовать кеш, а не тянуться за страницей в БД. Учту.
            0
            Можно предоставить возможность редактировать, но хранить в файловой системе. В БД однозначно удобнее, потому что в случае хранения файлов — надо еще к каждому файлу будет файл свойств пристыковывать — или сохранять свойства в файле.
      0
      А как же индексы в таблице?
        0
        Для чего они здесь?
          0
          Конкретно здесь, где извлечение идёт либо всех данных (для карты путей), либо по первичному ключу (для вывода страницы посетителю) они не нужны. Однако, если всё же не останавливаться на этом голом функционале, а, например, дополнить модель методом, отдающим массив хлебных крошек для CBreadcrumbs, то можно и индексы добавить. Добавлю.
          0
          Спасибо! может пригодится для шаблонных сайтов. За реализацию спасибо! Сохранил Ваш модуль в дропбоксе)

          Only users with full accounts can post comments. Log in, please.