Статья является продолжением материала о том, что такое 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'); }