PrestaShop 9 Multistore Modules: Preventing Data-Scope Leaks

44 Views

The most dangerous multistore bug is often a valid query missing one condition. It returns a real record, passes ordinary type checks and saves successfully—into the wrong shop’s state. A single-shop development installation rarely exposes the mistake.

Reliable multistore modules begin with data ownership. Decide which values are global, which belong to a shop group and which belong to one shop. Then carry that decision through keys, reads, writes, background jobs and caches.

This is a general PrestaShop 9 module-design article. It does not describe new 9.2 administration features, and the SQL examples use a deliberately illustrative module table rather than an assumed production prefix or core schema.

When scoped work runs in the background, apply reliable cron-job design while carrying the explicit shop identity into each execution.

Model ownership before adding an id_shop column

Adding id_shop to a table does not make the module multistore-safe. Every operation must use it consistently, and some facts should not be duplicated per shop at all.

Consider an external reference mapping. If the external system assigns a different reference to the same product in each shop, shop scope belongs in the mapping’s identity. If the reference is truly global, duplicating it can create conflicting copies. Establish the external system’s semantics first.

Separate entity association from value ownership. A product may be associated with several shops while a particular module value differs among them. Conversely, an entity associated with one shop may refer to a globally owned record.

Write an ownership table for each module dataset:

Dataset Owner Read scope Write scope
External destination credentials Chosen configuration scope Explicit effective configuration Authorized context
Product mapping Defined by external identity rules Product plus required scope Same identity boundary
Job progress One logical job partition Explicit shop/job key Owning worker
Shared lookup vocabulary Global if genuinely shared Global Controlled administrator action

The table is a design artifact. It should explain why a scope exists, not merely mirror whatever columns were easiest to add.

Scope reads and writes with the same identity

A common defect is a correctly scoped read followed by a broadly scoped update. Another is looking up by product and shop but enforcing uniqueness only by product.

The following query illustrates a module-owned mapping read using parameter binding. The literal table name is an example; a real installer must derive its trusted prefix from the application configuration and its own declared schema.

SELECT external_reference
FROM demo_product_mapping
WHERE id_shop = :shop_id
  AND id_product = :product_id;

The corresponding write must use the same ownership boundary. Do not update solely by id_product after reading with both conditions. The caller should pass a validated scope object or explicit shop identifier rather than relying on a mutable global context inside the repository.

Dynamic identifiers need separate treatment from parameter values. Bound parameters protect values, not arbitrary table names. Never accept a table name or prefix from an HTTP parameter to make a generic multistore query work.

For a batch read, include scope in joins as well as the outer predicate. A join on product alone can attach another shop’s module row even when the main entity query is scoped correctly.

Test the negative case explicitly: ask for shop A’s value when only shop B has one. Returning no local mapping may be correct; silently borrowing B’s mapping is not a safe default.

Make uniqueness express the business rule

If the model is one mapping per product per shop, its key should express that pair. A unique key on product alone rejects valid records from another shop. No unique key permits duplicates under concurrent inserts.

An illustrative module-table definition is:

CREATE TABLE demo_product_mapping (
    id_shop INT UNSIGNED NOT NULL,
    id_product INT UNSIGNED NOT NULL,
    external_reference VARCHAR(191) NOT NULL,
    PRIMARY KEY (id_shop, id_product)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

This is not an import for a live PrestaShop database. It demonstrates the ownership key only. A real schema needs the module’s installation conventions, verified identifier types, migration strategy and any additional relationship constraints.

Do not add uniqueness on external_reference without deciding whether it is globally unique, shop-local or destination-local. If several external systems can assign the same string, destination identity may also belong in the key.

Before adding a constraint to existing data, inspect and resolve duplicates through a defined policy. Deleting whichever row happens to come second is not a migration strategy. Preserve evidence of which record was retained and why.

A database constraint complements application validation. It protects the invariant when two valid-looking requests race.

Treat inherited configuration as state, not just a value

A shop can inherit a value or explicitly store an equal value. They behave differently when the parent changes. A module that reads the effective value and writes it back unconditionally can accidentally turn inheritance into a local override.

PrestaShop’s configuration documentation describes scope-aware access using ShopConstraint, and its form guidance distinguishes local configuration from inherited values. Follow the relevant service contract for the platform version you support. Shops configuration, configuration forms.

When implementing a save, distinguish “set a local value,” “remove the local override” and “leave this field unchanged.” Those operations should not collapse into a single value assignment.

The same care applies to credentials and external destinations. A worker should know which effective configuration it used and which shop it was authorized to process. Logging the destination identifier can help diagnose scope problems without exposing the credential itself.

Avoid inventing id_shop = 0 as a universal global-row convention for every dataset. Core and module storage models differ. Follow the specific schema and service semantics instead of spreading an undocumented sentinel through the codebase.

Carry scope through queued and scheduled work

A background job should contain enough identity to execute without an employee session: shop, operation, relevant entity identifiers and a stable business key. Validate the shop before loading data or configuration.

Do not enqueue only a product ID and let a later worker infer the shop from whatever global context it happens to hold. Long-lived workers can process several messages, making leaked context especially difficult to reproduce.

Where legacy code requires ambient context, isolate the adaptation carefully and restore prior state even when an exception occurs. Prefer explicit scope at your own service boundaries so the legacy dependency does not spread throughout the module.

A job for all shops should iterate a deliberate authorized set, establishing and clearing the appropriate scope for each partition. “All shops” should be an explicit operation with a defined result, not the accidental consequence of a missing argument.

Include scope in lock and idempotency keys where the work is independent by shop. A global lock may unnecessarily serialize unrelated work, while a key missing destination identity may treat two different exports as duplicates.

Give cached data the same dimensions as the query

A cache key is part of the data model. If a result depends on shop, language, currency or customer group, the key and invalidation strategy must reflect the relevant dimensions.

For a module-owned product label, a conceptual key might include module namespace, product ID, shop ID, language ID and a representation version. This is a design pattern, not a prescribed PrestaShop cache-key format.

Only include dimensions that actually affect the value, but do not omit one for convenience. A correct database query followed by a shared cache entry can still leak the first shop’s result into the second.

Test by requesting the same entity in different contexts in both orders: A then B, and B then A. If the result changes with request order, suspect cached or retained context. Clear-cache-only tests can conceal this class of defect.

Invalidation must reach every affected representation. A global value change may require invalidating several shop-specific outputs; a local change should not overwrite unrelated shop data in the cache rebuild process.

Use adversarial fixtures for isolation tests

Create two shops with intentionally conflicting values for a shared entity. Give them different external destination identifiers and different inherited/local configuration states. Then exercise each read and write through the module’s supported interfaces.

The useful assertions include:

  • A write in shop A leaves shop B’s row and effective value unchanged.
  • A read in A never falls back to B without an explicit business rule.
  • An all-shops action affects exactly the selected authorized set.
  • A background job processes the shop named in its payload.
  • Cache results remain correct regardless of request order.
  • Duplicate concurrent writes preserve the declared unique identity.
  • Removing a local override restores inheritance rather than copying a parent value.

Also test missing association and deleted entities. The module should return a defined error or absence, not select an arbitrary remaining row to keep the workflow moving.

For a regression report, retain the two shop IDs, the distinct fixture values and the operation that crossed the boundary. A minimal two-shop case often explains the defect more clearly than a large production export.

Make isolation a review habit

During code review, ask where scope enters the service, where it is validated and whether every persistence and cache operation preserves it. Review query predicates and unique constraints together. Review background message payloads alongside the handlers that consume them.

For existing modules, begin with the highest-impact writes and external integrations. Fixing a scoped read is insufficient if the corresponding update or export still uses global defaults.

Multistore correctness is not a special mode added at the end of development. It is agreement between the meaning of the data and every boundary that handles it. Once that agreement is explicit, cross-shop leaks become testable failures instead of mysterious customer reports.

For the underlying platform context, see our PrestaShop 9 module development guide and apply the version-specific checks above to the installation you are evaluating.

When selecting an extension, browse PrestaSuite’s PrestaShop 9 modules and check the documented version support and integration requirements against your own test results.