Full Page Cache,FPC,Magento Enterprise,кеширование,оптимизация,highload, magento

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