Version 1.0.0
August 29, 2026
- Fixed a critical bug where real-time sync and full reindex both silently indexed nothing, ever.
CatalogSyncService::isIndexable()required a variant'sgetStatus()to equal the literal string"live"— butVariant, unlikeProduct, has no post/expiry date of its own and never reportslive/pending/expired, onlyenabled/disabled(already covered separately). Every variant therefore always failed this check, so every sync attempt — real-time andreindexalike — always took the "not indexable" branch and queued a deletion instead of an upsert. Verified live:buildVariantPayloads()(the exact methodreindexuses) returned zero payloads for a completely normal, fully enabled, live product. Fixed by only checking live/pending/expired status on the product, which is the only one of the two that actually has that concept. - Fixed a crash when the configured
image_linkfield exists only on the product, not the variant — exactly the setup the plugin's own documentation recommends ("just set it once on the product"). Resolving the image checks the variant first, falling back to the product; but callingElement::getFieldValue()for a handle that isn't part of that specific element's field layout throws, rather than returning null, so the recommended configuration crashed every single sync. Fixed by checking the field layout before reading the value. - Fixed the same field-layout crash for
categoriesFieldHandleandfieldMappingRows. BothCategoryResolverandCatalogSyncService::resolveCustomFields()calledgetFieldValue()unconditionally too — a merchant with more than one product type only needs the configured field missing from one of them to crash every sync for that type, categories and mapped custom fields alike. Both now check the field layout first, same fix asimage_link, and simply omit that part of the payload rather than throwing. Found while integration-testing a sibling Kernpfad plugin against this shared test install. - Fixed the "Test connection" button doing nothing when clicked from the real Settings → Plugins page.
settings.twig's inline JS looked up its button and result elements by literalid, but Craft's generic plugin-settings page renders every field throughview->namespaceInputs(), which rewrites everyidtosettings-<original>— the literal lookup always returned null, so the click handler was silently never attached. Fixed via|namespaceInputId.
Both the indexing bug and the field-layout crash were found live while seeding real product/category/image data to screenshot the settings and sync-status screens for documentation — the button bug came from noticing Test connection produced no visible reaction at all on the real settings page.
- Real-time sync of products/variants to a Doofinder search index on save/delete, grouped by product via
group_id/group_leader. php craft commerce-doofinder/reindex— a zero-downtime full-catalog reindex using Doofinder's temporary-index + atomic-replace workflow.- Custom field mapping — map any Craft product field to any Doofinder item custom field.
- Configurable queue component, same pattern as Commerce Klaviyo.
- Every Doofinder API call runs on the queue; a Doofinder outage never blocks a product save.
- Added explicit request timeouts (5s connect, 15s total; 60s for the bulk endpoint used by the reindex command) to the Doofinder API client. Craft's shared Guzzle client sets none, so an unresponsive Doofinder would have held a queue worker open until PHP's execution limit rather than failing and letting Craft retry.
- Fixed: drafts and revisions were being indexed as if they were real products. Because Craft creates a revision on every control-panel publish, an actively-edited store accumulated a junk entry in the live search index per edit — keyed to the draft/revision's element ID, never removed, and shown in real customer search results. Drafts, revisions, and multi-site propagation saves are now all skipped.
- Fixed: disabled products and variants remained in the Doofinder index after being turned off. Disabled variants are now removed on save; disabling a whole product removes every variant item even when Commerce does not fire separate variant save events.
- Fixed: a null product on variant delete could crash job queueing.
- Added:
FieldValueNormalizerto flatten Craft field values (assets, options, categories, dates, etc.) into JSON-safe scalars before they are sent to Doofinder. - Added:
declare(strict_types=1)across all plugin and test source files. - Added: automatic
image_linkresolution. NewimageFieldHandlesetting (checked on the variant first, falling back to the product) plus an optionalimageTransformHandlefor a named image transform. - Added:
availabilityandstock_quantityon every synced item, sourced from Commerce's own inventory system (Purchasable::getIsAvailable()/getStock()). Inventory-untracked variants omitstock_quantityrather than reporting a misleading0. - Added:
apiTokenandsearchEngineHashIdnow accept environment variable aliases (e.g.$DOOFINDER_API_TOKEN), resolved viacraft\helpers\App::parseEnv(), so real credentials never have to be committed to project config. The settings screen now uses Craft's standard env-var-autosuggest field for both. - Fixed: disabling a product on a single-site install did not remove it from the Doofinder index.
CatalogSyncServiceonly checkedgetEnabledForSite(), the per-site override — it never looked at theenabledglobal toggle, which is what a single-site product's edit page actually writes (per Craft's ownElement::getStatus(), an element is disabled if either flag is off). Any store disabling products the normal way was leaving them live and purchasable in Doofinder search. - Added a connection test —
php craft commerce-doofinder/testand a "Test connection" button on the settings screen — that fetches the configured index's own metadata (GET .../search_engines/{hashid}/indices/{name}, verified against Doofinder's live API) to confirm the API token, search engine hash ID and index name are all valid together, without creating, modifying or deleting anything. - Added: automatic Doofinder
categoriespaths from a configured Categories field handle or auto-discovered from the product's field layout (Parent > Child > Leafbreadcrumb format). - Added:
sale_priceon items when a variant's promotional price is lower than its base price (was already wired; now documented). - Added: last sync/reindex status on the plugin settings screen, persisted via
SyncStatusService. - Added: pending (future post date) and expired products/variants are removed from the index consistently, same as disabled elements.
- Added:
reindex --if-staleand--stale-hoursto skip full reindexes when the catalog was recently rebuilt; cron example and last-reindex timestamp on the settings screen. - Added:
CommerceDoofinder::EVENT_MODIFY_ITEM_PAYLOADso project code can mutate item payloads before sync. - Changed: custom field mapping is edited via an editable table in the control panel (legacy
fieldMappingRawlines remain supported).