Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Magento Enterprise: Что такое Full Page Cache и отчего он необходим

Anna | 29.05.2014 | нет комментариев

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

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

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

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

Magento Enterprise - 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>
    < ul id="nav">
        < li>
            < a 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.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB