1

I've created a customer EAV attribute and trying to show this attribute on the product details page but it always returns chaced data till I run bin/magento cache:flush full_page.

I've tried to use sections.xml in the frontend area but it's not working.

section.xml file content:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
    <action name="catalog/product/view">
        <section name="customer"/>
    </action>
</config>

I also tried to add isScopePrivate and getCacheLifetime methods in my custom block but it also makes no changes.

I also tried the getIdentities method and it's also still getting cached data even when I can see the tags have been added.

Full block class code:

<?php

namespace Me\Vendor\Block\Profile;


use Magento\Eav\Model\Cache\Type;
use Magento\Eav\Model\Entity\Attribute;


class Address extends \Vnecoms\Vendors\Block\Profile\Address
{
    public function getIdentities()
    {
        $identities = parent::getIdentities();

        $identities[] = Type::CACHE_TAG;
        $identities[] = Attribute::CACHE_TAG;
        $identities[] = Attribute::CACHE_TAG . '_' . $this->getVendor()
            ->getCustomer()->getAttribute('location')->getId();
        $identities[] = 'customer' . '_' . $this->getVendor()
            ->getCustomer()->getId();

        return $identities;
    }

    /**
     * Get vendor address
     *
     * @return string
     */
    public function getAddress()
    {
        $customer = $this->getVendor()->getCustomer();

        if ($countryId = $customer->getData('location')) {
            $locationAttrOptions = $this->getVendor()
                ->getCustomer()->getAttribute('location')
                ->getSource()->getAllOptions();

            foreach ($locationAttrOptions as $locationAttrOption) {
                if ($locationAttrOption['value'] == $countryId) {
                    $data =  $locationAttrOption['label'];

                    return $data;
                }
            }
        }
    }

    public function isScopePrivate()
    {
        return true;
    }

    protected function getCacheLifetime()
    {
        return 0;
    }
}

Critical notes:

1- When I was debugging, I found that the block class method is called only once, and the next request is not calling my class method.

2- When I disable FPC everything works perfectly.

3- I can't use cacheable="false" because I'm referencing another block in a third-party extension.

Any help, please?

2 Answers 2

1

Here is example of same code but without using of customer data (private data).

Note: Complete code can be found in the same repo on GitHub.

First you need to define your block inside another product block which is cached using product tags (to make sure it will be unique for each product page):

app/code/MageWorx/CustomerAge/view/frontend/layout/catalog_product_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>
        <referenceContainer name="product.info.extrahint">
            <block class="Magento\Framework\View\Element\Template" name="mw_customer_attributes.age.ajax" template="MageWorx_CustomerAge::customer_attributes_ajax.phtml">
                <arguments>
                    <argument name="viewModel" xsi:type="object">MageWorx\CustomerAge\ViewModel\AgeJs</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

I have used the viewModel inside standard template to pass data to the JS component. This way we can access the product id or other data per requirements:

app/code/MageWorx/CustomerAge/ViewModel/AgeJs.php

<?php
declare(strict_types=1);

namespace MageWorx\CustomerAge\ViewModel;

use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class AgeJs implements ArgumentInterface
{
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $storeManager;

    /**
     * @var \Magento\Framework\Registry
     */
    protected $registry;

    /**
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     */
    public function __construct(
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\Registry                $registry
    ) {
        $this->storeManager = $storeManager;
        $this->registry     = $registry;
    }

    /**
     * @return int
     */
    public function getCurrentProductId(): int
    {
        $product = $this->registry->registry('current_product');
        if (!$product) {
            return 0;
        }

        return (int)$product->getId();
    }

    /**
     * Retrieve store code
     *
     * @return string
     */
    public function getStoreCode(): string
    {
        try {
            return $this->storeManager->getStore()->getCode();
        } catch (LocalizedException $localizedException) {
            return 'default';
        }
    }
}

This is not the best way to determine the actual product ID, but feel free to use any other way that suits your needs and code quality requirements. It doesn't matter how, but it should just return the product id :) because it's not exist on the product page by default.

Template with new component:

app/code/MageWorx/CustomerAge/view/frontend/templates/customer_attributes_ajax.phtml

