.label` / `.value` in the row. * All three adapters fill the same shape, so one spec-table component is identical on a commerce site and * an enquiry site. * * `specs` was added on 2026-08-22 and its origin is worth recording. Before that it was marked "not * implemented", so the only route for NexCart's attributes to reach a layout was the shadow meta * `_aumnexcart_attr_` — and the shadow is **only generated for attributes registered as "filterable" * in NexCart's settings**, while a kit could not write plugin settings at all (the importer's options section * knew only a fixed handful). * The consequence was the spec table completely blank after a buyer imported the kit, with no error, while * our own target site had been configured by hand once and showed nothing wrong — the same class of accident * as "nobody but us has that import button". * An attribute is the product's own data and should not depend on a filtering feature's switch. * * post.product.gallery additional images, a list of `{url, alt}` rows, also ready for a loop * * `gallery` works exactly like `specs` (`source:'field'`) and is likewise filled to one shape by all three. * The main image is not in it — that is WordPress's featured image (`post.thumbnail.url`) and is the * same across all three engines. `gallery` means "the ones besides the main image"; merging them would make a * detail page render the main image twice. * * # Two kinds of button, both needed * * 1. `action_url` + `action_label` — the button is drawn in the editor, its styling entirely the * designer's, and the component is 100% portable. The cost: Woo's add-to-cart is an ordinary link * navigation with no AJAX. * 2. The `[aumframe_product_action]` shortcode — the adapter emits its own native HTML (Woo's AJAX add-to-cart * button, NexCart's contact button group). Full functionality, with the appearance decided by the adapter. * * Commerce sites use 2 and everything else uses 1. A Shortcode node is all it takes; no new editor capability * is required. * * @package Aumframe */ /** * The contract an adapter implements. * * An abstract class rather than an interface, deliberately: most fields do not apply to any given engine, and * an interface would force every adapter to write a pile of `return null`. */ final class Bag implements \ArrayAccess { private $post_id; private $cache = []; public function __construct(int $post_id) { $this->post_id = $post_id; } #[\ReturnTypeWillChange] public function offsetExists($key) { return true; } #[\ReturnTypeWillChange] public function offsetGet($key) { if (!array_key_exists($key, $this->cache)) { $a = Catalog::adapter_for($this->post_id); $this->cache[$key] = $a ? $a->field((string) $key, $this->post_id) : null; } return $this->cache[$key]; } #[\ReturnTypeWillChange] public function offsetSet($key, $value) { $this->cache[$key] = $value; } #[\ReturnTypeWillChange] public function offsetUnset($key) { unset($this->cache[$key]); } }