Developer notes
Overview · Installation · Configuration · Usage · FAQ
How the recommendations are built
Section titled “How the recommendations are built”Builder::run() reads basket heads (orders with parent_order_id = 0: single orders and Multi-Vendor parent orders) in chunks of 500 order ids, takes the products of the head and its vendor orders whose status is in the selected statuses, skips baskets with more distinct products than the limit, and adds for every ordered product pair (A, B):
orders: number of baskets with A and B (support),score: the sum of the basket weights0.5 ^ (age in days / half-life)(1 when the half-life is 0),
and per product the number of baskets that contain it. Both go into staging tables with multi-row upserts. When all chunks are read, one transaction replaces the live pairs with the pairs that pass the minimum support and confidence (orders(A,B) * 100 / orders(A)). A run stops after its time budget (25 s for cron URL / admin fallback) and the next run continues from the saved cursor; a lock prevents parallel runs. Run marks and the cursor are stored with StorageData (files instead of ?:storage_data while the full page cache is on).
Candidates for a product: pins (in order) → pairs by score → category fallback (main category, by ?:product_popularity.total), minus exclusions, excluded categories and, in Multi-Vendor, other vendors. They are loaded with fn_get_products(['pid' => …]) plus fn_gather_additional_products_data() (storefront visibility, user groups, prices with catalog promotions) and filtered by Recommender::isPurchasable().
Data model
Section titled “Data model”| Table | Contents |
|---|---|
?:kv_fbt_pairs |
company_id (Ultimate storefront, otherwise 0), product_id, related_id, orders, score, confidence |
?:kv_fbt_pairs_tmp, ?:kv_fbt_counts_tmp |
Staging of a running build |
?:kv_fbt_manual |
product_id, related_id, type (P pin, X exclude, H hide), position |
?:kv_fbt_stats |
Daily counters per day, storefront, product and placement (P product block, C cart block): impressions, clicks, items |
?:kv_fbt_orders |
Attributed order lines: order_id (real vendor order in Multi-Vendor), product_id, source_id, amount, subtotal, timestamp |
| Hook | Use |
|---|---|
pre_add_to_cart |
Requests from our blocks carry kv_fbt_source: lines whose product is the source or one of its candidates get extra.kv_fbt = source; the click is counted. A kv_fbt marker sent from outside is removed; cart updates ($update) keep existing markers. |
place_order |
Stores the order lines with the marker (?:kv_fbt_orders). |
delete_order, delete_product_post |
Clean-up. |
update_product_post |
Saves the Bought together tab (product_data[kv_fbt]), vendors limited to their own products. |
before_dispatch |
Admin fallback rebuild at shutdown when due. |
Template products:product_tabs (pre) |
The product block. |
Template checkout:cart_content_bottom_left_buttons (post) |
The cart block (moved below the cart buttons by the script). |
| Promotion schema | Bonus kv_fbt_discount (Discount::apply(), using the core fn_promotion_apply_discount()). The core skips unknown bonuses, so a disabled add-on leaves such promotions harmless. |
Endpoints
Section titled “Endpoints”| Dispatch | |
|---|---|
kv_frequently_bought_together.track (storefront, POST) |
Impression beacon (navigator.sendBeacon); no CSRF token needed (schemas/security/csrf_validation.post.php), counts only active products. GET answers 404. |
kv_frequently_bought_together.cron?key=… (storefront, GET) |
Rebuild by URL with the secret key; JSON result. |
php admin.php --dispatch=kv_frequently_bought_together.cron |
Console rebuild without a time limit. |
kv_frequently_bought_together.dashboard / .rebuild (admin) |
Analytics; Rebuild now (POST). Permissions: view reports / manage promotions; vendors are denied. |
Full page cache (4.21) and block cache
Section titled “Full page cache (4.21) and block cache”The product block depends only on the product, settings, pairs, pins and promotions, so cached pages are the same for every visitor (prices follow the core rules of the product page). The core caches the main block of products.view; schemas/block_manager/blocks.post.php adds update handlers for our tables, promotions and settings and makes the license state part of the cache key. No session access and no writes happen on storefront GET requests; impressions are POSTed by a beacon after the page was shown. The cart page is never cached.
License check
Section titled “License check”Tygh\Addons\KvFrequentlyBoughtTogether\License\Client (generated) verifies the license. Gate points: Config::isActive() for the product and cart block data, the impression beacon, marking added products and rebuilds (cron, URL, admin). Not gated: the promotion bonus and attribution of lines already marked, so a started cart keeps its price.
Not in 1.0.0
Section titled “Not in 1.0.0”Block Manager block for free placement, “customers who viewed this also viewed”, per-vendor analytics, quantity selection inside the block, recommendations for products with required text/file options.
File map
Section titled “File map”| Path | Purpose |
|---|---|
app/addons/kv_frequently_bought_together/addon.xml |
Settings, tables, install/uninstall functions |
init.php, func.php |
Hooks, install (cron key, bonus label, first build), cron helpers, fn_kv_frequently_bought_together_is_licensed() |
Tygh/Addons/KvFrequentlyBoughtTogether/Builder.php |
Batched rebuild of the pairs |
Recommender.php |
Candidates, product and cart block data, purchasability |
Manual.php |
Pins, exclusions, hide flag |
Tracker.php, Stats.php |
Impressions, clicks, attribution; analytics queries |
Discount.php |
Promotion bonus and the savings line |
Config.php, StorageData.php |
Settings; run marks safe for the full page cache |
License/, controllers/backend/kv_frequently_bought_together_license.php |
License check (generated) |
Upgrade/Synchronizer.php |
Self-healing upgrades (generated) |
controllers/frontend/, controllers/backend/ |
Product/cart data, beacon, cron, analytics, product tab |
schemas/ |
Menu, permissions, promotion bonus, block cache, CSRF exception, setting variants |
var/themes_repository/responsive/templates/addons/kv_frequently_bought_together/ |
Product and cart block templates and hooks |
js/addons/kv_frequently_bought_together/func.js |
Checkbox totals, beacon, cart block (jQuery, no external libraries) |