parser

Новостной раздел для чайников

Misha v.3 [07 июня 2004]

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

Если вы считаете, что файлики — рулез, а база данных — сакс, то дальше можете не читать.

Сразу отмечу, что код, написанный ниже, использует класс для работы с датами и некоторые методы, описанные в полезных пользовательских операторов. Кроме того подразумевается, что в $MAIN создан объект pSQL одного из sql-ных классов.

Структура таблицы новостей в БД должна быть примерно такая (в применении к MySQL):

CREATE TABLE article (
	article_id INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
	article_type_id TINYINT(4) NOT NULL DEFAULT 0,
	title VARCHAR(255),
	lead VARCHAR(255),
	body TEXT,
	is_published TINYINT(1) NOT NULL DEFAULT 0,
	dt DATETIME NOT NULL,
	dt_published DATETIME NOT NULL
)

CREATE INDEX ix_article_0 ON article (
	article_type_id,
	is_published,
	dt_published,
	dt
)

Это была самая важная часть работы. Правильно созданная структура в БД будет позволять просто получать нужную информацию и делать это очень быстро.

Всё, для большинства применений этого бывает достаточно.
Краткое описание полей:

article_id - идентификатор новости.
article_type_id - тип новости, у нас в одной таблице будут лежать новости (article_type_id = 0),
	пресс-релизы (article_type_id = 1) и ... все остальные данные подобного типа какие вы только придумаете.
title - заголовок новости
lead - анонс новости. показывается с заголовком новости при выводе списка новостей.
body - текст новости
is_published - показываем новость (1) или нет (0)
dt - дата/время новости
dt_published - дата/время, начиная с которой показываем новость на сайте

Ни в коем случае не стоит забывать про индексы. Они на порядок, а то и больше, могут ускорить выполнение запросов, но прежде чем их создавать — стоит ознакомиться с документацией по SQL серверу. Если вы поймете, что это такое и как их использовать — то тот код, который вы будете писать для работы с БД будет работать очень быстро.

В частности, т.к. используется один из классов sql, то есть возможность получить статистику по запросам, добавив ?mode=debug в адресную строку броузера, и посмотреть, что происходит при доставании данных при наличии индекса и без него. При этом разница будет очень существенна, если у вас в БД будет много новостей.

Метод получения статей по заданным параметрам

@getArticles[hParam]
$hParam[^hash::create[$hParam]]
$result[^pSQL.table{
	SELECT
		article_id AS id,
		title,
		lead,
		dt
		^if(^hParam.id.int(0)){
			, body
		}
	FROM
		article
	WHERE
		article_type_id = ^hParam.iArticleTypeId.int(0)
		AND is_published = 1
		AND dt_published <= ^pSQL.now[]
		^if(^hParam.iId.int(0)){
			AND article_id = ^hParam.iId.int(0)
		}
		^if(def $hParam.sWhere){
			AND $hParam.sWhere
		}
	ORDER BY
		dt DESC
}[
	^if(def $hParam.iLimit){$.limit($hParam.iLimit)}
	^if(def $hParam.iOffset){$.offset($hParam.iOffset)}
]]
#end @getArticles[]

Отображение списка анонсов статей

@printArticlesList[tArticle]
<ul>
^untaint[as-is]{
	^tArticle.menu{
		<li><b>^dtf:format[%d.%m.%Y;$tArticle.dt]</b>
		^printTitle[$tArticle]
		<br />$tArticle.lead</li>
	}
}
</ul>
#end @printArticlesList[]


@printTitle[tArticle]
^if(def $tArticle.title && ^tArticle.title.match[\^[[^^\^]]+\^]]){
	$result[^tArticle.title.match[\^[([^^\^]]+)\^]][g]{<a href="?id=$tArticle.id">$match.1</a>}]
}{
	$result[<a href="?id=$tArticle.id">$tArticle.title</a>]
}
#end @printTitle[]

Обратите внимание, что при выводе используется форматирование дат парсером, т.к. обычно не принято выводить на страничку сотни новостей. Однако при выводе больших списков (не новостей) лучше форматировать даты средствами SQL сервера.

Ещё обратите внимание на конструкции с match. Они нужны для того, чтобы ссылки можно было ставить не только со всего заголовка новости, а с его части, обозначенной символами '[' и ']'.

Внимательные птицеводы наверняка обратят внимание на используемый при выводе новостей untaint и спросят: «как-же так? почему мы выводим все как есть? а не поломается ли страница если в title/lead окажутся, например, не закрытые теги?».
Да, страница поломается, но мы делаем новостной раздел, и в данном случае расчитываем на то, что новости в базу данных будут попадать через административный раздел, и именно в нем должна быть реализована проверка на валидность текстов и отсутствие незакрытых тегов.

Отображение одной статьи с текстом

