Back

Customer session and Full Page Cache, Magento 2

Monday morning, coffee and a simple task waiting to be accomplished: showing some customer information on category pages. Because I thought it would be a routine task, with a “full of confidence” stack on board, I started to work on it. I enabled the Template path hints, to find out which template/block is used to rendering the products list, and I created a module, in order to override those elements, magento-catalog/Block/Product/ListProduct  block and magento-catalog/view/frontend/templates/product/list.phtml template.

The idea was to inject in the block’s constructor the customer session and use it in the template, to get the customer id.

  • Leadlion/CustomerInfo/Block/Product/ListProduct

public function __construct(

…..

        \Magento\Customer\Model\Session $customerSession

   

…..

        $this->session = $customerSession;

     }

After a page refresh, the customer id has been displayed on top of the products grid, along with other information, and I wanted to declare the task as closed. But, before doing that, I decided to switch from developer mode to production mode and test one more time the functionality. I had a big surprise when I saw the customer id displayed as null. I took a deep breath and I started to search for some answers.

  • Public and private content

As Magento 2 documentation pointed out, there are two types of content, public and private:

  • Public, which can display to many customers

Public content is stored in your cache storage (file system, database, or Redis), or by Varnish. Examples of public content includes the header, footer, and category listing.

  • Private, which is not stored in the Magento server cache; instead, it’s stored on the client only.

Examples of private content include the wishlist, shopping cart, customer name, and addresses. Private content should be limited to a small portion of the total content on a page.

So, I found out that my category page is a public page (cacheable) and its content is intended to be the same for every user. However, for these pages, there is a way to insert the private content, so, in the following chapters, I will make a wrap up of my research and, also, I will bring the solution.

  • What’s happening with the customer session in the public pages ?

In the generateXml() method from magento/framework/View/Layout.php the layout is generated. This process is accompanied by the Magento\Customer\Model\Layout\DepersonalizePlugin.php which, in the afterGenerateXml method, performs the cleaning of the customer session.

  • Magento\Customer\Model\Layout\DepersonalizePlugin.php

public function afterGenerateXml(\Magento\Framework\View\LayoutInterface $subject, $result)

    {

        if ($this->depersonalizeChecker->checkIfDepersonalize($subject)) {

            $this->visitor->setSkipRequestLogging(true);

            $this->visitor->unsetData();

            $this->session->clearStorage();

            $this->customerSession->clearStorage();

            $this->session->setData(\Magento\Framework\Data\Form\FormKey::FORM_KEY, $this->formKey);

            $this->customerSession->setCustomerGroupId($this->customerGroupId);

            $this->customerSession->setCustomer($this->customerFactory->create()->setGroupId($this->customerGroupId));

        }

        return $result;

    }

As you can see, before cleaning the session and data storage, it is performing a check to see if the layout needs to be depersonalized: 

if ($this->depersonalizeChecker->checkIfDepersonalize($subject)) {…}

This check if performed in the Magento\PageCache\Model\DepersonalizeChecker.php:

 

  • Magento\PageCache\Model\DepersonalizeChecker.php

public function checkIfDepersonalize(\Magento\Framework\View\LayoutInterface $subject)

    {

        return ($this->moduleManager->isEnabled(‘Magento_PageCache’)

            && $this->cacheConfig->isEnabled()

            && !$this->request->isAjax()

            && ($this->request->isGet() || $this->request->isHead())

            && $subject->isCacheable());

    }

So, here is the place where my customer session has been cleared, after I activated the production mode.

  • Don’t use cacheable=’false’ !

You have the choice to declare blocks as non-cacheable by setting the cacheable attribute to false in the layout XML files.

<block class=”Class” name=”blockname” cacheable=”false” /> 

But, it’s not a good practice because a single element with cacheable=’false’ attribute will disable the Full Page Cache functionality for the whole page and time loading will be increased drastically. It’s time to prove it.

We’ll start from magento/framework/View/Layout.php with function renderElement. Here, the elements from layout are rendered and returned as string output, also an event being dispatched: core_layout_render_element.

  • magento/framework/View/Layout.php

    public function renderElement($name, $useCache = true)

    {

        …….

        $this->_eventManager->dispatch(

            ‘core_layout_render_element’,

            [‘element_name’ => $name, ‘layout’ => $this, ‘transport’ => $this->_renderingOutput]

        );

        ……

    }

This event is listened in the module-page-cache by the Magento\PageCache\Observer\ProcessLayoutRenderElement observer.

  1. module-page-cache/etc/events.xml

<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:Event/etc/events.xsd”>

    <event name=”core_layout_render_element”>

        <observer name=”pagecache”   instance=”Magento\PageCache\Observer\ProcessLayoutRenderElement” />

     </event>

…………….

</config>

Now, in the  ProcessLayoutRenderElement observer, before render the elements from Full Page Cache, is performed a check to see if the Full Page Cache is enabled and the block is cacheable.

  • module-page-cache/Observer/ProcessLayoutRenderElement

