Quantcast
Channel: Linux.org.ru: Форум (тех. форум)
Viewing all articles
Browse latest Browse all 74589

История изменений сообщений как на ЛОРе

$
0
0

Вещь очень крутая, даже если не используется явно, кмк, в любой движок сайта должна закладываться такая фича. Чтобы любой элемент сайта имел историю изменений, как на ЛОРе: любой пост можно редактировать и всё сохраняется. Да чего там, взгляните на опеннет: даже там любой анонимус может исправить новость! Ящитаю это «маст хэв» фичей.

Мне захотелось поиграться и вместо банальных post_title, post_content реализовать более динамичное содержимое SQL-таблиц, в которых хранится вся история изменений, будь то комментариев, постов и т.д.

Хочу поделиться идеей реализации и спросить ваших советов, о мудрецы всея ЛОРа! Спасибо заранее. :)

Значит, ключевая табличка blog_posts — тут храним посты в ЖЖ, всё как обычно. Однако, как вы могли заметить, здесь отсутствуют привычные нам post_title, post_content и прочие «как у всех» поля, потому что в blog_posts будут хранится лишь мета-данные о постах, вроде, «доступен ли пост для чтения» и всё в таком роде. в post_object можно хранить serialized-массив с такими мета-данными. Лишние поля не нужны.

CREATE TABLE 'blog_posts'
(
	'post_id' INTEGER PRIMARY KEY,
	'post_object' TEXT
);

В табличке revision_posts хранится история изменений всех постов в ЖЖ. Привязка по post_id, затем id самой ревизии, далее отсылка на text_id — сам текст тоже храним в отдельной табличке, комментарий для описания коммита, и информация об авторе, конечно же, который создал этот «коммит», внеся изменения.

CREATE TABLE 'revision_posts'
(
	'post_id' INTEGER,
	'id' INTEGER PRIMARY KEY,
	'date' DATETIME DEFAULT CURRENT_TIMESTAMP,
	'text_id' INTEGER,
	'text_comment' NVARCHAR(255),
	'text_length' INTEGER DEFAULT 0,
	'author_id' INTEGER,
	'author_object' NVARCHAR(255),
	'author_ip' NVARCHAR(45),
	'author_agent' NVARCHAR(255)
);

Отдельно от всех мета-данных хранится уже сам текст поста в ЖЖ, каждая новая ревизия ссылается на новый изменённый текст, и старый текст никуда не девается. В поле text хранится сырой текст, который пользователь создал, text_filtered предназначен для хранения HTML-варианта, образованного из обработки сырого text, а text_flags это какие-нибудь опции, например, можно хранить сжатый gzip-текст, и указывать в text_flags что он был пожат.

CREATE TABLE 'text_posts'
(
	'text_id' INTEGER PRIMARY KEY,
	'text' TEXT,
	'text_filtered' TEXT,
	'text_flags' NVARCHAR(255)
);

Как вы знаете, пост в ЖЖ это не только сам текст, это ещё и заголовок, а ещё у поста есть тэги и прочая-прочая-прочая. Где это?

Решение простое — храним вместе с текстом, здесь же.

Как? В формате HTTP-заголовков, лол. :)

Date: дата создания
Tags: тэг.
	ещё один тэг, на новой строке.
	HTTP разрешает переносы строк,
	если ставить таб в начале.
Title: Заголовок поста

После пустой строки идёт содержание поста.

Храним целиком все дополнительные данные о посте вместе с его текстом, преобразуя эти данные в формат HTTP-заголовков!

Почему мне так захотелось? А прост. Для простоты измерения изменений.

1) HTTP-заголовки имеют простой формат, понятный человеку, любой дурак сможет отредактировать сырой текст поста, не говоря уже о том, что будет веб-интерфейс.

2) Когда мы захотим сравнить две ревизии, два коммита, — мы сравним не только текст поста, а ещё и все мета-данные! Вдруг, изменился заголовок, были добавлены/удалены тэги, и т.п.

Все данные в одном месте — при сравнении ревизий все изменения будут выявлены.

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

Тэги тоже дублировать в другое место, чтобы сделать более эффективную выборку по тэгам. Это понятно.

Но суть в том, что храня все данные разом — их история изменений будет очень наглядной! Вот в чём фишка предлагаемого мною формата данных.

Ну так вот, значит, для записи у нас есть данные $_POST['title'], $_POST['content'], $_POST['tags'], угу?

Нужно часть данных сохранить в заголовках, а текст оставить «как есть».

