Magento Enterprise: Что такое Full Page Cache и почему он нужен

в 11:33, , рубрики: cms, ecommerce, FPC, highload, Magento, magento ecommerce, Magento Enterprise, open source, php, Веб-разработка, кеширование, оптимизация, электронная коммерция, метки: , , , , , , , , , , , ,

Full Page Cache

Magento Enterprise: Что такое Full Page Cache и почему он нужен.

Для тех, кто знаком с Magento, не секрет, что этот e-commerce движок довольно требователен к железу. Но разработчики этого интернет-магазина попытались решить эту проблему и придумали много различного рода «ускорялок», без которых, пожалуй, запускать магазин на движке Magento в продакшн не стоит. Слишком долго Magento будет отдавать конечному пользователю страницу. Среди таких «ускорялок» кеши, индексы, компиляция, объединение JS/CSS в один сжатый файл и др.

Одной из основных «фишек» Magento Enterprise является Full Page Cache (далее FPC). Эту «фишку» реализует модуль Enterprise_PageCache, входящий в состав пакета Magento Enterprise.

В статье рассматривается самая свежая на момент написания статьи версия Magento Enterprise: 1.13.1.

FPC позволяет отдавать серверу страницу за считанные миллисекунды, практически не нагружая сервер. Я провел замеры времени отдачи страницы продукта сервером (в одном из проектов, над которым работал), вот результаты:

FPC

  • 65 ms при включенном FPC (когда все блоки были закешированы);
  • 1250 ms при выключенном FPC (при этом все остальные виды кеша включены);
  • 2500 ms при отключенном кеше всех видов.

Почему разница настолько велика? Давайте разберёмся.

Что и когда кеширует FPC.

Как следует из названия, Full Page Cache кеширует полностью всю страницу. Но кеширует не все страницы. Как минимум потому, что для этого нет смысла. По умолчанию кешируются только страницы продукта, страницы категории, CMS-страницы и страница с 404 ошибкой (страница не найдена). В этом можно убедиться, посмотрев конфигурацию модуля (config.xml):

<frontend>
    <cache>
        <requests>
            <_no_route>enterprise_pagecache/processor_noroute</_no_route>
            <cms>enterprise_pagecache/processor_default</cms>
            <catalog>
                <category>
                    <view>enterprise_pagecache/processor_category</view>
                </category>
            </catalog>
            <catalog>
                <product>
                    <view>enterprise_pagecache/processor_product</view>
                </product>
            </catalog>
        </requests>
    </cache>
</frontend>

В секции frontend/cache/requests здесь указывается frontName контроллера модуля (его значение хранится в конфиге модуля по пути: frontend/routers/route_name/args/frontName), затем контроллер и экшен (контроллер и экшен — необязательные параметры). А передаваемое значение — процессор реквеста (запроса).

Вы можете точно так же заставить кешировать контроллеры/контроллер/экшен вашего модуля, достаточно добавить в нужную секцию данные аналогично тому, как это делает модуль Enterprise_PageCache. Пример:

<frontend>
    <cache>
        <requests>
            <productinfo>
                <index>enterprise_pagecache/processor_default</index>
            </productinfo>
        </requests>
    </cache>
</frontend>

При этом есть ряд условий, при которых FPC не кеширует страницы. Не кешируются, к примеру, HTTPS страницы, страницы с GET-параметром no_cache. Методы canProcessRequest и isAllowed класса Enterprise_PageCache_Model_Processor:

/**
 * Do basic validation for request to be cached
 *
 * @param Zend_Controller_Request_Http $request
 * @return bool
 */
public function canProcessRequest(Zend_Controller_Request_Http $request)
{
    $res = $this->isAllowed();
    $res = $res && Mage::app()->useCache('full_page');
    if ($request->getParam('no_cache')) {
        $res = false;
    }
 
    if ($res) {
        $maxDepth = Mage::getStoreConfig(self::XML_PATH_ALLOWED_DEPTH);
        $queryParams = $request->getQuery();
        unset($queryParams[Enterprise_PageCache_Model_Cache::REQUEST_MESSAGE_GET_PARAM]);
        $res = count($queryParams)
/**
 * Check if processor is allowed for current HTTP request.
 * Disable processing HTTPS requests and requests with "NO_CACHE" cookie
 *
 * @return bool
 */
public function isAllowed()
{
    if (!$this->_requestId) {
        return false;
    }
    if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
        return false;
    }
    if (isset($_COOKIE['NO_CACHE'])) {
        return false;
    }
    if (isset($_GET['no_cache'])) {
        return false;
    }
    if (isset($_GET[Mage_Core_Model_Session_Abstract::SESSION_ID_QUERY_PARAM])) {
        return false;
    }
    if (!Mage::app()->useCache('full_page')) {
        return false;
    }
 
    return true;
}

Как работает FPC.

Если страница кешируется FPC-кешем, то FPC отключает стандартный кеш блоков. См. метод processPreDispatch класса Enterprise_PageCache_Model_Observer, который срабатывает, когда возникает событие (event) controller_action_predispatch:

/**
 * Check when cache should be disabled
 *
 * @param Varien_Event_Observer $observer
 * @return Enterprise_PageCache_Model_Observer
 */
public function processPreDispatch(Varien_Event_Observer $observer)
{
    if (!$this->isCacheEnabled()) {
        return $this;
    }
    $action = $observer->getEvent()->getControllerAction();
    /* @var $request Mage_Core_Controller_Request_Http */
    $request = $action->getRequest();
 
    $noCache = $this->_getCookie()->get(Enterprise_PageCache_Model_Processor::NO_CACHE_COOKIE);
    if ($noCache) {
        Mage::getSingleton('catalog/session')->setParamsMemorizeDisabled(false);
        $this->_getCookie()->renew(Enterprise_PageCache_Model_Processor::NO_CACHE_COOKIE);
    } elseif ($action) {
        Mage::getSingleton('catalog/session')->setParamsMemorizeDisabled(true);
    }
    /**
     * Check if request will be cached
     */
    if ($this->_processor->canProcessRequest($request)) {
        Mage::app()->getCacheInstance()->banUse(Mage_Core_Block_Abstract::CACHE_GROUP);
    }
    $this->_getCookie()->updateCustomerCookies();
    return $this;
}

FPC генерирует для каждого HTTP-запроса свой идентификатор кеша, который будет использован для сохранении страницы в кеш. Идентификатор кеша зависит от нескольких параметров: авторизован кастомер или нет, к каким сегментам он относится, к какой группе пользователей относится и пр. Посмотреть, как генерируется этот идентификатор, можно в методе _createRequestIds класса Enterprise_PageCache_Model_Processor:

/**
 * Populate request ids
 * @return Enterprise_PageCache_Model_Processor
 */
protected function _createRequestIds()
{
    $uri = $this->_getFullPageUrl();
 
    //Removing get params
    $pieces = explode('?', $uri);
    $uri = array_shift($pieces);
 
    /**
     * Define COOKIE state
     */
    if ($uri) {
        if (isset($_COOKIE['store'])) {
            $uri = $uri.'_'.$_COOKIE['store'];
        }
        if (isset($_COOKIE['currency'])) {
            $uri = $uri.'_'.$_COOKIE['currency'];
        }
        if (isset($_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_GROUP])) {
            $uri .= '_' . $_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_GROUP];
        }
        if (isset($_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_LOGGED_IN])) {
            $uri .= '_' . $_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_LOGGED_IN];
        }
        if (isset($_COOKIE[Enterprise_PageCache_Model_Cookie::CUSTOMER_SEGMENT_IDS])) {
            $uri .= '_' . $_COOKIE[Enterprise_PageCache_Model_Cookie::CUSTOMER_SEGMENT_IDS];
        }
        if (isset($_COOKIE[Enterprise_PageCache_Model_Cookie::IS_USER_ALLOWED_SAVE_COOKIE])) {
            $uri .= '_' . $_COOKIE[Enterprise_PageCache_Model_Cookie::IS_USER_ALLOWED_SAVE_COOKIE];
        }
        $designPackage = $this->_getDesignPackage();
 
        if ($designPackage) {
            $uri .= '_' . $designPackage;
        }
    }
 
    $this->_requestId       = $uri;
    $this->_requestCacheId  = $this->prepareCacheId($this->_requestId);
 
    return $this;
}

FPC использует плейсхолдеры. Они в Magento добавляются с помощью файла конфигурации cache.xml в папке etc модуля. А плейсхолдер обрабатывается контейнером плейсхолдера. Контейнер — это класс, который будет обрабатывать плейсхолдер во время рендеринга FPC (процесс рендеринга блока FPC-кешем отличается от обычного рендеринга блока). Пример файла cache.xml:

<?xml version="1.0" encoding="UTF-8"?>
    <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>

Кратко о конфигурации плейсхолдера:

  • block — тип или класс блока
  • placeholder — название плейсхолдера
  • container — тип или класс контейнера плейсхолдера
  • cache_lifetime — время жизни кеша

Во время рендеринга при включенном FPC содержимое каждого блока оборачивается плейсхолдером и кешируется: когда срабатывает событие core_block_abstract_to_html_after, выполняется метод renderBlockPlaceholder класса Enterprise_PageCache_Model_Observer:

/**
 * Render placeholder tags around the block if needed
 *
 * @param Varien_Event_Observer $observer
 * @return Enterprise_PageCache_Model_Observer
 */
public function renderBlockPlaceholder(Varien_Event_Observer $observer)
{
    if (!$this->_isEnabled) {
        return $this;
    }
    $block = $observer->getEvent()->getBlock();
    $transport = $observer->getEvent()->getTransport();
    $placeholder = $this->_config->getBlockPlaceholder($block);
 
    if ($transport && $placeholder && !$block->getSkipRenderTag()) {
        $blockHtml = $transport->getHtml();
 
        $request = Mage::app()->getFrontController()->getRequest();
        /** @var $processor Enterprise_PageCache_Model_Processor_Default */
        $processor = $this->_processor->getRequestProcessor($request);
        if ($processor && $processor->allowCache($request)) {
            $container = $placeholder->getContainerClass();
            if ($container && !Mage::getIsDeveloperMode()) {
                $container = new $container($placeholder);
                $container->setProcessor(Mage::getSingleton('enterprise_pagecache/processor'));
                $container->setPlaceholderBlock($block);
                $container->saveCache($blockHtml);
            }
        }
 
        $blockHtml = $placeholder->getStartTag() . $blockHtml . $placeholder->getEndTag();
        $transport->setHtml($blockHtml);
    }
    return $this;
}

Здесь $placeholder получается при помощи класса Enterprise_PageCache_Model_Config методом getBlockPlaceholder:

/**
 * Create placeholder object based on block information
 *
 * @param Mage_Core_Block_Abstract $block
 * @return Enterprise_PageCache_Model_Container_Placeholder
 */
public function getBlockPlaceholder($block)
{
    $this->_initPlaceholders();
    $type = $block->getType();
 
    if (isset($this->_placeholders[$type])) {
        $placeholderData = false;
        foreach ($this->_placeholders[$type] as $placeholderInfo) {
            if (!empty($placeholderInfo['name'])) {
                if ($placeholderInfo['name'] == $block->getNameInLayout()) {
                    $placeholderData = $placeholderInfo;
                }
            } else {
                $placeholderData = $placeholderInfo;
            }
        }
 
        if (!$placeholderData) {
            return false;
        }
 
        $placeholder = $placeholderData['code']
            . ' container="' . $placeholderData['container'] . '"'
            . ' block="' . get_class($block) . '"';
        $placeholder.= ' cache_id="' . $block->getCacheKey() . '"';
 
        if (!empty($placeholderData['cache_lifetime'])) {
            $placeholder .= ' cache_lifetime="' . $placeholderData['cache_lifetime'] . '"';
        }
 
        foreach ($block->getCacheKeyInfo() as $k => $v) {
            if (is_string($k) && !empty($k)) {
                $placeholder .= ' ' . $k . '="' . $v . '"';
            }
        }
        $placeholder = Mage::getModel('enterprise_pagecache/container_placeholder', $placeholder);
        return $placeholder;
    }
    return false;
}

Конструктор класса Enterprise_PageCache_Model_Container_Placeholder:

/**
 * Class constructor.
 * Initialize placeholder name and attributes based on definition
 *
 * @param string $definition
 */
public function __construct($definition)
{
    if ($definition && array_key_exists($definition, self::$_definitionMap)) {
        $definition = self::$_definitionMap[$definition];
    }
    $this->_definition = $definition;
    $definition     = explode(' ', $definition);
    $this->_name    = $definition[0];
    $count = count($definition);
    if ($count>1) {
        for ($i=1; $i_attributes[$info[0]] = isset($info[1]) ? trim($info[1], '"'') : null;
        }
    }
}

В будущем в классе контейнера можно будет получить атрибуты блока методом getAttribute:

/**
 * Get attribute by specific code
 * @param $code string
 * @return string
 */
public function getAttribute($code)
{
    return isset($this->_attributes[$code]) ? $this->_attributes[$code] : null;
}

Методы для обёртки плейсхолдером контента блока:

/**
 * Retrieve placeholder definition hash
 *
 * @return string
 */
protected function _getDefinitionHash()
{
    $definition = $this->getDefinition();
    $result = array_search($definition, self::$_definitionMap);
    if ($result === false) {
        $result = $this->getName() . '_' . md5($definition);
        self::$_definitionMap[$result] = $definition;
    }
    return $result;
}
 
/**
 * Get placeholder start tag for block html generation
 *
 * @return string
 */
public function getStartTag()
{
    return '<!--{' . $this->_getDefinitionHash() . '}-->';
}
 
/**
 * Get placeholder end tag for block html generation
 *
 * @return string
 */
public function getEndTag()
{
    return '<!--/{' . $this->_getDefinitionHash() . '}-->';
}

Пример обёртки плейсхолдером контента блока:

< !--{TOPMENU_21c7f778e21d072d331836703b6295f5}-->
< div class="nav-container">
    < ul id="nav">
        < li class="level0 nav-1 first last level-top">
            < a class="level-top" href="http://example.com/test-category.html">
                < span>Test category</span>
            </a>
        </li>
    </ul>
</div>
< !--/{TOPMENU_21c7f778e21d072d331836703b6295f5}-->

Перед тем, как производится отправка ответа на запрос, срабатывавает событие controller_front_send_response_before и выполняется метод cacheResponse класса Enterprise_PageCache_Model_Observer, который сохраняет страницу в кеш, если это необходимо. Код метода cacheResponse:

/**
 * Save page body to cache storage
 *
 * @param Varien_Event_Observer $observer
 * @return Enterprise_PageCache_Model_Observer
 */
public function cacheResponse(Varien_Event_Observer $observer)
{
    if (!$this->isCacheEnabled()) {
        return $this;
    }
    $frontController = $observer->getEvent()->getFront();
    $request = $frontController->getRequest();
    $response = $frontController->getResponse();
    $this->_saveDesignException();
    $this->_processor->processRequestResponse($request, $response);
    return $this;
}

Здесь метод processRequestResponse сохраняет всю страницу и нужные данные в кеш, если это нужно:

/**
 * Process response body by specific request
 *
 * @param Zend_Controller_Request_Http $request
 * @param Zend_Controller_Response_Http $response
 * @return Enterprise_PageCache_Model_Processor
 */
public function processRequestResponse(Zend_Controller_Request_Http $request,
    Zend_Controller_Response_Http $response
) {
    // we should add original path info tag as another way we can't drop some entities from cron job
    $this->addRequestTag(Enterprise_PageCache_Helper_Url::prepareRequestPathTag($request->getOriginalPathInfo()));
    $cacheInstance = Enterprise_PageCache_Model_Cache::getCacheInstance();
    /**
     * Basic validation for request processing
     */
    if ($this->canProcessRequest($request)) {
        $processor = $this->getRequestProcessor($request);
        if ($processor && $processor->allowCache($request)) {
            $this->setMetadata('cache_subprocessor', get_class($processor));
 
            $cacheId = $this->prepareCacheId($processor->getPageIdInApp($this));
            $content = $processor->prepareContent($response);
 
            /**
             * Replace all occurrences of session_id with unique marker
             */
            Enterprise_PageCache_Helper_Url::replaceSid($content);
            Enterprise_PageCache_Helper_Form_Key::replaceFormKey($content);
 
            if (function_exists('gzcompress')) {
                $content = gzcompress($content);
            }
 
            $contentSize = strlen($content);
            $currentStorageSize = (int) $cacheInstance->load(self::CACHE_SIZE_KEY);
 
            if (Mage::getStoreConfig(Enterprise_PageCache_Model_Processor::XML_PATH_CACHE_DEBUG)) {
                $response->setBody(implode(', ', $this->getRequestTags()) . $response->getBody());
            }
 
            $maxSizeInBytes = Mage::getStoreConfig(self::XML_PATH_CACHE_MAX_SIZE) * 1024 * 1024;
 
            if ($currentStorageSize >= $maxSizeInBytes) {
                Mage::app()->getCacheInstance()->invalidateType('full_page');
                return $this;
            }
 
            $cacheInstance->save($content, $cacheId, $this->getRequestTags());
 
            $cacheInstance->save(
                $currentStorageSize + $contentSize,
                self::CACHE_SIZE_KEY,
                $this->getRequestTags()
            );
 
            /*
             * Save design change in cache
             */
            $designChange = Mage::getSingleton('core/design');
            if ($designChange->getData()) {
                $cacheInstance->save(
                    serialize($designChange->getData()),
                    $this->getRequestCacheId() . self::DESIGN_CHANGE_CACHE_SUFFIX,
                    $this->getRequestTags()
                );
            }
 
            // save response headers
            $this->setMetadata('response_headers', $response->getHeaders());
 
            // save original routing info
            $this->setMetadata('routing_aliases', Mage::app()->getRequest()->getAliases());
            $this->setMetadata('routing_requested_route', Mage::app()->getRequest()->getRequestedRouteName());
            $this->setMetadata('routing_requested_controller',
                Mage::app()->getRequest()->getRequestedControllerName());
            $this->setMetadata('routing_requested_action', Mage::app()->getRequest()->getRequestedActionName());
 
            $this->setMetadata('sid_cookie_name', Mage::getSingleton('core/session')->getSessionName());
 
            Mage::dispatchEvent('pagecache_processor_metadata_before_save', array('processor' => $this));
 
            $this->_saveMetadata();
        }
 
        if (isset($_GET[Mage_Core_Model_Session_Abstract::SESSION_ID_QUERY_PARAM])) {
            Mage::getSingleton('enterprise_pagecache/cookie')->updateCustomerCookies();
            Mage::getModel('enterprise_pagecache/observer')->updateCustomerProductIndex();
 
        }
    }
    return $this;
}

Здесь очень важный момент заключается в том, что перед сохранением в кеш страницы содержимое блоков с плейсхолдером заменяется на описание блоков: $content = $processor->prepareContent($response);. Метод prepareContent:

/**
 * Prepare response body before caching
 *
 * @param Zend_Controller_Response_Http $response
 * @return string
 */
public function prepareContent(Zend_Controller_Response_Http $response)
{
    return $this->replaceContentToPlaceholderReplacer($response->getBody());
}

Метод replaceContentToPlaceholderReplacer:

/**
 * Replace block content to placeholder replacer
 *
 * @param string $content
 * @return string
 */
public function replaceContentToPlaceholderReplacer($content)
{
    $placeholders = array();
    preg_match_all(
        Enterprise_PageCache_Model_Container_Placeholder::HTML_NAME_PATTERN,
        $content,
        $placeholders,
        PREG_PATTERN_ORDER
    );
    $placeholders = array_unique($placeholders[1]);
    try {
        foreach ($placeholders as $definition) {
            $this->_placeholder = Mage::getModel('enterprise_pagecache/container_placeholder', $definition);
            $content = preg_replace_callback($this->_placeholder->getPattern(),
                array($this, '_getPlaceholderReplacer'), $content);
        }
        $this->_placeholder = null;
    } catch (Exception $e) {
        $this->_placeholder = null;
        throw $e;
    }
    return $content;
}

В итоге в кеше страницы будет содержаться информация о блоке вместо содержимого блока. Эта информация поможет отрендерить блок в будущем. Вот пример такой информации, которую FPC будет заменять на содержимое блока во время своего рендеринга:

< !--{POPUP
container="Turnkeye_Popup_Model_PageCache_Container_Popup"
block="Turnkeye_Popup_Block_Popup"
cache_id="c3b32091a1ebd3b276a8fd70496a8e6da20865d0"
cache_lifetime="86400"
template="turnkeye/popup/popup.phtml"
handles="a:2:{i:0;s:15:"cms_index_index";i:1;s:8:"cms_page";}"
customer_segment_ids="a:1:{i:0;i:0;}"
popup_ids="a:1:{i:0;s:1:"1";}"
excluded_popup_ids="a:1:{i:0;i:1;}"}-->

При следующей загрузке этой страницы Magento загрузит эту страницу из кеша (см. метод extractContent класса Enterprise_PageCache_Model_Processor). Если в кеше пусто — обычный рендеринг с инициализацией Magento (как будто FPC и не было) с последующим сохранением в кеш страницы. Если же страница есть в кеше, то FPC будет обрабатывать контент из этого кеша.

Методы _processContent и _processContainers класса Enterprise_PageCache_Model_Processor:

/**
 * Determine and process all defined containers.
 * Direct request to pagecache/request/process action if necessary for additional processing
 *
 * @param string $content
 * @return string|false
 */
protected function _processContent($content)
{
    $containers = $this->_processContainers($content);
    $isProcessed = empty($containers);
    // renew session cookie
    $sessionInfo = Enterprise_PageCache_Model_Cache::getCacheInstance()->load($this->getSessionInfoCacheId());
 
    if ($sessionInfo) {
        $sessionInfo = unserialize($sessionInfo);
        foreach ($sessionInfo as $cookieName => $cookieInfo) {
            if (isset($_COOKIE[$cookieName]) && isset($cookieInfo['lifetime'])
                && isset($cookieInfo['path']) && isset($cookieInfo['domain'])
                && isset($cookieInfo['secure']) && isset($cookieInfo['httponly'])
            ) {
                $lifeTime = (0 == $cookieInfo['lifetime']) ? 0 : time() + $cookieInfo['lifetime'];
                setcookie($cookieName, $_COOKIE[$cookieName], $lifeTime,
                    $cookieInfo['path'], $cookieInfo['domain'],
                    $cookieInfo['secure'], $cookieInfo['httponly']
                );
            }
        }
    } else {
        $isProcessed = false;
    }
 
    if (isset($_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_FORM_KEY])) {
        $formKey = $_COOKIE[Enterprise_PageCache_Model_Cookie::COOKIE_FORM_KEY];
    } else {
        $formKey = Enterprise_PageCache_Helper_Data::getRandomString(16);
        Enterprise_PageCache_Model_Cookie::setFormKeyCookieValue($formKey);
    }
 
    Enterprise_PageCache_Helper_Form_Key::restoreFormKey($content, $formKey);
 
    /**
     * restore session_id in content whether content is completely processed or not
     */
    $sidCookieName = $this->getMetadata('sid_cookie_name');
    $sidCookieValue = $sidCookieName && isset($_COOKIE[$sidCookieName]) ? $_COOKIE[$sidCookieName] : '';
 
    // XSS vulnerability protection provided by htmlspcialchars call - escape & " ' < > chars
    Enterprise_PageCache_Helper_Url::restoreSid($content, htmlspecialchars($sidCookieValue, ENT_QUOTES));
 
    if ($isProcessed) {
        return $content;
    } else {
        Mage::register('cached_page_content', $content);
        Mage::register('cached_page_containers', $containers);
        Mage::app()->getRequest()
            ->setModuleName('pagecache')
            ->setControllerName('request')
            ->setActionName('process')
            ->isStraight(true);
 
        // restore original routing info
        $routingInfo = array(
            'aliases'              => $this->getMetadata('routing_aliases'),
            'requested_route'      => $this->getMetadata('routing_requested_route'),
            'requested_controller' => $this->getMetadata('routing_requested_controller'),
            'requested_action'     => $this->getMetadata('routing_requested_action')
        );
 
        Mage::app()->getRequest()->setRoutingInfo($routingInfo);
        return false;
    }
}

