Working with static pages in Yii

In this article I want to consider writing basic functionality for working with static pages. The task seems rather banal, but if we need nesting of pages, it must be admitted that it becomes more complicated. In this article I want to offer a simple solution for such a problem, which, it seems to me, can cover most of the requirements for static pages put forward by small websites.

And the requirements, in fact, are not so many:

  • support for nesting pages,
  • the ability to manage and edit pages from the admin panel,
  • quick, without numerous queries to the database, checking the existence of the page at the requested address, as well as quick generation of the page URL.

Since the described requirements are presented to the functionality necessary for most of the sites being created, it makes sense to design it as a module, and in the future just copy the latter from project to project.

Our entire system will revolve around a simple array, let's call it a path map . Each element of the array characterizes a separate page. Primary keys (hereinafter ID) of pages in the database are used as array indices, and paths to the corresponding pages are used as values.

Thus, the task is to write code that should:

  • in the process of parsing the URL, search for the page (its ID) by the requested path, and if the result is positive, return the page to the user.
  • when creating a URL, check for the existence of an element with an index equal to the ID of the page to which the link is created, and if such an element exists, return the path to this page.
  • Of course, everything should be cached, and the cache is updated when changes are made to the page hierarchy.

So let's get started. Let's start by creating a table for storing pages. The SQL query for this is as follows:

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`)
);

As you can see from the query, to build a hierarchical structure, the method of storing trees "nested sets" is used, therefore, when adding the administrative part of the module, it will make sense to use the Nested Set Behavior extension .

Next, using Gii, we generate the module framework (let's call it pages), as well as a model for working with the newly created table (we will call it Page).

Correct the code of the created module. Add the attribute $ cacheId, which will store the identifier for the cached path map.

When the module is initialized, a check must be made whether the path map exists in the cache, and if it is not there, the map that is current at the time of the call must be generated. To do this, we add the init () function.

We also add three methods: generating, updating and returning a map of paths. Total, the module code takes the following form:

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;
	}
}

We are done with the module class, do not forget to let the application know about it by adding the module identifier to the modules property of the configuration array.

Now create the PagesUrlRule rule class inherited from CBaseUrlRule. It is enough to declare only two methods in it: for creating and for parsing URLs. The code for the method for creating the URL is as follows:

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;
}

In the method, the existence of the page in the path map is checked, and when it is found, the path to it is returned (do not forget about the URL suffix! I love that the addresses end with a slash).

The code of the method for parsing the URL (here, on the contrary, the page ID is searched for along the path to it):

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';
}

Remember to add an entry with a reference to the rule class in the configuration array. Well, since we are returning a link to the default controller here, its code will not be out of place.

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;
	}
}

Actually, that's all. The functionality for parsing, creating URLs, and displaying pages to the visitor is ready. And the implementation of the page management functionality (it is quite standard), if you wish, you can see in the finished project, which can be downloaded from here .

UPD1. Fixed work with the cache, added indexes to the table, updated link to the file with the finished project.

Also popular now: