Welcome to php club

Кэширование и PHP

Как мне управлять кэшированием на стороне клиента средствами PHP?

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


Новые имена функций


Если вы используете PHP 4.3.0 с Apache, HTTP-заголовки доступны функцией apache_request_headers и apache_response_headers. Функция getallheaders стала псевдонимом для новой функции apache_request_headers.


Механизмом для работы с кэшем web-браузера вновь является HTTP. Множество заголовков вовлечёны в инструктирование web-браузеров и прокси-серверов независимо кэшировать страницу, ситуация осложняется тем фактом, что некоторые из них доступны только с HTTP 1.1.


Проверка HTTP-заголовков в вашем браузере


Простым но очень удобным инструментом для проверки заголовков запросов и откликов является Live Http Headers? – аддон к браузеру Mozilla. Необходимо точно знать, какие заголовки посылает ваш скрипт, особенно когда вы имеете дело с заголовками кэширования HTTP.


Для простоты мы рассмотрим только заголовки кэширования HTTP 1.0, а именно Expires, Last-Modified и If-Modified-Since, а также статус-код HTTP 304 (Not Modified).


Другие заголовки, доступные с HTTP 1.1, например Cache-Control и ETag, предназначены для обеспечения расширенного механизма, который может использоваться совместно с состоянием web-сессии, иными словами, версия данной страницы, отображаемой неавторизованному посетителю, может значительно отличаться от отображаемой авторизованному пользователю. Заголовки HTTP 1.1 изначально добавлялись для того, чтобы позволить кэшировать такие страницы.


Истечение срока жизни страницы


Самым простым в использовании заголовком является заголовок Expire, который устанавливает дату (возможно, будущую), когда страница устареет. До этого момента web-браузеру разрешается использовать кэшированную версию страницы.


Пример:


Пример 5.15. 6.php

<?php
/**
* Посылает заголовок Expires HTTP 1.0.
* @param int количество секунд до времени истечения срока жизни
*/
function setExpires($expires){
  
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . 'GMT');
}

// Устанавливаем заголовок времени истечения срока жизни Expires
setExpires(10);

// Отображаем
echo 'Эта страница самоуничтожится через 10 секунд<br />';
echo
'Сейчас ' . gmdate('H:i:s') . ' GMT<br />';
echo
'<a href="' . $_SERVER['PHP_SELF'] . '">Посмотреть вновь </a><br />';
?>

Функция setExpires отправляет заголовок HTTP Expires с будущим временем, заданном в секундах. Вышеприведённый пример показывает текущее время по Гринвичу и выводит ссылку, которая вам позволяет перейти на страницу вновь. Используя кнопку Refresh вашего браузера, вы можете сообщить браузеру о желании обновить кэш. Используя ссылку, вы увидите, что время изменяется только раз в 10 секунд.


Даты и время в HTTP


Даты в HTTP всегда вычисляются относительного меридиана времени Гринвича (GMT). Функция PHP gmdate точно такая же функция, как date, за исключением того, что она автоматически компенсирует время по Гринвичу, основанное на системных часах и настройках региона вашего сервера.


Когда браузер сталкивается с заголовком Expires, он кэширует страницу. Все последующие запросы страницы, сделанные до указанного времени истечения срока жизни, используют версию страницы из кэша, никаких запросов к web-серверу при этом не происходит.


Заголовок Expires преимущественно прост в реализации, но в большинстве случаев, если вы не высокоорганизованный человек, вы не можете знать точно, когда данная страница вашего сайта обновлена. Поскольку браузер войдёт в контакт с сервером только после того, как страница устареет, нет ни одного способа сообщить браузеру, что страница, находящаяся в его кэше, устарела. Вы также теряете некоторую часть трафика к вашему web-сайту, поскольку браузер не обращается к серверу при запросе страницы из кэша.


Время изменения страницы


Более практично использовать заголовки Last-Modified и If-Modified-Since, доступные в HTTP 1.0. Технически он известно как выполнение условного GET-запроса, вы возвращаете любой контент, основываясь на условии пришедшего заголовка запроса If-Modified-Since.


При использовании этого метода вы должны отправлять заголовок Last-Modified каждый раз, когда обращаются к вашему PHP-скрипту. При следующем запросе страницы браузером, он отправит заголовок If-Modified-Since, содержащий время, по которому ваш скрипт может определить, обновлялась ли страница со времени последнего запроса. Если это не так, ваш скрипт посылает код статуса HTTP 304, чтобы указать, что страница не изменялась, не выводя при этом содержимого страницы.


Простейший пример условного GET довольно мудрёный, довольно удобное средство, чтобы показать как это работает – PEAR::Cache_Lite. Однако не следует считать, что это пример серверного кэширования, это просто предусматривает файл, периодически модифицирующийся.


Вот код:


Пример 5.16. 7.php (начало)

<?php
// Подключаем PEAR::Cache_Lite
require_once 'Cache/Lite.php';