/**
 * Process Containers
 *
 * @param $content
 * @return array
 */
protected function _processContainers(&$content)
{
    $placeholders = array();
    preg_match_all(
        Enterprise_PageCache_Model_Container_Placeholder::HTML_NAME_PATTERN,
        $content, $placeholders, PREG_PATTERN_ORDER
    );
    $placeholders = array_unique($placeholders[1]);
    $containers = array();
    foreach ($placeholders as $definition) {
        $placeholder = new Enterprise_PageCache_Model_Container_Placeholder($definition);
        $container = $placeholder->getContainerClass();
        if (!$container) {
            continue;
        }
 
        $container = new $container($placeholder);
        $container->setProcessor($this);
        if (!$container->applyWithoutApp($content)) {
            $containers[] = $container;
        } else {
            preg_match($placeholder->getPattern(), $content, $matches);
            if (array_key_exists(1,$matches)) {
                $containers = array_merge($this->_processContainers($matches[1]), $containers);
                $content = preg_replace($placeholder->getPattern(), str_replace('$', '\$', $matches[1]), $content);
            }
        }
    }
    return $containers;
}

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

Контейнеры плейсхолдеров сначала обрабатываются методом applyWithoutApp. Этот метод пытается заменить содержимое блока в закешированной странице на нужное нам содержимое. Пример метода applyWithoutApp, абстрактного для всех контейнеров класса Enterprise_PageCache_Model_Container_Abstract:

/**
 * Generate placeholder content before application was initialized and apply to page content if possible
 *
 * @param string $content
 * @return bool
 */
public function applyWithoutApp(&$content)
{
    $cacheId = $this->_getCacheId();
 
    if ($cacheId === false) {
        $this->_applyToContent($content, '');
        return true;
    }
 
    $block = $this->_loadCache($cacheId);
    if ($block === false) {
        return false;
    }
 
    $block = Enterprise_PageCache_Helper_Url::replaceUenc($block);
    $this->_applyToContent($content, $block);
    return true;
}

Из кода понятно, что в случае, если вы хотите изменять блок динамически, вам необходимо определить метод _getCacheId контейнера исходя из логики вашего блока. Если хотя бы один контейнер не был обработан (applyWithoutApp вернул false), то его нужно рендерить(см. метод _processContent класса Enterprise_PageCache_Model_Processor), а при этом произойдёт инициализация Magento (выполнится Mage::app()). При этом запрос обрабатывается контроллером Enterprise_PageCache_RequestController экшеном process. Код экшена process:

/**
 * Request processing action
 */
public function processAction()
{
    $processor  = Mage::getSingleton('enterprise_pagecache/processor');
    $content    = Mage::registry('cached_page_content');
    $containers = Mage::registry('cached_page_containers');
    $cacheInstance = Enterprise_PageCache_Model_Cache::getCacheInstance();
    foreach ($containers as $container) {
        $container->applyInApp($content);
    }
    $this->getResponse()->appendBody($content);
    // save session cookie lifetime info
    $cacheId = $processor->getSessionInfoCacheId();
    $sessionInfo = $cacheInstance->load($cacheId);
    if ($sessionInfo) {
        $sessionInfo = unserialize($sessionInfo);
    } else {
        $sessionInfo = array();
    }
    $session = Mage::getSingleton('core/session');
    $cookieName = $session->getSessionName();
    $cookieInfo = array(
        'lifetime' => $session->getCookie()->getLifetime(),
        'path'     => $session->getCookie()->getPath(),
        'domain'   => $session->getCookie()->getDomain(),
        'secure'   => $session->getCookie()->isSecure(),
        'httponly' => $session->getCookie()->getHttponly(),
    );
    if (!isset($sessionInfo[$cookieName]) || $sessionInfo[$cookieName] != $cookieInfo) {
        $sessionInfo[$cookieName] = $cookieInfo;
        // customer cookies have to be refreshed as well as the session cookie
        $sessionInfo[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER] = $cookieInfo;
        $sessionInfo[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_GROUP] = $cookieInfo;
        $sessionInfo[Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER_LOGGED_IN] = $cookieInfo;
        $sessionInfo[Enterprise_PageCache_Model_Cookie::CUSTOMER_SEGMENT_IDS] = $cookieInfo;
        $sessionInfo[Enterprise_PageCache_Model_Cookie::COOKIE_MESSAGE] = $cookieInfo;
        $sessionInfo = serialize($sessionInfo);
        $cacheInstance->save($sessionInfo, $cacheId, array(Enterprise_PageCache_Model_Processor::CACHE_TAG));
    }
}

