Skip to content

Products are saved before their metafields are stored, so a uriFormat using metafields renders empty #230

Description

@mtwalsh

Plugin: 7.2.0 (same code in 8.1.0) · Craft: 5.10.14

Summary

ProcessBulkOperationData::processItem() saves the product element as soon as it reads the Product line of the bulk-operation JSONL. The product's Metafield lines come after it and haven't been stored yet, so a uriFormat that references metafields renders empty and the product is saved with uri = NULL. Nothing saves the product again once the metafields are stored.

For per-product syncs (webhooks → syncProductByShopifyId()) this breaks existing products too: clearData is the product GID, so before() deletes the product's stored metafields before the element is saved. Every product that changes in Shopify loses its URL.

Steps to reproduce

  1. Set Product URI Format to {metafields.canonical_uri} (any metafield will do) and make sure the metafield is populated in Shopify.
  2. Run a full sync and resave the products, then confirm they have URIs.
  3. Edit one product in Shopify so the products/update webhook fires, and let the queue run.
  4. That product's elements_sites.uri is now NULL and product.url is null. Its canonical_uri metafield row is present in shopify_data.
  5. Resaving the product restores the URI.

Cause

src/jobs/ProcessBulkOperationData.php:

$record->save();

// Process the data based on the type
if ($record->type === 'Product') {
    Plugin::getInstance()->getProducts()->createOrUpdateProduct($item);
}

Shopify writes child objects as separate JSONL lines carrying a __parentId, and they follow their parent. When createOrUpdateProduct() → saveElement() → setElementUri() runs, Product::getMetafields() queries shopify_data and finds none of the product's metafields yet: it runs before any of the product's child lines have been stored, and a per-product sync has just deleted the previous rows. The URI renders as an empty string, which Craft stores as NULL.

Variants and images have the same timing problem. Anything that reads them during the product save (element events, search keywords, uriFormat) sees stale or missing data.

Suggested fix

Save each product only once all of its child lines have been stored.

  • Store the Product line's record as now, but don't call createOrUpdateProduct() yet. Keep the pending product (e.g. its GID) in a public property on the job so it survives BaseBatchedJob's batch splitting.
  • When the next top-level line arrives (no __parentId), call createOrUpdateProduct() for the pending product, then make the new line the pending one.
  • Call it for whatever is still pending at the end of processing, in after(), before the bulk op is marked completed.

This needs no extra queries and keeps one element save per product. It assumes that a product's child lines (including nested variant metafields) arrive before the next top-level line, which holds for the data we've seen.

If you'd rather not rely on the ordering, a fallback is to keep the current save but record the product GIDs that were processed. After the last batch, call Elements::updateElementSlugAndUri($product, false, false) for each one, or push an UpdateElementSlugsAndUris job. That's cheap for webhook syncs, which touch one GID, but for full syncs it costs a second write per product.

Workaround we're using meanwhile

Listen for ShopifyData EVENT_AFTER_INSERT / EVENT_AFTER_UPDATE. When the saved row is the product's canonical_uri metafield, reload the product and call updateElementSlugAndUri().

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions