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):