<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \MageWorx\CustomerAge\ViewModel\AgeJs $viewModel */
$viewModel = $block->getData('viewModel');
?>
<!-- The scope from default.xml layout: <item name="mwCustomerAttributesAjax" xsi:type="array"> -->
<div id="age-container-ajax" data-bind="scope: 'mwCustomerAttributesAjax'" data-role="customer-age-ajax-container">
    <!-- Tag is invisible by default, but will be shown using component logic -->
    <p data-bind="visible: isVisible" style="display: none;"><?= __('Age is ');?><span data-bind="html: age"></span></p>
</div>
<script type="text/x-magento-init">
    {
        "#age-container-ajax":
        {
            "Magento_Ui/js/core/app": {
                "components" : {
                    "mwCustomerAttributesAjax": {
                        "component": "MageWorx_CustomerAge/js/view/customer-attributes-ajax",
                        "config": {
                            "storeCode": "<?= $escaper->escapeJs($viewModel->getStoreCode())?>",
                            "productId": "<?= $escaper->escapeJs($viewModel->getCurrentProductId())?>"
                        }
                    }
                }
            }
        }
    }
</script>

Add component responsible for requesting data from the backend and rendering it on the template. It will call our future controller and receive data from it on the page load:

app/code/MageWorx/CustomerAge/view/frontend/web/js/view/customer-attributes-ajax.js

define([
    'uiComponent',
    'jquery',
    'ko'
], function (Component, $, ko) {
    'use strict';

    return Component.extend({

        // Property which will be displayed as example in the template
        observableProperties: [
            'age'
        ],

        /** @inheritdoc */
        initialize: function () {
            this._super();
            this.initObservable();
            this.initSubscribers();
            this.initData();
        },

        /**
         * Make age observable
         *
         * @returns {*}
         */
        initObservable: function () {
            this._super();
            this.observe(this.observableProperties);

            return this;
        },

        /**
         * Make isVisible computed, based on the age value
         *
         * @returns {*}
         */
        initSubscribers: function () {
            this.isVisible = ko.computed(function () {
                return Boolean(this.age());
            }.bind(this));

            return this;
        },

        /**
         * Call for controller on the page load and set data to the component's property: age
         */
        initData: function () {
            let data = this.getData();

            if (data.age) {
                this.age(data.age);
            } else {
                this.age(false);
            }

            console.log(data);
        },

        /**
         * Receive data from controller. Hardcoded.
         *
         * @returns {{}}
         */
        getData: function() {
            let url = window.BASE_URL + "mageworx_customer_age/index/detectAge",
                result = {};

            $.ajax({
                url: url,
                data: {
                    "product_id": this.getProductId()
                },
                type: "GET",
                async: false,
                dataType: "json",
                contentType: "application/json",
                success: function (data, status, xhr) {
                    result = data;
                },
                error: function (request, status, error) {
                    console.log(error);
                },
            });

            return result;

        },

        /**
         * Get current product id from component's configuration. @see \MageWorx\CustomerAge\ViewModel\AgeJs
         * @returns {*}
         */
        getProductId: function () {
            return this.productId;
        }
    });
});

And finally we need a controller with router:

app/code/MageWorx/CustomerAge/etc/frontend/routes.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="mageworx_customer_age" frontName="mageworx_customer_age">
            <module name="MageWorx_CustomerAge"/>
        </route>
    </router>
</config>

app/code/MageWorx/CustomerAge/Controller/Index/DetectAge.php

<?php
/**
 * Copyright © MageWorx. All rights reserved.
 * See LICENSE.txt for license details.
 */
declare(strict_types=1);

namespace MageWorx\CustomerAge\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Request\Http;
use Magento\Framework\App\ResponseInterface;

class DetectAge implements HttpGetActionInterface
{
    /**
     * @var Http
     */
    protected $request;
    /**
     * @var \Magento\Framework\Controller\ResultFactory
     */
    protected $resultFactory;
    /**
     * @var \Magento\Customer\Model\Session
     */
    protected $customerSession;

    /**
     * @param Http $request
     * @param \Magento\Framework\Controller\ResultFactory $resultFactory
     * @param \Magento\Customer\Model\Session $customerSession
     */
    public function __construct(
        \Magento\Framework\App\Request\Http         $request,
        \Magento\Framework\Controller\ResultFactory $resultFactory,
        \Magento\Customer\Model\Session             $customerSession
    ) {
        $this->request         = $request;
        $this->resultFactory   = $resultFactory;
        $this->customerSession = $customerSession;
    }

    /**
     * Execute action based on request and return result
     *
     * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
     * @throws \Magento\Framework\Exception\NotFoundException
     */
    public function execute()
    {
        $customerId = (int)$this->customerSession->getCustomerId();
        $productId  = (int)$this->request->getParam('product_id', 0);

        $result = $this->resultFactory->create(
            \Magento\Framework\Controller\ResultFactory::TYPE_JSON
        );

        // Call here your model and return correct data to the component

        // For testing: Dummy data + input data
        $result->setData(
            [
                'age'         => 133,
                'customer_id' => $customerId,
                'product_id'  => $productId
            ]
        );

        return $result;
    }
}

This way each product page will be loaded from the cache, but there will also be an ajax call to the backend for every open, no matter who opens the page. This is not the best way in terms of server load, but it can be improved.

Screenshots of same product page in different browsers (with logged in customer and for guest; dummy age of 133 is hardcoded, but in console you can see the actual customer id is returned, so you can use it on the backend to get desired data):

logged in customer guest

3
  • This answer is constructive, but you still get the customer from the session, and we can't retrieve this data from the session sometimes. Therefore I passed the customer id instead of the product id, and everything is working fine. Anyway, I upvoted and marked it as the solution.
    – AboElnouR
    Commented Aug 1, 2022 at 15:16
  • @AboElnouR how do you detect the customer id (if FPC is disabled)? It seems you just need to replace this method to make it work. I think using session is more accurate way because controllers action is not cached. Commented Aug 1, 2022 at 15:34
  • I can get the customer id from the product data because I retrieve the product creator data, not the data of the logged-in customer.
    – AboElnouR
    Commented Aug 1, 2022 at 17:22
1

The private content (sections.xml) was designed for this, but in you example you are using it wrong way:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
    <action name="catalog/product/view">
        <section name="customer"/>
    </action>
</config>

Note: catalog/product/view is a GET request

You are trying to update content on the GET request, which is totally wrong, because the private data will only be changed when something is updated on the server side, so it will only work for the POST, PUT or DELETE requests, which means "to change something on the server side you must send data to the server".

If you wish to use the private data you must update section data on any request that can update the customer entity (for example <action name="customer/account/editPost"> or <action name="customer/ajax/login">). Alternatively, you can use a wildcard for your section, such as <action name="customer/*/*">.

All you need to make it work is create the corresponding sections source model in the CustomerData directory of your module, and declare it in the frontend/di.xml. If your template is suitable for using customer data, it will work fine.


Here is example module on GitHub, which adds the customer age (predefined attribute) to any page.


And here is a code with comments:

etc/frontend/sections.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">
    <!-- Any POST, PUT or DELETE request to customer route will trigger the data update using our model -->
    <action name="customer/*/*">
        <section name="mw-customer-attributes"/>
    </action>
</config>

etc/frontend/di.xml

<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Add customer attributes data model to source map (private data provider) -->
    <type name="Magento\Customer\CustomerData\SectionPoolInterface">
        <arguments>
            <argument name="sectionSourceMap" xsi:type="array">
                <!-- The 'mw-customer-attributes' is key for our data on frontend. The `CustomerAttributes` class will provide data from server. -->
                <item name="mw-customer-attributes" xsi:type="string">MageWorx\CustomerAge\CustomerData\CustomerAttributes</item>
            </argument>
        </arguments>
    </type>
</config>

Here we are getting the customer from session and pass his age to the private data. If customer has no age it will be null, so we can display something like "The customer did not indicate the age".

CustomerData/CustomerAttributes.php

<?php
/**
 * Copyright © MageWorx. All rights reserved.
 * See LICENSE.txt for license details.
 */
declare(strict_types=1);

namespace MageWorx\CustomerAge\CustomerData;

class CustomerAttributes implements \Magento\Customer\CustomerData\SectionSourceInterface
{
    /**
     * @var \Magento\Customer\Model\Session
     */
    protected $customerSession;
    /**
     * @var \Magento\Customer\Api\CustomerRepositoryInterface
     */
    protected $customerRepository;

