Pull to refresh

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

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

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

  • поддержка вложенности страниц,
  • возможность управления и редактирования страниц из админки,
  • быстрая, без многочисленных запросов к базе данных, проверка существования страницы по запрашиваемому адресу, а также быстрая генерация 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. Исправлена работа с кешем, добавлены индексы в таблицу, обновлена ссылка на файл с готовым проектом.
Tags:
Hubs:
+4
Comments 9
Comments Comments 9

Articles