public function execute(\Magento\Framework\Event\Observer $observer)

    {

        $event = $observer->getEvent();

        /** @var \Magento\Framework\View\Layout $layout */

        $layout = $event->getLayout();

        if ($this->isFullPageCacheEnabled() && $layout->isCacheable()) {

//render elements from FPC

}

….

}

The second condition, $layout->isCacheable(), calls the method isCacheable() from  magento/framework/View/Layout.php, to check if there exist non-cacheable elements in the layout. 

  • magento/framework/View/Layout.php

    public function isCacheable()

    {

        $this->build();

        $cacheableXml = !(bool)count($this->getXml()->xpath(‘//’ . Element::TYPE_BLOCK . ‘[@cacheable=”false”]’));

        return $this->cacheable && $cacheableXml;

    }

If one of those cacheable=’false’ elements is found, the entire page will be labeled as NON-CACHEABLE and the entire functionality of Full Page Cache will be disabled.

  • Here’s the solution

In the following section, I’m going to present the solution and the important files that have been added to the module and I’ll skip the basic module configuration.

  1. Create Leadlion\CustomerInfo\CustomerData\CustomInfo.php which implements Magento\Customer\CustomerData\SectionSourceInterface. This class needs to implement the method getSectionData() and must return an array with data for the private block.

<?php

namespace Leadlion\CustomerInfo\CustomerData;

use Magento\Customer\CustomerData\SectionSourceInterface;

class CustomInfo implements SectionSourceInterface

{

    protected $session;

 

    public function __construct(

         \Magento\Customer\Model\Session $customerSession

     ) {

         $this->session = $customerSession;

     }

     public function getSectionData()

     {

         return [

             ‘customerId’ =>  $this->session->getCustomerId()

         ];

     }

}

  1. Create Leadlion\CustomerInfo\etc\frontend\di.xml

<?xml version=”1.0″?>

 

<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:ObjectManager/etc/config.xsd”>

    <type name=”Magento\Customer\CustomerData\SectionPoolInterface”>

        <arguments>

            <argument name=”sectionSourceMap” xsi:type=”array”>

                <item name=”custom-info” xsi:type=”string”>Leadlion\CustomerInfo\CustomerData\CustomInfo</item>

            </argument>

        </arguments>

    </type> 

</config>

 

  1. Create an UI component which will be responsible for rendering blocks data on the storefront. Leadlion\CustomerInfo\view\frontend\web\js\view\custom-info.js

define([

    ‘uiComponent’,

     ‘Magento_Customer/js/customer-data’

], function (Component, customerData) {

     ‘use strict’;

     return Component.extend({

         initialize: function () {

             this._super();

             this.customInfo = customerData.get(‘custom-info’);

         }

     });

});

  1. To render private content, create a block and a template to display user-agnostic data; this data is replaced with user-specific data by the UI component. Leadlion\CustomerInfo\view\frontend\templates\product\list.phtml

……………………….

<li data-bind=”scope: ‘customInfo'” data-role=”custom-information”>

             <p data-bind=”text: customInfo().customerId”> </p>

         </li>

<script type=”text/x-magento-init”>

{“[data-role=custom-information]”: {“Magento_Ui/js/core/app”: <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>}}

</script>

 

Leadlion\CustomerInfo\view\frontend\layout\catalog_category_view.xml

<?xml version=”1.0″?>

<page xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:View/Layout/etc/page_configuration.xsd”>

    <body>

        <referenceBlock name=”category.products.list” >

            <action method=”setTemplate”>

                <argument name=”template” xsi:type=”string”>Leadlion_CustomerInfo::product/list.phtml</argument>

            </action>

            <arguments>

             <argument name=”jsLayout” xsi:type=”array”>

                    <item name=”components” xsi:type=”array”>

                        <item name=”customInfo” xsi:type=”array”>

                            <item name=”component” xsi:type=”string”>Leadlion_CustomerInfo/js/view/custom-info</item>

                        </item>

                    </item>

                </argument>

            </arguments>

        </referenceBlock>

    </body>

</page>

 

  1. Invalidate private content. Specify actions that trigger cache invalidation for private content blocks in a sections.xml configuration file in the Leadlion\CustomerInfo\etc\frontend\section.xml.

 

<?xml version=”1.0″?>

<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”

    xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Customer:etc/sections.xsd”>

     <action name=”catalog/category/view”>

         <section name=”custom-info”/>

     </action>

</config>

After all this work, finally, I cleared the cache and I saw my customer id displayed on the category page. Also, customer id has been saved in the browser Local Storage under mage-cache-storage:

“custom-info”:{
“customerId”:“2”,
“data_id”:1488332248
}

Ai un proiect interesant?

Haide sa discutam despre cum te poate ajuta aceasta integrare!