Skip to content

Developer notes

Overview · Installation · Configuration · Usage · FAQ

Every change is written with core fn_update_product($data, $product_id, $lang_code), one product at a time, with only the changed fields in $data:

  • built-in fields as on the product page (price, list_price, amount, status, weight, free_shipping, shipping_freight, product, product_code, page_title, meta_description, meta_keywords, search_words, short_description, full_description, seo_name);
  • categories as category_ids + main_category;
  • features as product_features[<feature_id>] (variant id for select features, '' removes the value).

So every hook of core and other add-ons runs (update_product_pre, update_product_post, update_product_categories_post, update_product_features_value_pre, …): SEO names and redirects, product variations, master products, vendor premoderation, stock notifications, block cache table tags and full page cache bans behave as after a save on the product page. Core notifications raised during a save are collected and shown in the grid instead of as pop-ups.

After the saves of a request (or a step), the values are read back from the database. The change log stores the real old and new values; a requested change that core or an add-on refused is reported as not accepted and not logged.

Price: the base price (quantity 1, all user groups) in ?:product_prices; in Ultimate with a storefront selected, that storefront’s ?:ult_product_prices row when it exists. Texts: ?:product_descriptions in the content language, ?:ult_product_descriptions of the selected storefront when it exists.

Table Purpose
?:kv_bpe_batches One row per save, mass action or undo: user, company (vendor or storefront scope), language, type (I inline, M mass, U undo), status (R running, A applied, X undone, F failed), summary (JSON: fields or the action), product ids of the scope (JSON), cursor, counters, messages, times
?:kv_bpe_changes One row per changed cell: batch, product, field (price, categories, f_<feature_id>, …), old value, new value
?:kv_bpe_views Saved views: user, company, name, shared flag, parameters (JSON: filters, columns, page size)

Values are stored as canonical strings: prices 12.50, weight 1.250, stock -3, status A/D/H, flags Y/N, categories main:12,15,18, select features the variant id.

controllers/backend/kv_bulk_product_editor.php (admin and vendor panel):

Mode Method Purpose
manage GET The page
rows GET One page of products: filters (JSON), columns (JSON), page, per_page
categories, variants GET Category search, variants of a select feature
log, changes GET Change log, cell changes of an entry
save POST Inline changes: changes = JSON [{id, field, value, orig}] (orig = the value the grid loaded, for the conflict check)
preview, apply_start POST Mass action: spec (JSON: field, op, value, mode, rounding, find, case, categories), scope (selected with ids or filter with filters)
step POST Runs the next step of a running action or undo (batch_id)
undo_start POST Starts the undo of an entry
view_save, view_delete POST Saved views

POST requests need the admin security_hash (core CSRF check). Permissions: schemas/permissions/admin.post.php (GET view_catalog, POST manage_catalog) and vendor_multivendor.post.php (the vendor setting). Every product id is checked against what core fn_get_products() shows the current user; entries and views are scoped to the user’s company or storefront.

  • The grid pages through fn_get_products() with the filters, so core and add-on conditions (vendor scope, storefront scope, variations) apply; the visible columns are then read with a few batch queries per page.
  • A mass action resolves its scope once (product ids in pages of 1,000, at most 100,000) and stores it in the batch. Each step claims the next slice atomically (a second tab or a double click never applies a slice twice) and saves the products of the slice.
  • Measured on Multi-Vendor 4.21.2 / PHP 8.3 with 10,000 products: a page of 250 rows with filter and sorting in about 0.15 s; resolving 10,000 ids in about 1 s; a price change on 10,000 products in 200 steps of 50 in about 80 s (slowest step under 0.5 s); its undo in about the same time. Times depend on the server and on the add-ons that act on product saves.

The add-on has no storefront output and writes nothing from storefront requests. Saved products are refreshed in the page cache by core (update_product_post).

Tygh\Addons\KvBulkProductEditor\License\Client (generated) verifies the license. Gate points (fn_kv_bulk_product_editor_is_licensed(), Access::licensed() in class code): the save and apply_start modes. Not gated: the grid and all GET endpoints, saved views, the change log, undo_start and step (so an action that was already running can finish and earlier changes can always be undone).

Product options and option combinations, images, multiple-checkbox and date features, creating and deleting products, and file import/export (use core Export/Import). Short and full descriptions are changed with find and replace only.

Path Purpose
app/addons/kv_bulk_product_editor/addon.xml Settings, tables
init.php, func.php License and upgrade blocks, settings info handler, fn_kv_bulk_product_editor_is_licensed()
Tygh/Addons/KvBulkProductEditor/Fields.php Editable fields, validation, canonical values, features
Reader.php Filters, paging, reading grid values (Ultimate overrides, SEO names, features)
Writer.php Saving through fn_update_product(), conflict check, read-back, change log rows
MassAction.php Mass-action rules, rounding, preview
ChangeLog.php Batches, the step runner, undo, log list, retention
Views.php, Access.php, Ui.php Saved views, scope and permissions, configuration of the grid script
License/Client.php, License/Ed25519.php, controllers/backend/kv_bulk_product_editor_license.php License check (generated)
Upgrade/Synchronizer.php Self-healing upgrades (generated)
controllers/backend/kv_bulk_product_editor.php Page and JSON endpoints
schemas/ Menu (admin, vendor panel configurator), permissions (admin, vendor)
design/backend/templates/addons/kv_bulk_product_editor/ Page template, license card/bar/badge (license/info.tpl for the License tab)
design/backend/css/addons/kv_bulk_product_editor/styles.less Grid styles (admin LESS variables, light and dark)
js/addons/kv_bulk_product_editor/grid.js The grid (ES5 + jQuery, no external libraries)