
Статья является продолжением материала о том, что такое Full Page Cache в Magento Enterprise и почему он нужен.
Недавно в одном проекте у меня была задача написать модуль всплывающих окон (попапов). Задача состояла в следующем. Админ магазина мог создавать в панели администрирования всплывающее окно с настройками:
- на каких страницах показывать (мультиселект с выбором хендлов)
- кому показывать (мультиселект с выбором сегментов кастомеров)
- время жизни куки
- контент
- и др.
Модуль должен был показывать попапы пользователям и вешать куки, показывающие, что пользователь просмотрел данный попап. Этот попап не должен показываться некоторое время, пока у пользователя установлена соответствующая кука.
Для вывода этого попапа я создал блок Turnkeye_Popup_Block_Popup. Этот блок ищет все попапы, которые нужно показать текущему пользователю на текущей странице. После этого выбирает один для показа и показывает его, устанавливая куку (она устанавливается с помощью Javascript), чтобы не показать повторно.
Но существовала проблема при включенном Full Page Cache: этот блок сохранялся после генерации страницы в кеш. Впоследствии кеш всегда его отображал или не отображал независимо от того, установлена ли кука у пользователя.
Для того, чтобы Full Page Cache выводил мой блок динамически, я определил специальным образом метод getCacheKeyInfo класса блока Turnkeye_Popup_Block_Popup. Здесь мы задаём информацию блока, которую сможем потом использовать (ключи массива, который возвращает getCacheKeyInfo, станут ключами атрибутов плейсхолдера, а значения - значениями атрибутов плейсхолдера). При этом она будет влиять на атрибут cache_id плейсхолдера.
/**
* Get cache key informative items that must be preserved in cache placeholders
* for block to be rerendered by placeholder
*
* @return array
*/
public function getCacheKeyInfo()
{
$items = array(
'handles' => serialize($this->getHandles()),
'customer_segment_ids' => serialize($this->getCustomerSegmentIds()),
'popup_ids' => serialize($this->getPopupIds()),
'popup_id' => (int) $this->getPopupId(),
);
$items = parent::getCacheKeyInfo() + $items;
return $items;
}
Затем я создал файл cache.xml в папке etc моего модуля с содержимым:
<config>
<placeholders>
<turnkeye_popup>
<block>turnkeye_popup/popup</block>
<placeholder>POPUP</placeholder>
<container>Turnkeye_Popup_Model_PageCache_Container_Popup</container>
<cache_lifetime>86400</cache_lifetime>
</turnkeye_popup>
</placeholders>
</config>
После этого я создал класс контейнера Turnkeye_Popup_Model_PageCache_Container_Popup для блока Turnkeye_Popup_Block_Popup:
class Turnkeye_Popup_Model_PageCache_Container_Popup extends Enterprise_PageCache_Model_Container_Abstract
{
/**
* Id of popup chosen to be shown to user this time
*/
protected $_chosenPopup = null;
/**
* Get cache identifier for popup block contents.
* Used only after rendered popup is selected.
*
* @return string
*/
protected function _getCacheId()
{
return 'CONTAINER_POPUP_'
. md5($this->_placeholder->getAttribute('cache_id'))
. '_' . (int) $this->_chosenPopup;
}
/**
* Generates placeholder content before application was initialized and applies it to page content if possible.
* First we get meta-data with list of prepared popup ids. Then we select popup to render and
* check whether we already have that content in cache.
*
* @param string $content
* @return bool
*/
public function applyWithoutApp(&$content)
{
// Find a popup block to be rendered for this user
$this->_chosenPopup = $this->_selectPopupToRender();
if ($this->_chosenPopup) {
$cacheId = $this->_getCacheId();
$block = $this->_loadCache($cacheId);
} else {
// No popups to render - just fill with empty content
$block = '';
}
if ($block !== false) {
$this->_applyToContent($content, $block);
return true;
}
return false;
}
/**
* get exclude popups array
*
* @return array
*/
protected function _getExcludedPopups()
{
$excludedPopups = array();
$popups = $this->_getCookieValue(Turnkeye_Popup_Block_Popup::COOKIE_NAME, null);
if ($popups && is_array($popups)) {
foreach($popups as $popupId) {
$popupId = intval($popupId);
if ($popupId > 0) {
$excludedPopups[$popupId] = $popupId;
}
}
}
return $excludedPopups;
}
/**
* Selects the popups we want to show to the current customer.
* The popups depend on the list of popup ids.
*
* @return array
*/
protected function _selectPopupToRender()
{
$placeholder = $this->_placeholder;
$popupIds = unserialize($placeholder->getAttribute('popup_ids'));
$nonExcludedPopupIds = array_diff($popupIds, $this->_getExcludedPopups());
return count($nonExcludedPopupIds) > 0 ? $nonExcludedPopupIds[array_rand($nonExcludedPopupIds)] : null;
}
/**
* Render block content from placeholder
*
* @return string|false
*/
protected function _renderBlock()
{
/**
* @var $block Turnkeye_Popup_Block_Popup
*/
$block = $this->_getPlaceHolderBlock();
$placeholder = $this->_placeholder;
$serializedParameters = array('handles', 'customer_segment_ids', 'popup_ids');
foreach ($serializedParameters as $parameter) {
$value = unserialize($placeholder->getAttribute($parameter));
$block->setDataUsingMethod($parameter, $value);
}
$parameters = array('template');
foreach ($parameters as $parameter) {
$value = $placeholder->getAttribute($parameter);
$block->setDataUsingMethod($parameter, $value);
}
$renderedInfo = $block->renderAndGetInfo();
$renderedParams = $renderedInfo['params'];
$this->_chosenPopup = $renderedParams['popup_id']; // Later _getCacheId() will use it
return $renderedInfo['html'];
}
}
А теперь подробно о том, что здесь к чему.
Я могу использовать значения атрибутов плейсхолдера handles, customer_segment_ids и popup_ids в этом контейнере (они установлены благодаря методу getCacheKeyInfo блока), не вычисляя их заново. Дело в том, что cache_id страницы зависит от URL страницы и сегментов клиента. От URL зависит, какие хендлы лейаута применились на этой странице. И cache_id блока зависит от кастомеров клиента и от сегментов кастомера. Ну а popup_ids зависит от этих двух параметров.
FPC сначала пытается применить контент блока к общему контенту (заменить регулярным выражением контент) методом applyWithoutApp. Если он возвращает false, то методом applyInApp.
В методе applyWithoutApp я определяю, какой попап нужно показывать (определяю popup_id). Если попап не нашли, то применяем пустой контент (т.к. попап выводить не надо). Если нашли попап, который нужно показывать, то происходит попытка загрузки из кеша. Если есть в кеше - применяем этот контент, иначе выполнится applyInApp (в котором контент вернёт метод _renderBlock).
В методе _renderBlock я задаю блоку handles, customer_segment_ids и popup_ids (чтобы блок не высчитывал их заново, мы берём их из атрибутов плейсхолдера), а после того, как был отрендерен блок, я устанавливаю свойство $_chosenPopup контейнеру, которое будет использовано для генерации cache_id при сохранении в кеш.
Для более полной картины приведу несколько методов блока Turnkeye_Popup_Block_Popup
Метод renderAndGetInfo. Возвращает не только html-содержимое блока, но и дополнительные параметры. Правда, в данном случае нужен был лишь один параметр. Он задаётся в момент вызова метода _toHtml
/**
* Clears information about rendering process parameters.
*
* @return Turnkeye_Popup_Block_Popup
*/
protected function _clearRenderedParams()
{
$this->_renderedParams = array();
return $this;
}
public function getRenderedParams()
{
return $this->_getRenderedParams();
}
/**
* Returns parameters about last popup rendering that this block has performed.
* Used to know the information about process this block implemented to choose popups depending on
* customer and select one of them to render.
*
* @return array
*/
protected function _getRenderedParams()
{
return $this->_renderedParams;
}
/**
* Sets rendered param information
*
* @param string $key
* @param mixed $value
* @return Turnkeye_Popup_Block_Popup
*/
protected function _setRenderedParam($key, $value)
{
$this->_renderedParams[$key] = $value;
return $this;
}
/**
* Clears information about rendering process parameters and renders block (new parameters are filled
* during this process).
*
* @return string
*/
protected function _toHtml()
{
$this->_clearRenderedParams();
$this->_setRenderedParam('popup_id', (int) $this->getPopupId());
if (!$this->getPopupId()) {
return '';
}
return parent::_toHtml();
}
/**
* Returns rendered html and information about data used to render the popups.
* Used by cache placeholder to get html and additional data about it, so later cache placeholder
* can make some actions on its own.
*
* @return array
*/
public function renderAndGetInfo()
{
$result = array(
'html' => $this->toHtml(),
'params' => $this->_getRenderedParams()
);
return $result;
}
Массив попапов, из которых нужно выбрать один, получаем следующим образом:
public function getPopupIds()
{
if (!$this->hasData('popup_ids')) {
$popupIds = $this->getPopupCollection()->getAllIds(); //array_keys($this->getPopupCollection()->getItems());
$this->setData('popup_ids', $popupIds);
}
return $this->getData('popup_ids');
}
Это нужно для того, чтобы можно было установить popup_ids из контейнера FPC этого блока (см. метод _renderBlock контейнера) и не вычислять заново массив попапов.
Остальные параметры, которые нет смысла вычислять заново, получаются точно так же. К примеру, массив хендлов, которые сработали на данной странице, я получаю так. Метод getAllHandles класса Turnkeye_Popup_Model_Source_Handles возвращает массив хендлов, которые можно выбрать для попапа.
public function getHandles()
{
if (!$this->hasData('handles')) {
/**
* @var $handlesSource Turnkeye_Popup_Model_Source_Handles
*/
$handlesSource = Mage::getModel('turnkeye_popup/source_handles');
$handles = array_intersect($handlesSource->getAllHandles(), $this->getLayout()->getUpdate()->getHandles());
sort($handles);
$this->setData('handles', $handles);
}
return $this->getData('handles');
}