Для каждого не обработанного контейнера запускается метод applyInApp, который должен отрендерить блок и заменить содержимое плейсхолдера в контенте. Код метода applyInApp, абстрактного для контейнеров класса Enterprise_PageCache_Model_Container_Abstract:

/**
 * Generate and apply container content in controller after application is initialized
 *
 * @param string $content
 * @return bool
 */
public function applyInApp(&$content)
{
    $blockContent = $this->_renderBlock();
    if ($blockContent === false) {
        return false;
    }
 
    if (Mage::getStoreConfig(Enterprise_PageCache_Model_Processor::XML_PATH_CACHE_DEBUG)) {
        $debugBlock = new Enterprise_PageCache_Block_Debug();
        $debugBlock->setDynamicBlockContent($blockContent);
        $debugBlock->setTags($this->_getPlaceHolderBlock()->getCacheTags());
 
        $debugBlock->setType($this->_placeholder->getName());
        $this->_applyToContent($content, $debugBlock->toHtml());
    } else {
        $this->_applyToContent($content, $blockContent);
    }
 
    $subprocessor = $this->_processor->getSubprocessor();
    if ($subprocessor) {
        $contentWithoutNestedBlocks = $subprocessor->replaceContentToPlaceholderReplacer($blockContent);
        $this->saveCache($contentWithoutNestedBlocks);
    }
 
    return true;
}