// Определяем настройки Cache_Lite
$options = array(
'cacheDir' => './cache/'
);

// Инициализируем Cache_Lite
$cache = new Cache_Lite($options);

// Некоторые фиктивные данные для хранения
$id = 'MyCache';

// Инициализируем кэш, если страница запрошена впервые
if (!$cache->get($id)) {
$cache->save('Dummy', $id);
}

// Рандомизатор…
$random = array(0, 1, 1);
shuffle($random);

// Произвольное обновление кэша
if ($random[0] == 0) {
$cache->save('Dummy', $id);
}

// Получаем время последней модификации кэш-файла
$lastModified = filemtime($cache->_file);

// Выдаём заголовок HTTP Last-Modified
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');

// Получаем заголовки запроса клиента – только для Apache
$request = getallheaders();

if (isset(
$request['If-Modified-Since'])) {
// Разделяем If-Modified-Since (Netscape < v6 отдаёт их неправильно)
$modifiedSince = explode(';', $request['If-Modified-Since']);

// Преобразуем запрос клиента If-Modified-Since в таймштамп
$modifiedSince = strtotime($modifiedSince[0]);
} else {
// Устанавливаем время модификации в ноль
$modifiedSince = 0;
}

// Сравниваем время последней модификации контента с кэшем клиента
if ($lastModified <= $modifiedSince) {
// Разгружаем канал передачи данных!
header('HTTP/1.1 304 Not Modified');
exit();
}

echo
'Сейчас ' . gmdate('H:i:s') . ' по Гринвичу<br />';
echo
'<a href="' . $_SERVER['PHP_SELF'] . '">Обновить</a><br />';
?>

Не забудьте пользоваться ссылкой «Обновить» при запуске этого примера (нажатие Refresh’а обычно очищает кэш вашего браузера). Если вы кликните по ссылке неоднократно, в конечном итоге кэш будет изменён, ваш браузер удалит версию из кэша и сохранит в нём новую страницу, предоставленную PHP.


В вышеприведённом примере мы использовали PEAR::Cache_Lite для создания произвольно модифицируемого кэш-файла. Мы устанавливаем время модификации кэш-файла этой строкой:

<?php
$lastModified
= filemtime($cache->_file);
?>

Говоря техническим языком, это хак, поскольку переменная $_file класса PEAR::Cache_Lite должна быть приватной. Тем не менее мы вынуждены её использовать, чтобы получить имя кэш-файла и узнать его время модификации.


Затем, используя время модификации кэш-файла, мы посылаем заголовок Last-Modified. Нам нужно посылать её для каждой предоставляемой страницы, чтобы вынудить браузер посылать нам заголовок If-Modified-Since с каждым запросом.

<?php
// Выдаём заголовок HTTP Last-Modified
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
?>

Использование функции getallheaders обеспечивает нам получение от PHP всех входящих заголовков в виде массива. Затем мы должны проверить, что заголовок If-Modified-Since действительно существует, если он есть, мы должны обработать специальный случай старых версий Mozilla (нижа 6й версии), который добавлял в конец (отклоняясь от спецификации) дополнительное поле к заголовку If-Modified-Since. Используя функцию PHP strtotime, мы получаем таймштамп даты, переданной нам браузером. Если такого заголовка нет, мы присваиваем таймштампу ноль, вынуждая таким образом PHP отдать посетителю последнюю версию страницы.
<?php
// Получаем заголовки запроса клиента – только для Apache
$request = getallheaders();

if (isset(
$request['If-Modified-Since'])) {
  
// Разделяем If-Modified-Since (Netscape < v6 отдаёт их неправильно)
   
$modifiedSince = explode(';', $request['If-Modified-Since']);

   
// Преобразуем запрос клиента If-Modified-Since в таймштамп
   
$modifiedSince = strtotime($modifiedSince[0]);
} else {
  
// Устанавливаем время модификации в ноль
   
$modifiedSince = 0;
}
?>

Наконец, мы проверяем, был ли модифицирован кэш с тех пор как посетитель получал эту страницу в последний раз. Если это не так, мы просто посылаем в заголовке отклик Not Modified и прекращаем выполнение скрипта, не нагружая канал передачи данных и экономя процессорное время, инструктируя браузер отобразить кэшированную версию страницы.
<?php
// Сравниваем время последней модификации контента с кэшем клиента
if ($lastModified <= $modifiedSince) {
// Разгружаем канал передачи данных!
header('HTTP/1.1 304 Not Modified');
exit();
}
?>

Если вы объедините подход времени последнего изменения со значением времени, являющимся уже доступным в вашем приложении (например, время самой последней новостной статьи, или время устаревания из системы серверного кэширования, которое мы видели в последнем решении), вы сможете воспользоваться преимуществами кэша web-браузера и разгрузите канал передачи данных, по возможности сэкономив информационный трафик с вашего сайта и улучшив его воспринятую производительность.


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


 
Один комментарий. [Показать комментарии/форму]