    /**
     * @param \Magento\Customer\Model\Session $customerSession
     * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository
     */
    public function __construct(
        \Magento\Customer\Model\Session  $customerSession,
        \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository
    ) {
        $this->customerSession = $customerSession;
        $this->customerRepository = $customerRepository;
    }

    /**
     * @inheritDoc
     */
    public function getSectionData()
    {
        $customer = $this->customerSession->getCustomer();
        if (!$customer->getId()) {
            return [];
        }

        return [
            'age' => $customer->getData('age')
        ];
    }
}

Adding a block to the layout. Make sure you use the same component name in the template later (mwCustomerAttributes):

view/frontend/layout/default.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="header-wrapper">
            <container name="customer-attributes-wrapper" after="top.search" htmlTag="div" htmlClass="compare wrapper">
                <block class="Magento\Framework\View\Element\Template" name="mw_customer_attributes.age" template="MageWorx_CustomerAge::customer_attributes.phtml">
                    <arguments>
                        <argument name="jsLayout" xsi:type="array">
                            <item name="components" xsi:type="array">
                                <item name="mwCustomerAttributes" xsi:type="array">
                                    <item name="component" xsi:type="string">MageWorx_CustomerAge/js/view/customer-attributes</item>
                                </item>
                            </item>
                        </argument>
                    </arguments>
                </block>
            </container>
        </referenceContainer>
    </body>
</page>

Add template which will render our js component:

view/frontend/templates/customer_attributes.phtml

<?php
/** @var \Magento\Framework\View\Element\Template $block */
?>
<!-- The scope from default.xml layout: <item name="mwCustomerAttributes" xsi:type="array"> -->
<div id="age-container" data-bind="scope: 'mwCustomerAttributes'" data-role="customer-age-container">
    <!-- ko if: (customerAttributes().age) -->
    <div class="mw-customer-attributes-container">
        <p><?= __('Customer age is ');?><span data-bind="html: customerAttributes().age"></span></p>
    </div>
    <!-- /ko -->
    <!-- ko ifnot: (customerAttributes().age) -->
    <p><?= __('The customer did not indicate the age');?></p>
    <!-- /ko -->
</div>
<script type="text/x-magento-init">
    {"#age-container": {"Magento_Ui/js/core/app": <?php echo $block->getJsLayout();?>}}
</script>

And finally, let's add our js component that will use the customer data (private data) to provide it to the template:

view/frontend/web/js/view/customer-attributes.js

define([
    'uiComponent',
    'Magento_Customer/js/customer-data',
    'jquery'
], function (Component, customerData, $) {
    'use strict';

    return Component.extend({

        /** @inheritdoc */
        initialize: function () {
            this._super();

            this.customerAttributes = customerData.get('mw-customer-attributes');
        }
    });
});

Enable module (in case it is new), clean cache, and check it on the page. For the not logged in customer:

example view when customer is a guest

Login as a customer with the age attribute defined:

example view when customer is logged in and has age

Change age attribute from the customer edit page:

example view after changing the age

5
  • Thank you so much for this great answer. I am not trying to get the current logged-in customer data. Still, I am trying to get data from the customer who is associated with the product that maybe a different customer than the logged-in customer, so can I pass the customer id to my section or what?
    – AboElnouR
    Commented Jul 27, 2022 at 1:06
  • @AboElnouR do you mean some kind of vendor related to the product? If so, you need custom ajax query to the webapi, controller or graphql endpoint every time the page is opening, because the data in the customerData will be cached until POST, PUT or DELETE request to the server with matching route. I'll try to add another answer. Commented Jul 27, 2022 at 13:29
  • @AboElnouR how do you identify the vendor on the product page? Are there any parameters used other than the product_id? Commented Jul 27, 2022 at 15:40
  • vendor_id is added to the product as an EAV attribute.
    – AboElnouR
    Commented Jul 27, 2022 at 16:18
  • Can you please explain how to do it using ajax request? I tried using addHandle to Result/Layout but it's not working.
    – AboElnouR
    Commented Jul 28, 2022 at 10:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.