Здесь, как мы видим, содержимое получается методом _renderBlock класса контейнера. Этот метод, как правило, уникален для каждого контейнера и содержит какую-то особую логику. В абстрактном классе Enterprise_PageCache_Model_Container_Abstract для контейнеров этот метод возвращает false. Именно он должен возвращать HTML-содержимое блока.

При этом существует проблема при рендеринге контейнеров. Для блока контейнера родитель мог не отрендериться, да и контроллер выполняется другой. Это означает, что каких-то данных внутри блока может не быть и надо предусматривать этот случай (к примеру, вы получаете продукт таким способом: $product = Mage::registry('current_product'), или задаёте блоком родителя какое-то свойство: $this->getChild('block_alias')->setProduct($_product)).

Но, как мы помним, FPC сохраняет всю информацию, необходимую для рендеринга. Поэтому, если вы передали информацию в методе getCacheKeyInfo вашего блока для сохранения, вы сможете её задать во время FPC-рендеринга. Делать это нужно в методе _renderBlock контейнера. Получить в нём экземпляр блока можно методом _getPlaceHolderBlock:

/**
 * Get Placeholder Block
 *
 * @return Mage_Core_Block_Abstract
 */
protected function _getPlaceHolderBlock()
{
    if (null === $this->_placeholderBlock) {
        $blockName = $this->_placeholder->getAttribute('block');
        $this->_placeholderBlock = new $blockName;
        $this->_placeholderBlock->setTemplate($this->_placeholder->getAttribute('template'));
        $this->_placeholderBlock->setLayout(Mage::app()->getLayout());
        $this->_placeholderBlock->setSkipRenderTag(true);
    }
    return $this->_placeholderBlock;
}

Как видно, мы получаем экземпляр класса блока с установленным шаблоном. Пример метода _renderBlock:

/**
 * 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', 'popup_ids');
    foreach ($serializedParameters as $parameter) {
        $value = unserialize($placeholder->getAttribute($parameter));
        $block->setDataUsingMethod($parameter, $value);
    }
 
    return $block->toHtml();
}

Здесь, как видно из кода, мы передали блоку параметры handles и popup_ids. В самом же блоке их необходимо получать вот так:

public function getPopupIds()
{
    if (!$this->hasData('popup_ids')) {
        $popupIds = ...
        ...
        $this->setData('popup_ids', $popupIds);
    }
 
    return $this->getData('popup_ids');
}

Я надеюсь, данная статья приоткрыла завесу тайны над функционалом FPC, и вы узнали, как работает механизм FPC изнутри.

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

В моей следующей статье по FPC я опишу, как задавать свои собственные динамические блоки в Magento Enterprise для корректной работы дополнительного динамического функционала с включенным FPC.

Автор статьи Эльданиз Гасымов — ведущий разработчик Turnkey Ecommerce, сертифицированный Magento разработчик.

Автор: Turnkeye

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js