$headers = array(
        'Date' => date(DATE_RFC2822),
        'Tags' => http_header_tabbed($_POST['tags']),
        'Title' => http_header_tabbed($_POST['title'])
);
$content = $_POST['content'];

$text = http_headers_from_array($headers) . $content;

Готовый $text сохраняем в БД, всё просто.

Я вам своего кода принёс, братишки!

Функция http_header_tabbed ($multiline_text) исправляет переносы строк для HTTP-заголовков, расставляя в начале каждой новой строки TAB.

Функция http_headers_to_array ($raw_headers) преобразует сырой текст HTTP-заголовков в массив данных.

Функция http_headers_from_array ($php_array) соответственно наоборот (не тестировал, юзайте на страх и риск).

<?php
function http_headers_to_array($raw_headers) {
	if ($pos = strpos($raw_headers, "\r\n\r\n")) {
		$raw_headers = substr($raw_headers, 0, $pos + 4);
	}
	$headers = array();
	$header = '';
	foreach(explode("\r\n", $raw_headers) as $i => $line) {
		$key = strstr($line, ':', true);
		$value = substr(strstr($line, ':'), 2);
		if (isset($header) && substr($line, 0, 1) == "\t") {
			$headers[$header] .= "\r\n\t" . trim($line);
		}
		elseif (isset($value)) {
			$header = $key;
			if (isset($headers[$key])) {
				if (is_array($headers[$key])) {
					$headers[$key] = array_merge($headers[$key], array($value));
				}
				else {
					$headers[$key] = array_merge(array($headers[$key]), array($value));
				}
			}
			else {
				$headers[$key] = $value;
			}
		}
		else {
			$headers[0] = $key;
		}
	}
	return $headers;
}
function http_headers_from_array($php_array, $force_header = null) {
	$headers = '';
	foreach ($php_array as $key => $value) {
		if (isset($force_header)) {
			$key = $force_header;
		}
		if (is_array($value)) {
			$headers .= substr(http_headers_from_array($value, $key), -2);
		}
		else {
			$headers .= ucwords(trim($key, ':'), '-') . ': ';
			$headers .= http_header_tabbed($value) . "\r\n";
		}
	}
	return $headers . "\r\n";
}
function http_header_tabbed($multiline_text) {
	return str_replace(array("\r\n", "\r", "\n"), "\r\n\t", $multiline_text);
}
?>

Таким образом, мы преобразовали текст поста и все мета-данные к нему, типа заголовка, тэгов, даты — в удобный читаемый формат, который очень наглядно сравнивать!

Теперь самое интересное.

Вот имеем мы все эти данные, разбросанные по трём разным таблицам, надо это дело как-то склеить, да?

В SQL я лох, чего скрывать, все это знают, поэтому написал такой стрёмный SQL-запрос, чтобы данные склеивались, и если вы поможете его оптимизировать — буду благодарен!

-- нам нужен пост
SELECT * FROM blog_posts

-- берём самую первую ревизию поста
INNER JOIN revision_posts AS r_init ON
	(
		r_init.post_id = blog_posts.post_id AND r_init.id =
		(
		SELECT id FROM revision_posts WHERE post_id = blog_posts.post_id ORDER BY id ASC LIMIT 1
		)
	)

-- и берём самую последнюю ревизию поста
INNER JOIN revision_posts AS r_last ON
	(
		r_last.post_id = blog_posts.post_id AND r_last.id =
		(
		SELECT id FROM revision_posts WHERE post_id = blog_posts.post_id ORDER BY id DESC LIMIT 1
		)
	)

-- берём самый последний текст поста, это последняя ревизия
INNER JOIN text_posts ON text_posts.text_id = r_last.text_id

-- автор поста тот, кто создал первую ревизию
LEFT JOIN user ON user.user_id = r_init.author_id

-- ищем такой-то пост
WHERE post_id = :post_id

Такие дела.

Дабы не быть многословным... Я уже.

Я уже переделал свой ЖЖ под новый формат данных, который описал.

blog_posts, revision_posts, text_posts, text_posts изнутри

Блог, все комментарии в нём уже хранятся, добавляются и выводятся с учётом истории изменений! Дело тривиальное теперь, прикрутить функционал, нарисовать дополнительный интерфейс для редактирования всего и вся любыми анонимусам. И будет совсем как на ЛОРе! Так вот.

Очень хочется услышать ваших рекомендаций. Вдоль не предлагать.

 ,


Viewing all articles
Browse latest Browse all 74589

Trending Articles