Skip to main content

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​

  1. Prerequisites
  2. Architecture Overview
  3. Installation
  4. Module Structure
  5. Configuration
  6. Implementation Steps
  7. Testing
  8. Deployment
  9. Troubleshooting
  10. 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:

  1. BEYABLE Cookie Reading: Read visitor segment from cookie set by BEYABLE Platform
  2. API Integration: Fetch rankings from BEYABLE API via scheduled cron jobs
  3. Database Storage: Store rankings in custom Magento table
  4. Product Collection Modification: Apply rankings to product listings
  5. 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.eu1.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​

  1. 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:flush
  2. Manual Sync Test:

    php bin/magento beyable:sync-rankings
  3. Verify Database:

    SELECT * FROM beyable_product_ranking LIMIT 10;
  4. 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​

  1. Pre-deployment:

    # Backup database
    php bin/magento setup:backup --db

    # Enable maintenance mode
    php bin/magento maintenance:enable
  2. Deploy 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:flush
  3. Configure via Admin:

    • Navigate to: Stores > Configuration > BEYABLE > Product Ranking
    • Enter your credentials
    • Configure ranking rules
    • Save configuration
  4. Initial Sync:

    php bin/magento beyable:sync-rankings
  5. Verify:

    • Check logs: var/log/system.log
    • Verify rankings in database
    • Test on frontend
  6. 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.eu1.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.log and var/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​

EndpointPurposeSegmentation
/productranking/{accountId}/{rankingId}Default tenant, no segmentationNo
/productranking/{accountId}/{rankingId}/{tenant}Specific tenant, no segmentationNo
/segmented-productranking/{accountId}/{rankingId}/{segmentId}Default tenant, with segmentationYes
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant}Specific tenant, with segmentationYes