TL;DR — In PrestaShop, saving one product can silently wipe a feature from other products nobody touched. This repo contains a drop-in override that stops it, plus patches for the core.
Affected versions: 1.6 · 1.7 · 8.x · 9.x — verified identical in 1.6.1.24, 1.7.8.11, 8.2.7 and 9.1.1.
Two behaviours in classes/Product.php combine into silent, permanent data loss:
Called on every product save:
foreach ($features as $tab) {
// Delete product custom features
if ($tab['custom']) {
Db::getInstance()->execute('DELETE FROM `ps_feature_value` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']);
Db::getInstance()->execute('DELETE FROM `ps_feature_value_lang` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']);
}
}The DELETE is global. It does not check whether other products reference that value. Those products keep their ps_feature_product row pointing at an id_feature_value that no longer exists — the feature renders blank on the front office.
setWsProductFeatures() inserts the received id_feature_value as-is, with no validation:
foreach ($product_features as $product_feature) {
$this->addFeaturesToDB($product_feature['id'], $product_feature['id_feature_value']);
}And getWsProductFeatures() strips the custom flag from the API response:
unset(
$rows[$keyrow]['id_product'],
$rows[$keyrow]['custom'] // ← the "is this custom?" indicator is removed
);An ERP/PIM connector reading one product's features and writing them to another has no way of knowing it is reusing someone else's custom value. It does the reasonable thing — resend the id PrestaShop just gave it — and causes the damage anyway.
This is a core API defect, not an integration bug.
ps_feature_product has no ObjectModel, so PrestaShop fires no hook on these writes. No module can observe them without database triggers. The edited product looks perfect; the damage lands elsewhere and surfaces only when a customer complains.
-- 1. Damage already done: associations pointing to a deleted value
SELECT fp.id_product, fp.id_feature, fp.id_feature_value
FROM ps_feature_product fp
LEFT JOIN ps_feature_value fv ON fv.id_feature_value = fp.id_feature_value
WHERE fv.id_feature_value IS NULL;
-- 2. Ticking bombs: custom values shared by more than one product
SELECT fv.id_feature_value, COUNT(DISTINCT fp.id_product) AS products
FROM ps_feature_value fv
INNER JOIN ps_feature_product fp ON fp.id_feature_value = fv.id_feature_value
WHERE fv.custom = 1
GROUP BY fv.id_feature_value
HAVING products > 1
ORDER BY products DESC;
-- 3. Quick health ratio (> 1.5 means values pile up as orphans)
SELECT
(SELECT COUNT(*) FROM ps_feature_value) AS values_count,
(SELECT COUNT(*) FROM ps_feature_product) AS associations,
ROUND((SELECT COUNT(*) FROM ps_feature_value) /
(SELECT COUNT(*) FROM ps_feature_product), 2) AS ratio;Query 1 returning rows means products are showing blank features right now.
- Copy
override/classes/Product.phpto your shop'soverride/classes/Product.php - Delete
var/cache/prod/class_index.php(and thedevone if present) - Clear the cache in Advanced Parameters → Performance
If you already have an
override/classes/Product.php, do not replace it — copy the four methods into your existing class.
Only if you are preparing a PR. Lost on update.
php patches/apply-patches.php /path/to/PrestaShop/classes/Product.php --dry-run
php patches/apply-patches.php /path/to/PrestaShop/classes/Product.php
php -l /path/to/PrestaShop/classes/Product.phpMatches by code pattern, not line number, so it works across 1.7.x / 8.x / 9.x. Creates a timestamped backup and is idempotent.
| # | Method | Change |
|---|---|---|
| 1 | deleteFeatures() |
A custom value is deleted only if no other product uses it |
| 2 | setWsProductFeatures() |
A custom value already owned by another product is cloned, not shared |
| 3 | getWsProductFeatures() |
Stops stripping the custom flag |
| 4 | $webserviceParameters |
Declares custom as an exposed field |
Catalogue values (custom = 0) are untouched — sharing those between products is correct and intended.
Change #1 alone stops the data loss. It is backwards compatible: when the product is the sole owner, the value is still deleted as before.
- Fixes new damage only. It does not repair what is already broken.
- The text of already-deleted values is unrecoverable —
deleteFeatures()removed it fromps_feature_value_langtoo. It must be re-entered from your ERP or restored from a backup. - Does not cover the Symfony layer.
ProductFeatureValueUpdateris a service, not a legacy class, so the classic override system cannot replace it — it would need decoration viaservices.yml. The legacy path, which is what shops hit in practice, is covered.
| Check | Result |
|---|---|
php -l on the override (7.4 / 8.1 / 8.3) |
No syntax errors |
Patches applied to real PS 9.1.1 Product.php |
4/4 applied |
| Syntax of the patched file (7.4 / 8.1 / 8.3) | No syntax errors |
| Re-running the patcher (idempotency) | 4/4 skipped, nothing duplicated |
Not tested against a live database. Back up and test on staging before production.
Measured on a production shop (PrestaShop 8.2.1) synchronised from an external ERP via the webservice:
| Associations sharing a custom value | 1,704 |
| Associations already pointing to a deleted value | 824 |
| Feature values vs. real associations | 16,978 / 6,364 (ratio 2.67) |
While the ERP account had write permission, product updates arrived in bursts — up to 32 products sharing the exact same date_upd down to the second, 93 within a single minute. After the permission was revoked, the bursts stopped and so did new damage.
Reported to the PrestaShop project: PrestaShop/PrestaShop#42347
Full issue text with reproduction steps and proposed patches: docs/GITHUB_ISSUE.md.
If you hit this bug, please comment on issue #42347 — the more affected shops are documented, the sooner it gets fixed in core.
Si en tu PrestaShop desaparecen características personalizadas de productos que nadie ha editado, este repositorio contiene la corrección.
La causa: varios productos comparten el mismo valor de característica personalizado. Al guardar uno, Product::deleteFeatures() borra ese valor de la base de datos sin comprobar si otros lo usan. Los demás quedan apuntando a un valor inexistente y su característica sale vacía.
Cómo llegan a compartirse: por el webservice. El API acepta que varios productos usen el mismo valor personalizado y, además, oculta el campo custom en sus respuestas, así que la integración externa no puede saber que está reutilizando un valor ajeno.
La solución: copia override/classes/Product.php a tu tienda, borra var/cache/prod/class_index.php y vacía la caché.
Ojo: esto evita daños nuevos, no repara los existentes. Y el texto de los valores ya borrados es irrecuperable.
AFL-3.0 — same license as PrestaShop modules.
Ecom Experts · ecomyseo@gmail.com