Cache Manager

Я почему-то думал, что давно уже выложил в блоге свои классы опубликованные на phpclasses.org, а когда недавно хотел посмотреть один из них, оказалось, что их нет. А вот сейчас я собираюсь исправить этот недосмотр и начну с класса для кэширования страниц.

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


class cache {
	/**
	 * @author CTAPbIu_MABP
	 * @version 1.4
	 * @license GNU General Public License
	 *
	 */

	protected $cache = NULL;
	protected $handler = array();

	/**
	 * Стартует кеширование с makeGZip(),
	 * если у браузера присутствует соотвецтвующий заголовок
	 *
	 * @param bool $gzip
	 */
	public function __construct($gzip=TRUE){
		//принимает ли браузер сжатие?
		if (@strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip')!==false && $gzip) // @ отключает NOTICE если заголовок HTTP_ACCEPT_ENCODING не пришел вообще
			// второй параметр это степень буфферизации 0 > 9
			$this->start('makeGZip', array(5));
	}

	/**
	 * Начинает новую буферизацию и записывает ее обработчик
	 *
	 * @param string $func
	 * @param array $argv
	 */
	public function start($func, $argv=NULL){
		@ob_start();
		// notice: надо передавать именно NULL, а не FALSE тогда при array_merge элемент удалится
		$this->handler[] = array($func, $argv);
	}

	/**
	 * Очищает буфер $loop раз и выталкивает последние $loop обработчиков
	 *
	 * @param int $loop
	 */
	public function free($loop=0){
		$loop = $loop ? $loop : count($this->handler);
		for($i=0;$i< =$loop;$i++){
			@ob_end_clean();
			array_pop($this->handler);
		}
	}

	/**
	 * Парсит последние $loop буфферов
	 *
	 * @param integer $loop
	 * @return mixed
	 */
	public function flush($loop=0){
		$loop = $loop ? $loop : count($this->handler);
		for ($i=$loop-1;$i>=0;$i--){
			// узнаем номер последнего хендлера
			$x = count($this->handler)-1;
			// получаем последний буффер для работы
			$this->cache = @ob_get_contents();
			// создаем правильный массив параметров
			$params = array_merge((array)$this->cache, (array)$this->handler[$x][1]);
			// проверяем принадлежит ли функция объекту или она пользовательская
			$handler = method_exists($this,$this->handler[$x][0]) ? array($this,$this->handler[$x][0]) : $this->handler[$x][0];
			// вызываем нужную функцию с параметрами
			$this->cache = call_user_func_array($handler, $params);
			// удаляем последний. отработаный, элемент массива
			array_pop($this->handler);
			// отключаем буферизацию
			@ob_end_clean();
			// выбрасываем назад содержимое буффера
			echo $this->cache;
		}

		return $this->cache;
	}

	/**
	 * Gzip'ирует буффер и возвращает в поток или в файл,
	 * если имя файла и расширение указано
	 *
	 * @param mixed $buf
	 * @param integer $ratio
	 * @param string $name
	 * @param string $extention
	 * @return mixed
	 */
	private function & makeGZip(&$buf, $ratio=0, $name='', $extention=''){
		if ($ratio === 0) return $buf;
		$bufziped = gzcompress($buf, $ratio);
		$bufziped = pack('cccccccc',0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00)
        	.substr($bufziped, 0, -4)
        	.pack('V',crc32($buf))
        	.pack('V',strlen($buf));
        header('Content-Encoding: gzip');
        if ($name && $extention){
        	header('Content-description: File Transfer');
        	header('Content-type: application/x-gzip');
        	header('Content-length: '.strlen($bufziped));
        	header('Content-Disposition: attachment; filename='.$name.'.'.$extention.'.gz');
        }
		return $bufziped;
	}

	/**
	 * Парсит буффер в поисках ссылок и заменяет их
	 *
	 * @param mixed $buf
	 * @return mixed
	 */
	private function & makeURL(&$buf){
		$search = "~([actionhrefsrclocationbackground]) = [\"|'] /? ([/.a-z0-9_-]+) . (w+) ?? (S*?)? (#[^'\"]*)? [\"|']~six";
		$replace = array($this, "_makeURL");
		return preg_replace_callback($search, $replace, $buf);
	}

	/**
	 * Пересобирает ссылки из полученного массива call_back_func
	 *
	 * @return string
	 */
	private final function _makeURL($url){
		$string  = $url[1]."="http://".$_SERVER['SERVER_NAME'];
		$string .= $_SERVER['SERVER_PORT']!= 80 ? ":".$_SERVER['SERVER_PORT']."/" : "/";
		$string .= ($url[2]!="index") ? $url[2] : "";
		$string .= ($url[3]!="php") ? ".".$url[3] : (($url[2]!="index") ? "/" : "");
		parse_str($url[4],$query);
		foreach ($query as $val)
			$string .= $val ? $val."/" : "";
		$string .= $url[5]."\"";

		return $string;
	}

	/**
	 * Подсвечивает текст
	 *
	 * @param mixed $buf
	 * @param array|string $matches
	 * @param string $tag
	 * @param string $color
	 * @return mixed
	 */
	private function & makeHighlight(&$buf, $matches, $tag = 'b', $color = 'ffff00'){
		if (!$matches) return $buf;
		$matches = (array)$matches;
		foreach ($matches as $match)
			if($match)
				$array[] = preg_quote($match);
		if (!$array) return $buf;
		return preg_replace('#(?!<.*)(?<!w)('.implode('|',$array).')(?!w|[^<>]*>)#i', '<'.$tag.' style="background-color:#'.$color.'">1</'.$tag.'>', $buf);
	}

	/**
	 * парсит и выводит буфер
	 *
	 */
	public function __destruct(){
		$this->flush();
	}
}

Из исходного кода видно что класс помимо разруливания буферизации и обработки буфера callback функциями имеет три базовые метода для обработки страницы.

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


$cach = new cache();
echo "Hello World!";
$cach->flush();

Этот кусок кода вернет браузеру всем уже порядком надоевшую gzip-ированую строку «Hello World!». Если же нужно вернуть не html страницу а например файл, то пример придется немного переделать.


$cache = new cache(false);
$cache->start("makeGZip", array(5,"file","txt"));
echo "Hello World!";
$cach->flush();

Надо заметить что функция $cache->start() начинает кэширование с выводом в файл. При этом она принимает параметрами имя callback функции и массив ее параметров: степень сжатия, имя файла и его расширение. В результате мы получим файл file.txt.gz, с все той же надоевшей надписью про «Хэлоу Ворлд».

Все страницы этого сайта в том числе и rss-поток новостей сжаты этой функцией, а реальный пример сжатия в файл можно посмотреть на Google Site Map для сайта.

Второй идет функция для переписывания ссылок в человекочитаемую форму, например: «/page.php?foo=bar» превратится в «example.com/page/bar/». Я долгое время искал оптимальный вариант регулярного выражения для замены ссылок, придумывая самые разные комбинации, которые бы могли покрыть все виды ссылок, как например «/file.php?foo[0]=bar».


$search = "~(action|href|src|location|background)=(\"|') (/)? ([/.a-z0-9_-]+) .(w+) ?? (?: (w+) = (w+?))? (?: & (w+) = (w+?))? (?: & (S*?))? (#[-a-zа-я0-9_ ]*)?(\"|')~six";
$search = "~(action|href|src|location|background)=(\"|') (/)? ([/.a-z0-9_-]+) .(w+) ?? (?: ([a-z0-9[]]+) = (w+?))? (?: & ([a-z0-9[]]+) = (w+?))? (?: & (S*?))? (#[-a-zа-я0-9_ ]*)?(\"|')~six";
$search = "~(action|href|src|location|background)=(\"|') (/)? ([/.a-z0-9_-]+) .(w+) ?? (?: ([a-z0-9[]]+) = (w+?))? (?: & ([a-z0-9[]]+) = (w+?) (?: & ([a-z0-9[]]+) = (w+?) (?: & ([a-z0-9[]]+) = (w+?) (?: & ([a-z0-9[]]+) = (w+?)?)?)?)?)? (#[-a-zа-я0-9_ ]*)?(\"|')~six";

Но все они были недостаточно хороши, одни не покрывали массивы, другие могли распарсить ограниченное количество пар key=value в query string, а третьи были очень медлительны. А потом я решил, что занимаюсь не тем, регулярки не должны парсить урлы, они должны их только находить! и я решил переложить все на call-back функцию, в результате чего получился вот такой код, который был размазан для удобочитаемости


private function & makeURL(&$buf){
	return preg_replace_callback("~([actionhrefsrclocationbackground]) = [\"|'] /? ([/.a-z0-9_-]+) . (w+) ?? (S*?)? (#[^'\"]*)? [\"|']~six", array($this, "_makeURL"), $buf);
}

Я думаю надо объяснить эту функцию на примере, так как все мои знакомые программисты не смогли понять ее смысла и красоты. Функция ищет внутренние ссылки и отравляет результат другой функции, которая собственно над ними издевается, как хочет. Вторая функция принимает массив из 5 элементов. Попробую описать это таблицей на примере ссылки <a href = «/page.php?name=foo&param[0]=bar#anchor»>link</a> и картинки <img src = «/path/to/image.pic.jpg»>

Патерн Link Image
1 ([actionhrefsrclocationbackground]) href src
2 ([/.a-z0-9_-]+) page /path/to/image.pic
3 (w+) php jpg
4 (S*?) name=foo&param[0]=bar
5 (#[^'»]*) #anchor

Тут есть одно замечание — actionhrefsrclocationbackground на самом деле не что иное как описание местонахождения ссылки action, href, src, location, background, все это можно было бы написать через палку | но так работает быстрее. Более того если убрать action то он все равно будет заменяться так как слово location содержит все буквы слова action. Учитывая, что повторяющиеся буквы можно убрать и поставить в алфавитном порядке abcdefghiklnorstu все равно будет работать, но удобочитаемость пропадет напрочь!

Функция _makeURL получает массив и начинает его склеивать в нужном порядке, самую большую ценность тут представляет парсинг query string и его склейку через слеш, собственно то из-за чего все и затевалось. Можно склеивать не только через слеш и приводить к виду директории, а например приводить к виду «page/bar.html»

В результате получаем <a href=»http://example.com:8080/page/foo/bar#anchor»>link</a> и <img src=»http://example.com:8080/path/to/image.pic.jpg»>

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


#rewrite engine
Options FollowSymLinks -Indexes -Multiviews
RewriteEngine on
RewriteBase /
#RegExp have only 9 back-references
RewriteRule ^(content)(/([0-9]+)(/([0-9]+)(/([0-9]+)(/([a-z_]+))?)?)?)?/?$					index.php?act=$1&year=$3&month=$5&day=$7&name=$9	[NC,L]
RewriteRule ^(content)(/([a-z0-9_-]+)(/([a-z_]+))?)?/?$										index.php?act=$1&cat=$3&name=$5						[NC,L]
RewriteRule ^(user)(/([a-z0-9_-]+)(/([a-z0-9_-]+)(/([a-z0-9_-]+)(/([a-z0-9_-]+))?)?)?)?/?$	index.php?act=$1&name=$3							[NC,L]
RewriteRule ^(message)(/([a-z0-9_-]+))?/?$													index.php?act=$1&msg=$3								[NC,L]
RewriteRule ^(xml)(/([a-z]+)(/([a-z0-9_-]+))?)?/?$											shell.php?act=$1&type=$3&name=$5					[NC,L]
RewriteRule ^(plugins)(/([a-z]+)(/([a-z]+))?)?/?$											shell.php?act=$1&name=$3∂=$5					[NC,L]

Ну и наконец третья функция, она служит для подсветки слов в тексте. Честно признаюсь, регулярное выражение для нее я нарыл в интернете. Функция подсвечивает слова на странице, причем следит, чтобы эти слова не были частью html кода. Приведу пример:


$cache = new cache();
$cache->start("makeHighlight",array(array('class','div'),"i"));
echo "<div class='highlighted'>this div has class 'highlighted'</div>";
В результате получим вот такой html код, естественно сжатый gzip’ом, его же никто не отменял

<div class='highlighted'>this <i style="background-color:#ffff00">div</i> has <i style="background-color:#ffff00">class</i> 'highlighted'</div>
Но это плохой пример, можно смастерить что-то более правдоподобное

$cache = new cache();
$url = parse_url($_SERVER['HTTP_REFERER']);
if (strpos($url['host'],'google') !== false){
	parse_str($url['query'],$query);
	$cache->start("makeHighlight",array(explode(' ',$query['q'])));
}

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

И на последок приведу пример как вставить в класс свою функцию обработки текста, например вы хотите заменять строки по словарю и у вас есть два массива $search и $replace. Попробуем написать простенькую функцию simle_replace которая это будет делать.


$cache = new cache();
$search = array("CSS","PHP","JS");
$replace = array("<acronym title='Cascading Style Sheets'>CSS</acronym>",
		 "<acronym title='PHP Hypertext Preprocessor'>PHP</acronym>",
		 "<acronym title='JavaScript'>JS</acronym>",
		);
// $buf is always first argument
function & simle_replace(&$buf,$search,$replace){
	return str_replace($search,$replace,$buf);
}

$cache->start("simle_replace",array($search,$replace));
Как видите ничего сложного! Приятной работы!