@printArticleItem[tArticle]
^untaint[as-is]{
	^if(def $tArticle.title){<h1>^tArticle.title.match[[\[\]]][g]{}</h1>}
	^dtf:format[%d %h %Y;$tArticle.dt;$dtf:rr-locale]
	<p>$tArticle.body</p>
}
#end @printArticleItem[]

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

Достаем информацию для отображения календаря

@getCalendar[hParam]
$hParam[^hash::create[$hParam]]
$result[^pSQL.table{
	SELECT
		^pSQL.month[dt] AS month,
		^pSQL.year[dt] AS year
	FROM
		article
	WHERE
		article_type_id = ^hParam.iArticleTypeId.int(0)
		AND is_published = 1
		AND dt_published <= ^pSQL.now[]
	GROUP BY
		year,
		month
}]
#end @getCalendar[]

Отображение календаря.

Мне очень не нравится, когда делают календарь, как описано в уроке документации при наличии в месяце всего нескольких новостей. С моей точки зрения, значительно проще и удобнее в использовании календарь следующего вида, за исключением того, что в случае если у нас года идут по убыванию, месяцы должны также идти по убыванию ;), однако это несущественная деталь, которая может быть легко изменена.

@printCalendar[tCalendar][i;dtNow;iCurrentYear;iCurrentMonth;hYear;tYear;hYM;iMonth]
^if($tCalendar){
	$dtNow[^date::now[]]
	$iCurrentYear(^form:tYear.int($dtNow.tYear))
	$iCurrentMonth(^form:month.int($dtNow.month))

	$hYM[^tCalendar.hash{^tCalendar.year.format[%04d]=^tCalendar.month.format[%02d]}[month][$.distinct(1)]]

	$hYear[^tCalendar.hash[year;year][$.distinct(1)]]
	$tYear[^hYear._keys[]]
	^tYear.sort($tYear.key)[desc]

	<table border="1" align="right">
	<tr valign="top">
		<td>
			^tYear.menu{
				<b>^if($tYear.key == $iCurrentYear){
					$tYear.key /&nbsp^;
				}{
					^rem{ *** если кликнем в год, по попадем на последний месяц года, 
						за который у нас есть новости *** }
					^if(^tCalendar.locate[year;$tYear.key]){}
					<a href="?tYear=$tYear.key&month=$tCalendar.month">$tYear.key</a>
				}
				</b><br />
			}
		</td>
		<td>
			^if(^tYear.locate[key;$iCurrentYear]){}
			^for[i](0;11){
				$iMonth(12-$i)
				^if(def $form:month && $iMonth == $iCurrentMonth){
					<b>$dtf:[ri-locale].month.$iMonth</b><br />
				}{
					^if(!($dtNow.tYear == $iCurrentYear && $iMonth > $dtNow.month)){
						^if($hYM.[^iCurrentYear.format[%04d]=^iMonth.format[%02d]]){
							<a href="?tYear=$iCurrentYear&month=$iMonth">$dtf:[ri-locale].month.$iMonth</a>
						}{
							$dtf:[ri-locale].month.$iMonth
						}
						<br />
					}
				}
			}
		</td>
	</tr>
	</table>
}
#end @printCalendar[]

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

@main[]
^if(!def $form:id){
	^rem{ *** $form:id не определена: показываем календарь со списком новостей *** }

	^rem{ *** получаем информацию о календаре и выводим его *** }
	$tCalendar[^getCalendar[$.iArticleTypeId(1)]]
	^printCalendar[$tCalendar]
	
	^rem{ *** получаем список последних новостей или новостей за указанный период и выводим его *** }
	$tArticle[^getArticles[
		$.iArticleTypeId(1)
		^if(^form:tYear.int(0)){
			$.sWhere[dt >= '^form:tYear.int(0)-^form:month.int(0)-00' AND dt <= '^form:tYear.int(0)-^form:month.int(0)-31']
		}{
			$.iLimit(20)
		}
	]]
	^if($tArticle){
		^printArticlesList[$tArticle]
	}{
		<p>Не найдено ни одной новости за указаный период.</p>
	}
}{
	^rem{ *** $form:id определена: достаем и показываем новость с выбранным id *** }
	$tArticle[^getArticles[
		$.iArticleTypeId(1)
		$.iId(^form:id.int(0))
	]]
	^if($tArticle){
		^printArticleItem[$tArticle]
	}{
		^rem{ *** указали такую id, новости с которой не существует... посылаем посетителя на... 404 ошибку. *** }
		^Lib:location[/404/]
	}
}

И напоследок: файл, в котором всё описанное выше собрано в кучу специально не прилагается, дабы было хоть одно место, где любители copy/paste имели возможность подумать сами.

P.S. см. также комментарии в форуме: http://www.parser.ru/forum/?id=55197