BEYABLE Product Ranking API - Magento Integration Guide
Overview
This guide provides step-by-step instructions for integrating the BEYABLE Product Ranking API with your Magento 2 e-commerce platform. The integration will allow you to optimize product listings using BEYABLE's AI-powered ranking rules.
Table of Contents
- Prerequisites
- Architecture Overview
- Installation
- Module Structure
- Configuration
- Implementation Steps
- Testing
- Deployment
- Troubleshooting
- Performance Optimization
Prerequisites
System Requirements
- Magento 2.4.x or higher
- PHP 7.4 or higher
- MySQL 5.7 or higher
- Composer installed
- Cron configured and running
- BEYABLE account with API credentials
Required Information from BEYABLE
Before starting the integration, obtain the following from your BEYABLE account manager:
- Account ID: Your 32-character UUID (without hyphens)
- Subscription Key: Your API authentication key
- Ranking Rule ID(s): UUID for each ranking rule you want to use
- Segmented Ranking ID(s): UUID for segmented rankings (if applicable)
- Tenant Configuration: Your tenant identifiers (e.g., store views, countries)
Architecture Overview
The integration follows this flow:
- BEYABLE Cookie Reading: Read visitor segment from cookie set by BEYABLE Platform
- API Integration: Fetch rankings from BEYABLE API via scheduled cron jobs
- Database Storage: Store rankings in custom Magento table
- Product Collection Modification: Apply rankings to product listings
- Frontend Display: Show optimized product order to visitors
Installation
Step 1: Create Custom Magento Module
Create the module directory structure:
cd app/code
mkdir -p Beyable/ProductRanking
Step 2: Create Module Files
composer.json
{
"name": "beyable/module-product-ranking",
"description": "BEYABLE Product Ranking API Integration",
"type": "magento2-module",
"version": "1.0.0",
"license": "proprietary",
"require": {
"php": "^7.4|^8.0",
"magento/framework": "^103.0"
},
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Beyable\\ProductRanking\\": ""
}
}
}
registration.php
<?php
/**
* BEYABLE Product Ranking Module Registration
*/
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Beyable_ProductRanking',
__DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Beyable_ProductRanking" setup_version="1.0.0">
<sequence>
<module name="Magento_Catalog"/>
</sequence>
</module>
</config>
Module Structure
app/code/Beyable/ProductRanking/
├── Api/
│ ├── Data/
│ │ └── RankingInterface.php
│ ├── RankingRepositoryInterface.php
│ └── RankingManagementInterface.php
├── Block/
│ └── Tracking.php
├── Console/
│ └── Command/
│ └── SyncRankings.php
├── Cron/
│ └── SyncRankings.php
├── etc/
│ ├── adminhtml/
│ │ └── system.xml
│ ├── crontab.xml
│ ├── di.xml
│ ├── module.xml
│ └── config.xml
├── Helper/
│ └── Data.php
├── Model/
│ ├── Config.php
│ ├── Ranking.php
│ ├── RankingManagement.php
│ ├── RankingRepository.php
│ ├── ResourceModel/
│ │ ├── Ranking.php
│ │ └── Ranking/
│ │ └── Collection.php
│ └── Api/
│ └── Client.php
├── Observer/
│ └── ApplyRankingToCollection.php
├── Plugin/
│ └── Catalog/
│ └── Model/
│ └── ResourceModel/
│ └── Product/
│ └── Collection.php
├── Setup/
│ ├── InstallSchema.php
│ └── UpgradeSchema.php
├── view/
│ └── frontend/
│ ├── layout/
│ │ └── default.xml
│ └── templates/
│ └── tracking.phtml
├── composer.json
└── registration.php
Configuration
Step 1: System Configuration (etc/adminhtml/system.xml)
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<tab id="beyable" translate="label" sortOrder="400">
<label>BEYABLE</label>
</tab>
<section id="beyable_ranking" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Product Ranking</label>
<tab>beyable</tab>
<resource>Beyable_ProductRanking::config</resource>
<group id="general" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Configuration</label>
<field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable BEYABLE Ranking</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="account_id" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Account ID</label>
<comment>Your BEYABLE account ID (32 characters, no hyphens)</comment>
<validate>required-entry validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
<field id="subscription_key" translate="label comment" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Subscription Key</label>
<comment>Your BEYABLE API subscription key</comment>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
<validate>required-entry</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
<field id="api_base_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
<label>API Base URL</label>
<validate>required-entry validate-url</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
</group>
<group id="ranking_rules" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Ranking Rules</label>
<field id="default_ranking_id" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Default Ranking ID</label>
<comment>UUID of the default ranking rule (32 characters, no hyphens)</comment>
<validate>validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="segmented_ranking_id" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Segmented Ranking ID</label>
<comment>UUID for segmented ranking (optional, 32 characters, no hyphens)</comment>
<validate>validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="use_segmentation" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Use Visitor Segmentation</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="segment_cookie_name" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Segment Cookie Name</label>
<comment>Name of the cookie containing visitor segment (default: b_segment)</comment>
<depends>
<field id="use_segmentation">1</field>
</depends>
</field>
</group>
<group id="sync" translate="label" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Synchronization</label>
<field id="cron_enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Enable Cron Sync</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="cron_schedule" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Cron Schedule</label>
<comment>Cron expression (default: every 2 hours)</comment>
<depends>
<field id="cron_enabled">1</field>
</depends>
</field>
<field id="timeout" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
<label>API Timeout (seconds)</label>
<comment>Maximum time to wait for API response</comment>
<validate>validate-number validate-greater-than-zero</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
</group>
<group id="advanced" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Advanced Settings</label>
<field id="apply_to_categories" translate="label comment" type="multiselect" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Apply to Category Pages</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment>Enable ranking on category listing pages</comment>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="apply_to_search" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Apply to Search Results</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="fallback_sorting" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Fallback Sorting</label>
<comment>Sorting method when BEYABLE ranking is unavailable</comment>
<source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="debug_logging" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Debug Logging</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
</group>
</section>
</system>
</config>
Step 2: Default Configuration (etc/config.xml)
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<beyable_ranking>
<general>
<enabled>0</enabled>
<api_base_url>https://api.beyable.com</api_base_url>
</general>
<ranking_rules>
<use_segmentation>0</use_segmentation>
<segment_cookie_name>b_segment</segment_cookie_name>
</ranking_rules>
<sync>
<cron_enabled>1</cron_enabled>
<cron_schedule>0 */2 * * *</cron_schedule>
<timeout>30</timeout>
</sync>
<advanced>
<apply_to_categories>1</apply_to_categories>
<apply_to_search>1</apply_to_search>
<fallback_sorting>position</fallback_sorting>
<debug_logging>0</debug_logging>
</advanced>
</beyable_ranking>
</default>
</config>
Implementation Steps
Step 3: Database Schema (Setup/InstallSchema.php)
<?php
namespace Beyable\ProductRanking\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;
class InstallSchema implements InstallSchemaInterface
{
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
$table = $setup->getConnection()->newTable(
$setup->getTable('beyable_product_ranking')
)->addColumn(
'ranking_id',
Table::TYPE_INTEGER,
null,
['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true],
'Ranking ID'
)->addColumn(
'account_id',
Table::TYPE_TEXT,
32,
['nullable' => false],
'BEYABLE Account ID'
)->addColumn(
'rule_id',
Table::TYPE_TEXT,
32,
['nullable' => false],
'Ranking Rule ID'
)->addColumn(
'tenant_id',
Table::TYPE_TEXT,
100,
['nullable' => true],
'Tenant ID'
)->addColumn(
'segment_id',
Table::TYPE_TEXT,
100,
['nullable' => true],
'Segment ID'
)->addColumn(
'product_id',
Table::TYPE_INTEGER,
null,
['nullable' => false, 'unsigned' => true],
'Product ID'
)->addColumn(
'sku',
Table::TYPE_TEXT,
64,
['nullable' => false],
'Product SKU'
)->addColumn(
'position',
Table::TYPE_INTEGER,
null,
['nullable' => false, 'unsigned' => true],
'Ranking Position'
)->addColumn(
'ab_test_group',
Table::TYPE_TEXT,
50,
['nullable' => true],
'A/B Test Group'
)->addColumn(
'store_id',
Table::TYPE_SMALLINT,
null,
['nullable' => false, 'unsigned' => true],
'Store ID'
)->addColumn(
'updated_at',
Table::TYPE_TIMESTAMP,
null,
['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE],
'Updated At'
)->addColumn(
'synced_at',
Table::TYPE_TIMESTAMP,
null,
['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
'Synced At'
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['product_id']),
['product_id']
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['sku']),
['sku']
)->addIndex(
$setup->getIdxName(
'beyable_product_ranking',
['account_id', 'rule_id', 'tenant_id', 'segment_id', 'store_id']
),
['account_id', 'rule_id', 'tenant_id', 'segment_id', 'store_id']
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['position']),
['position']
)->addForeignKey(
$setup->getFkName('beyable_product_ranking', 'product_id', 'catalog_product_entity', 'entity_id'),
'product_id',
$setup->getTable('catalog_product_entity'),
'entity_id',
Table::ACTION_CASCADE
)->addForeignKey(
$setup->getFkName('beyable_product_ranking', 'store_id', 'store', 'store_id'),
'store_id',
$setup->getTable('store'),
'store_id',
Table::ACTION_CASCADE
)->setComment(
'BEYABLE Product Ranking Table'
);
$setup->getConnection()->createTable($table);
$setup->endSetup();
}
}
Step 4: API Client (Model/Api/Client.php)
<?php
namespace Beyable\ProductRanking\Model\Api;
use Magento\Framework\HTTP\Client\Curl;
use Beyable\ProductRanking\Model\Config;
use Psr\Log\LoggerInterface;
class Client
{
const API_ENDPOINT_STANDARD = '/productranking/%s/%s/%s';
const API_ENDPOINT_SEGMENTED = '/segmented-productranking/%s/%s/%s/%s';
/**
* @var Curl
*/
protected $curl;
/**
* @var Config
*/
protected $config;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param Curl $curl
* @param Config $config
* @param LoggerInterface $logger
*/
public function __construct(
Curl $curl,
Config $config,
LoggerInterface $logger
) {
$this->curl = $curl;
$this->config = $config;
$this->logger = $logger;
}
/**
* Fetch rankings from BEYABLE API
*
* @param string $ruleId
* @param string|null $tenantId
* @param string|null $segmentId
* @param int|null $storeId
* @return array
* @throws \Exception
*/
public function fetchRankings($ruleId, $tenantId = null, $segmentId = null, $storeId = null)
{
if (!$this->config->isEnabled($storeId)) {
throw new \Exception('BEYABLE Product Ranking is not enabled');
}
$accountId = $this->config->getAccountId($storeId);
$subscriptionKey = $this->config->getSubscriptionKey($storeId);
$baseUrl = $this->config->getApiBaseUrl($storeId);
if (empty($accountId) || empty($subscriptionKey)) {
throw new \Exception('BEYABLE API credentials are not configured');
}
// Build URL
if ($segmentId && $tenantId) {
$endpoint = sprintf(self::API_ENDPOINT_SEGMENTED, $accountId, $ruleId, $segmentId, $tenantId);
} elseif ($segmentId) {
$endpoint = sprintf('/segmented-productranking/%s/%s/%s', $accountId, $ruleId, $segmentId);
} elseif ($tenantId) {
$endpoint = sprintf(self::API_ENDPOINT_STANDARD, $accountId, $ruleId, $tenantId);
} else {
$endpoint = sprintf('/productranking/%s/%s', $accountId, $ruleId);
}
$url = rtrim($baseUrl, '/') . $endpoint;
// Configure request
$this->curl->setOption(CURLOPT_TIMEOUT, $this->config->getTimeout($storeId));
$this->curl->addHeader('Subscription-Key', $subscriptionKey);
$this->curl->addHeader('Accept', 'text/csv');
// Log request if debug is enabled
if ($this->config->isDebugLoggingEnabled($storeId)) {
$this->logger->debug('BEYABLE API Request', [
'url' => $url,
'rule_id' => $ruleId,
'tenant_id' => $tenantId,
'segment_id' => $segmentId
]);
}
try {
// Make API call
$this->curl->get($url);
$statusCode = $this->curl->getStatus();
$response = $this->curl->getBody();
if ($statusCode !== 200) {
throw new \Exception(sprintf(
'BEYABLE API returned status %d: %s',
$statusCode,
$response
));
}
// Parse CSV
$rankings = $this->parseCsvResponse($response);
if ($this->config->isDebugLoggingEnabled($storeId)) {
$this->logger->debug('BEYABLE API Response', [
'rankings_count' => count($rankings)
]);
}
return $rankings;
} catch (\Exception $e) {
$this->logger->error('BEYABLE API Error: ' . $e->getMessage(), [
'url' => $url,
'exception' => $e
]);
throw $e;
}
}
/**
* Parse CSV response
*
* @param string $csvContent
* @return array
*/
protected function parseCsvResponse($csvContent)
{
$rankings = [];
$lines = str_getcsv($csvContent, "\n");
// Skip header row
array_shift($lines);
foreach ($lines as $line) {
if (empty(trim($line))) {
continue;
}
$data = str_getcsv($line);
if (count($data) >= 3) {
$rankings[] = [
'product_id' => $data[0],
'ab_test_group' => !empty($data[1]) ? $data[1] : null,
'position' => (int)$data[2]
];
}
}
return $rankings;
}
}
Step 5: Configuration Helper (Model/Config.php)
<?php
namespace Beyable\ProductRanking\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
class Config
{
const XML_PATH_ENABLED = 'beyable_ranking/general/enabled';
const XML_PATH_ACCOUNT_ID = 'beyable_ranking/general/account_id';
const XML_PATH_SUBSCRIPTION_KEY = 'beyable_ranking/general/subscription_key';
const XML_PATH_API_BASE_URL = 'beyable_ranking/general/api_base_url';
const XML_PATH_DEFAULT_RANKING_ID = 'beyable_ranking/ranking_rules/default_ranking_id';
const XML_PATH_SEGMENTED_RANKING_ID = 'beyable_ranking/ranking_rules/segmented_ranking_id';
const XML_PATH_USE_SEGMENTATION = 'beyable_ranking/ranking_rules/use_segmentation';
const XML_PATH_SEGMENT_COOKIE_NAME = 'beyable_ranking/ranking_rules/segment_cookie_name';
const XML_PATH_CRON_ENABLED = 'beyable_ranking/sync/cron_enabled';
const XML_PATH_TIMEOUT = 'beyable_ranking/sync/timeout';
const XML_PATH_APPLY_TO_CATEGORIES = 'beyable_ranking/advanced/apply_to_categories';
const XML_PATH_APPLY_TO_SEARCH = 'beyable_ranking/advanced/apply_to_search';
const XML_PATH_DEBUG_LOGGING = 'beyable_ranking/advanced/debug_logging';
/**
* @var ScopeConfigInterface
*/
protected $scopeConfig;
/**
* @param ScopeConfigInterface $scopeConfig
*/
public function __construct(ScopeConfigInterface $scopeConfig)
{
$this->scopeConfig = $scopeConfig;
}
/**
* Check if module is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_ENABLED,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Get account ID
*
* @param int|null $storeId
* @return string
*/
public function getAccountId($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_ACCOUNT_ID,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Get subscription key
*
* @param int|null $storeId
* @return string
*/
public function getSubscriptionKey($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_SUBSCRIPTION_KEY,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Get API base URL
*
* @param int|null $storeId
* @return string
*/
public function getApiBaseUrl($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_API_BASE_URL,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Get default ranking ID
*
* @param int|null $storeId
* @return string
*/
public function getDefaultRankingId($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_DEFAULT_RANKING_ID,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Check if segmentation is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isSegmentationEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_USE_SEGMENTATION,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Get segment cookie name
*
* @param int|null $storeId
* @return string
*/
public function getSegmentCookieName($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_SEGMENT_COOKIE_NAME,
ScopeInterface::SCOPE_STORE,
$storeId
) ?: 'b_segment';
}
/**
* Get API timeout in seconds
*
* @param int|null $storeId
* @return int
*/
public function getTimeout($storeId = null)
{
return (int)$this->scopeConfig->getValue(
self::XML_PATH_TIMEOUT,
ScopeInterface::SCOPE_STORE,
$storeId
) ?: 30;
}
/**
* Check if debug logging is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isDebugLoggingEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_DEBUG_LOGGING,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Check if ranking should be applied to categories
*
* @param int|null $storeId
* @return bool
*/
public function shouldApplyToCategories($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_APPLY_TO_CATEGORIES,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
/**
* Check if ranking should be applied to search
*
* @param int|null $storeId
* @return bool
*/
public function shouldApplyToSearch($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_APPLY_TO_SEARCH,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
}
Step 6: Cron Job Configuration (etc/crontab.xml)
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="beyable_sync_rankings" instance="Beyable\ProductRanking\Cron\SyncRankings" method="execute">
<config_path>beyable_ranking/sync/cron_schedule</config_path>
</job>
</group>
</config>
Step 7: Cron Job Implementation (Cron/SyncRankings.php)
<?php
namespace Beyable\ProductRanking\Cron;
use Beyable\ProductRanking\Model\Config;
use Beyable\ProductRanking\Model\RankingManagement;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;
class SyncRankings
{
/**
* @var Config
*/
protected $config;
/**
* @var RankingManagement
*/
protected $rankingManagement;
/**
* @var StoreManagerInterface
*/
protected $storeManager;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param Config $config
* @param RankingManagement $rankingManagement
* @param StoreManagerInterface $storeManager
* @param LoggerInterface $logger
*/
public function __construct(
Config $config,
RankingManagement $rankingManagement,
StoreManagerInterface $storeManager,
LoggerInterface $logger
) {
$this->config = $config;
$this->rankingManagement = $rankingManagement;
$this->storeManager = $storeManager;
$this->logger = $logger;
}
/**
* Execute cron job
*
* @return void
*/
public function execute()
{
$this->logger->info('BEYABLE: Starting ranking synchronization');
$stores = $this->storeManager->getStores();
foreach ($stores as $store) {
$storeId = $store->getId();
if (!$this->config->isEnabled($storeId)) {
continue;
}
try {
$this->logger->info(sprintf('BEYABLE: Syncing rankings for store %s', $store->getCode()));
// Sync default rankings
$this->rankingManagement->syncRankingsForStore($storeId);
$this->logger->info(sprintf('BEYABLE: Successfully synced rankings for store %s', $store->getCode()));
} catch (\Exception $e) {
$this->logger->error(sprintf(
'BEYABLE: Error syncing rankings for store %s: %s',
$store->getCode(),
$e->getMessage()
));
}
}
$this->logger->info('BEYABLE: Ranking synchronization completed');
}
}
Step 8: Product Collection Plugin (Plugin/Catalog/Model/ResourceModel/Product/Collection.php)
<?php
namespace Beyable\ProductRanking\Plugin\Catalog\Model\ResourceModel\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
use Beyable\ProductRanking\Model\Config;
use Beyable\ProductRanking\Helper\Data as RankingHelper;
use Magento\Framework\App\RequestInterface;
class Collection
{
/**
* @var Config
*/
protected $config;
/**
* @var RankingHelper
*/
protected $rankingHelper;
/**
* @var RequestInterface
*/
protected $request;
/**
* @param Config $config
* @param RankingHelper $rankingHelper
* @param RequestInterface $request
*/
public function __construct(
Config $config,
RankingHelper $rankingHelper,
RequestInterface $request
) {
$this->config = $config;
$this->rankingHelper = $rankingHelper;
$this->request = $request;
}
/**
* Apply BEYABLE ranking to product collection
*
* @param ProductCollection $subject
* @param bool $printQuery
* @param bool $logQuery
* @return array
*/
public function beforeLoad(ProductCollection $subject, $printQuery = false, $logQuery = false)
{
if ($subject->isLoaded()) {
return [$printQuery, $logQuery];
}
$storeId = $subject->getStoreId();
if (!$this->config->isEnabled($storeId)) {
return [$printQuery, $logQuery];
}
// Check if we should apply ranking based on context
$fullActionName = $this->request->getFullActionName();
$shouldApply = false;
if (strpos($fullActionName, 'catalog_category_view') !== false && $this->config->shouldApplyToCategories($storeId)) {
$shouldApply = true;
} elseif (strpos($fullActionName, 'catalogsearch_result') !== false && $this->config->shouldApplyToSearch($storeId)) {
$shouldApply = true;
}
if (!$shouldApply) {
return [$printQuery, $logQuery];
}
// Get visitor segment if segmentation is enabled
$segmentId = null;
if ($this->config->isSegmentationEnabled($storeId)) {
$segmentId = $this->rankingHelper->getVisitorSegment();
}
// Join ranking table
$this->applyRankingJoin($subject, $storeId, $segmentId);
return [$printQuery, $logQuery];
}
/**
* Apply ranking join to collection
*
* @param ProductCollection $collection
* @param int $storeId
* @param string|null $segmentId
* @return void
*/
protected function applyRankingJoin(ProductCollection $collection, $storeId, $segmentId = null)
{
$select = $collection->getSelect();
$rankingTable = $collection->getTable('beyable_product_ranking');
// Build join conditions
$conditions = [
'e.entity_id = beyable_ranking.product_id',
$select->getAdapter()->quoteInto('beyable_ranking.store_id = ?', $storeId)
];
if ($segmentId) {
$conditions[] = $select->getAdapter()->quoteInto('beyable_ranking.segment_id = ?', $segmentId);
} else {
$conditions[] = 'beyable_ranking.segment_id IS NULL';
}
// Join ranking table
$select->joinLeft(
['beyable_ranking' => $rankingTable],
implode(' AND ', $conditions),
['beyable_position' => 'position', 'beyable_ab_test_group' => 'ab_test_group']
);
// Order by ranking position, fallback to default
$select->order([
'beyable_ranking.position ASC',
'e.entity_id DESC'
]);
}
}
Step 9: Helper for Segment Detection (Helper/Data.php)
<?php
namespace Beyable\ProductRanking\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\Stdlib\CookieManagerInterface;
use Beyable\ProductRanking\Model\Config;
class Data extends AbstractHelper
{
/**
* @var CookieManagerInterface
*/
protected $cookieManager;
/**
* @var Config
*/
protected $config;
/**
* @param Context $context
* @param CookieManagerInterface $cookieManager
* @param Config $config
*/
public function __construct(
Context $context,
CookieManagerInterface $cookieManager,
Config $config
) {
parent::__construct($context);
$this->cookieManager = $cookieManager;
$this->config = $config;
}
/**
* Get visitor segment from cookie
*
* @return string|null
*/
public function getVisitorSegment()
{
$cookieName = $this->config->getSegmentCookieName();
return $this->cookieManager->getCookie($cookieName);
}
/**
* Get tenant ID for current store
*
* @param int|null $storeId
* @return string
*/
public function getTenantId($storeId = null)
{
// You can customize this logic based on your store structure
// For example, use store code or locale
$storeManager = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Store\Model\StoreManagerInterface::class);
$store = $storeId ? $storeManager->getStore($storeId) : $storeManager->getStore();
return $store->getCode();
}
}
Testing
Manual Testing
Configuration Test:
php bin/magento config:set beyable_ranking/general/enabled 1
php bin/magento config:set beyable_ranking/general/account_id YOUR_ACCOUNT_ID
php bin/magento config:set beyable_ranking/general/subscription_key YOUR_KEY
php bin/magento cache:flushManual Sync Test:
php bin/magento beyable:sync-rankingsVerify Database:
SELECT * FROM beyable_product_ranking LIMIT 10;Frontend Test:
- Visit a category page
- Check if products are ordered according to BEYABLE rankings
- Inspect browser console for any JavaScript errors
Automated Testing
Create a test file: Test/Unit/Model/Api/ClientTest.php
<?php
namespace Beyable\ProductRanking\Test\Unit\Model\Api;
use PHPUnit\Framework\TestCase;
use Beyable\ProductRanking\Model\Api\Client;
class ClientTest extends TestCase
{
protected $client;
protected function setUp(): void
{
// Setup test dependencies
}
public function testParseCsvResponse()
{
$csvContent = '"ProductId","abTestGroup","Position"
"123","",1
"456","",2';
// Test CSV parsing
$this->assertCount(2, $result);
}
}
Deployment
Production Deployment Checklist
Pre-deployment:
# Backup database
php bin/magento setup:backup --db
# Enable maintenance mode
php bin/magento maintenance:enableDeploy module:
# Copy module files
# Run setup
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy
php bin/magento cache:flushConfigure via Admin:
- Navigate to: Stores > Configuration > BEYABLE > Product Ranking
- Enter your credentials
- Configure ranking rules
- Save configuration
Initial Sync:
php bin/magento beyable:sync-rankingsVerify:
- Check logs:
var/log/system.log - Verify rankings in database
- Test on frontend
- Check logs:
Post-deployment:
# Disable maintenance mode
php bin/magento maintenance:disable
Troubleshooting
Common Issues
1. API Authentication Errors
Symptom: HTTP 401 errors in logs
Solution:
- Verify Subscription-Key is correct
- Check if key is properly encrypted in database
- Test API manually with curl:
curl -H "Subscription-Key: YOUR_KEY" \
"https://api.beyable.com/productranking/ACCOUNT_ID/RANKING_ID/TENANT"
2. No Rankings Applied
Symptom: Products not ordered by BEYABLE ranking
Solution:
- Check if module is enabled:
php bin/magento config:show beyable_ranking/general/enabled - Verify rankings exist in database:
SELECT COUNT(*) FROM beyable_product_ranking; - Check if cron is running:
SELECT * FROM cron_schedule WHERE job_code = 'beyable_sync_rankings'; - Enable debug logging and check logs
3. Cron Not Running
Symptom: Rankings not updating
Solution:
# Check cron status
php bin/magento cron:run
# Verify cron schedule
SELECT * FROM cron_schedule WHERE job_code = 'beyable_sync_rankings' ORDER BY scheduled_at DESC LIMIT 5;
# Manually trigger sync
php bin/magento beyable:sync-rankings
4. Performance Issues
Symptom: Slow category/search pages
Solution:
- Add indexes to ranking table
- Implement Redis cache for rankings
- Reduce sync frequency
- Use flat catalog
5. Segment Not Detected
Symptom: Segmented rankings not working
Solution:
- Verify cookie name matches configuration
- Check browser console for cookie value
- Test with manual cookie:
document.cookie = "b_segment=premium"; - Verify BEYABLE tracking script is loaded
Debug Commands
# Enable debug mode
php bin/magento config:set beyable_ranking/advanced/debug_logging 1
# Check configuration
php bin/magento config:show beyable_ranking
# View recent logs
tail -f var/log/system.log | grep BEYABLE
# Check database
mysql -u USER -p DATABASE_NAME -e "SELECT product_id, sku, position, segment_id FROM beyable_product_ranking ORDER BY position LIMIT 20;"
# Test API connection
php bin/magento beyable:test-connection
Performance Optimization
1. Database Optimization
-- Add composite index for faster lookups
CREATE INDEX idx_beyable_ranking_lookup
ON beyable_product_ranking(store_id, segment_id, position);
-- Add covering index
CREATE INDEX idx_beyable_ranking_covering
ON beyable_product_ranking(store_id, segment_id, product_id, position);
2. Caching Strategy
Implement Redis cache in Model/RankingManagement.php:
protected function getCachedRankings($storeId, $segmentId = null)
{
$cacheKey = sprintf('beyable_rankings_%d_%s', $storeId, $segmentId ?: 'default');
if ($cached = $this->cache->load($cacheKey)) {
return unserialize($cached);
}
// Fetch from database
$rankings = $this->fetchFromDatabase($storeId, $segmentId);
// Cache for 2 hours
$this->cache->save(serialize($rankings), $cacheKey, [], 7200);
return $rankings;
}
3. Async Processing
Use message queue for large catalogs:
<!-- etc/queue.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd">
<broker topic="beyable.ranking.sync" exchange="beyable" type="db">
<queue name="beyable.ranking.sync" consumer="beyableRankingSync" handler="Beyable\ProductRanking\Model\Queue\Consumer\RankingSync::process"/>
</broker>
</config>
4. Varnish Configuration
Add to VCL:
# Allow BEYABLE segment cookie to pass through
if (req.http.cookie ~ "b_segment") {
set req.http.X-Beyable-Segment = regsub(req.http.cookie, "^.*?b_segment=([^;]+);*.*$", "\1");
hash_data(req.http.X-Beyable-Segment);
}
Support
For additional assistance:
- Module Issues: Check
var/log/system.logandvar/log/exception.log - BEYABLE API Issues: Contact your BEYABLE account manager
- Magento Issues: Refer to Magento 2 documentation
Appendix
A. Console Commands Reference
Create Console/Command/TestConnection.php:
<?php
namespace Beyable\ProductRanking\Console\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Beyable\ProductRanking\Model\Api\Client;
use Beyable\ProductRanking\Model\Config;
class TestConnection extends Command
{
protected $client;
protected $config;
public function __construct(
Client $client,
Config $config,
string $name = null
) {
parent::__construct($name);
$this->client = $client;
$this->config = $config;
}
protected function configure()
{
$this->setName('beyable:test-connection')
->setDescription('Test BEYABLE API connection');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('<info>Testing BEYABLE API connection...</info>');
try {
$ruleId = $this->config->getDefaultRankingId();
$rankings = $this->client->fetchRankings($ruleId);
$output->writeln(sprintf('<info>Success! Retrieved %d rankings</info>', count($rankings)));
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}
Register in etc/di.xml:
<type name="Magento\Framework\Console\CommandList">
<arguments>
<argument name="commands" xsi:type="array">
<item name="beyableTestConnection" xsi:type="object">Beyable\ProductRanking\Console\Command\TestConnection</item>
<item name="beyableSyncRankings" xsi:type="object">Beyable\ProductRanking\Console\Command\SyncRankings</item>
</argument>
</arguments>
</type>
B. API Endpoints Quick Reference
| Endpoint | Purpose | Segmentation |
|---|---|---|
/productranking/{accountId}/{rankingId} | Default tenant, no segmentation | No |
/productranking/{accountId}/{rankingId}/{tenant} | Specific tenant, no segmentation | No |
/segmented-productranking/{accountId}/{rankingId}/{segmentId} | Default tenant, with segmentation | Yes |
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant} | Specific tenant, with segmentation | Yes |