> post id => WP_Post fields */ public array $posts = []; /** @var array option name => value */ public array $options = []; /** @var array extension ability name => what it staged */ public array $extensions = []; /** @var array{settings:array,styles:array}|null */ public ?array $globalStyles = null; /** @var array>,removed:array,updated:array>}> menu id => edits */ public array $menus = []; /** @var array> product id => meta fields */ public array $products = []; /** @var array template part slug => block markup, for parts the site has not customised */ public array $templateParts = []; /** @var array> post id => meta key => staged value (a page builder's data) */ public array $meta = []; private int $nextItemId = -1; public function empty(): bool { return $this->posts === [] && $this->options === [] && $this->extensions === [] && $this->globalStyles === null && $this->menus === [] && $this->products === [] && $this->templateParts === [] && $this->meta === []; } /** A post with the staged fields applied, or null when it does not exist. */ public function post(int $id): ?object { $post = get_post($id); if ($post === null) { return null; } return $this->applyToPost($post); } public function applyToPost(object $post): object { $fields = $this->posts[(int) $post->ID] ?? null; if ($fields === null) { return $post; } $copy = clone $post; foreach ($fields as $key => $value) { $copy->$key = $value; } return $copy; } public function setMeta(int $id, string $key, mixed $value): void { $this->meta[$id] ??= []; $this->meta[$id][$key] = $value; } /** @param array $fields WP_Post field names */ public function setPost(int $id, array $fields): void { $this->posts[$id] ??= []; foreach ($fields as $key => $value) { $this->posts[$id][$key] = $value; } } public function option(string $name, mixed $default = false): mixed { return array_key_exists($name, $this->options) ? $this->options[$name] : get_option($name, $default); } /** @return array */ public function menuItems(int $menuId): array { $items = (array) wp_get_nav_menu_items($menuId); return $this->applyToMenu($menuId, array_values(array_filter($items, 'is_object'))); } /** * @param array $items * @return array */ public function applyToMenu(int $menuId, array $items): array { $edits = $this->menus[$menuId] ?? null; if ($edits === null) { return $items; } $out = []; foreach ($items as $item) { $id = (int) $item->ID; if (in_array($id, $edits['removed'], true)) { continue; } if (isset($edits['updated'][$id])) { $item = clone $item; foreach ($edits['updated'][$id] as $key => $value) { $item->$key = $value; } } $out[] = $item; } foreach ($edits['added'] as $added) { $out[] = (object) $added; } usort($out, static fn (object $a, object $b): int => ((int) ($a->menu_order ?? 0)) <=> ((int) ($b->menu_order ?? 0))); return $out; } /** @return int the id the preview knows the new item by (negative, so it can never collide) */ public function addMenuItem(int $menuId, array $fields): int { $this->menu($menuId); $id = $this->nextItemId--; $this->menus[$menuId]['added'][] = self::menuItemObject($id, $menuId, $fields); return $id; } /** @param array $fields WP nav menu item object fields */ public function updateMenuItem(int $menuId, int $itemId, array $fields): void { $this->menu($menuId); if ($itemId < 0) { foreach ($this->menus[$menuId]['added'] as $index => $added) { if ((int) $added['ID'] === $itemId) { $this->menus[$menuId]['added'][$index] = array_merge($added, $fields); } } return; } $this->menus[$menuId]['updated'][$itemId] = array_merge($this->menus[$menuId]['updated'][$itemId] ?? [], $fields); } public function removeMenuItem(int $menuId, int $itemId): void { $this->menu($menuId); if ($itemId < 0) { $this->menus[$menuId]['added'] = array_values(array_filter($this->menus[$menuId]['added'], static fn (array $added): bool => (int) $added['ID'] !== $itemId)); return; } $this->menus[$menuId]['removed'][] = $itemId; unset($this->menus[$menuId]['updated'][$itemId]); } /** The staged product fields over the ones given (the ability's snapshot shape). */ public function applyToProduct(int $id, array $snapshot): array { return array_merge($snapshot, $this->products[$id] ?? []); } /** @param array $fields the ability's snapshot shape */ public function setProduct(int $id, array $fields): void { $this->products[$id] = array_merge($this->products[$id] ?? [], $fields); } /** What a staged product's meta reads as on the front end, or null for a key the preview does not touch. */ public function productMeta(int $id, string $key): ?string { $fields = $this->products[$id] ?? null; if ($fields === null) { return null; } switch ($key) { case '_regular_price': return isset($fields['regularPrice']) ? (string) $fields['regularPrice'] : null; case '_sale_price': return isset($fields['salePrice']) ? (string) $fields['salePrice'] : null; case '_price': if (!isset($fields['regularPrice']) && !isset($fields['salePrice']) && !isset($fields['saleFrom']) && !isset($fields['saleTo'])) { return null; } $regular = (string) ($fields['regularPrice'] ?? get_post_meta($id, '_regular_price', true)); $sale = (string) ($fields['salePrice'] ?? get_post_meta($id, '_sale_price', true)); $from = (string) ($fields['saleFrom'] ?? ''); $to = (string) ($fields['saleTo'] ?? ''); $today = gmdate('Y-m-d'); $onSale = $sale !== '' && ($from === '' || $from <= $today) && ($to === '' || $to >= $today); return $onSale ? $sale : $regular; case '_stock_status': return isset($fields['stockStatus']) ? (string) $fields['stockStatus'] : null; case '_stock': return array_key_exists('stockQuantity', $fields) && $fields['stockQuantity'] !== null ? (string) $fields['stockQuantity'] : null; case '_manage_stock': return array_key_exists('stockQuantity', $fields) && $fields['stockQuantity'] !== null ? 'yes' : null; default: return null; } } private function menu(int $menuId): void { $this->menus[$menuId] ??= ['added' => [], 'removed' => [], 'updated' => []]; } /** @param array $fields */ public static function menuItemObject(int $id, int $menuId, array $fields): array { $objectId = (int) ($fields['objectId'] ?? 0); $url = $objectId > 0 ? (string) get_permalink($objectId) : (string) ($fields['url'] ?? ''); return [ 'ID' => $id, 'attr_title' => '', 'classes' => [], 'db_id' => $id, 'description' => '', 'menu_item_parent' => (int) ($fields['parent'] ?? 0), 'menu_order' => (int) ($fields['position'] ?? 0) > 0 ? (int) $fields['position'] : 9999, 'object' => $objectId > 0 ? (string) ($fields['object'] ?? (get_post_type($objectId) ?: 'page')) : 'custom', 'object_id' => $objectId, 'post_parent' => 0, 'post_title' => (string) ($fields['title'] ?? ''), 'post_type' => 'nav_menu_item', 'target' => '', 'title' => (string) ($fields['title'] ?? ''), 'type' => $objectId > 0 ? 'post_type' : 'custom', 'type_label' => $objectId > 0 ? __('Page', 'sarcio') : __('Custom Link', 'sarcio'), 'url' => $url, 'xfn' => '', 'menu_id' => $menuId, ]; } }