From a58a2e7ca4700d3e23341fde767060a4c2c704a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 30 Aug 2026 21:04:37 +0000
Subject: [PATCH] =Minor tweaks to MenuBlock.php

---
 inc/meta/Meta.php |  889 +++++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 556 insertions(+), 333 deletions(-)

diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 05810e6..c0454a6 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -1,399 +1,622 @@
 <?php
 namespace JVBase\meta;
 
-use Exception;
+use JVBase\managers\Cache;
+use JVBase\registrar\Registrar;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
-/**
- * Main facade for meta operations
- * Fluent API for getting/setting meta values with validation & sanitization
- */
-class Meta
-{
-	protected Item $item;
-	protected Storage $storage;
-	protected MetaValidator $validator;
-	protected MetaSanitizer $sanitizer;
-	protected MetaTypeManager $typeManager;
+class Meta {
+	/**
+	 * @var string post, term, user, or options
+	 */
+	protected string $objectType;
+	/**
+	 * @var string|null post type, user role, or taxonomy
+	 */
+	protected ?string $content;
+	/**
+	 * @var int|string <int>post, term, or user ID OR {string} option key base
+	 */
+	protected int|string $ID;
+	/**
+	 * @var array of Registrar's defined fields, if any
+	 */
+	protected static array $registrars = [];
+	/**
+	 * @var array Field values, stored in memory
+	 */
+	protected array $fields;
 
-	protected bool $autoValidate = true;
-	protected bool $autoSanitize = true;
+	protected ?\WC_Product $product = null;
 
-	// ─────────────────────────────────────────────────────────────
-	// Factory Methods
-	// ─────────────────────────────────────────────────────────────
+	protected static Validator $validator;
+	protected static Sanitizer $sanitizer;
 
-	public static function forPost(int $id): self
-	{
-		return new self($id, 'post');
-	}
+	protected static array $coreFields = [
+		'post' => ['post_title', 'post_content', 'post_excerpt', 'post_status', 'post_date', 'post_modified', 'post_thumbnail', 'post_name'],
+		'term' => ['name', 'description', 'parent'],
+		'user' => ['first_name', 'last_name', 'user_email', 'display_name', 'user_login'],
+		'options' => []
+	];
 
-	public static function forTerm(int $id): self
-	{
-		return new self($id, 'term');
-	}
+	protected static array $instances = [
+		'post'	=> [],
+		'term'	=> [],
+		'user'	=> [],
+		'options'=> [],
+	];
 
-	public static function forUser(int $id): self
-	{
-		return new self($id, 'user');
-	}
+	private function __construct(string $objectType, int|string $ID) {
+		$this->objectType = $objectType;
+		$this->ID = $ID;
+		$this->content = $this->resolveBasedType();
 
-	public static function forOptions(?string $baseKey = null): self
-	{
-		$instance = new self($baseKey, 'options');
-		$instance->item->baseKey = $baseKey;
-		return $instance;
-	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Constructor
-	// ─────────────────────────────────────────────────────────────
-
-	public function __construct(int|string|null $id, string $type)
-	{
-		$this->storage = new Storage();
-		$this->validator = new MetaValidator();
-		$this->sanitizer = new MetaSanitizer();
-		$this->typeManager = new MetaTypeManager();
-
-		$this->item = $this->buildItem($id, $type);
-	}
-
-	protected function buildItem(int|string|null $id, string $type): Item
-	{
-		$contentType = null;
-		$wpObject = null;
-
-		if ($id && $type !== 'options') {
-			[$wpObject, $contentType] = match ($type) {
-				'post' => [get_post($id), jvbNoBase(get_post_type($id))],
-				'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)],
-				'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)],
-				default => [null, null]
-			};
-		}
-
-		$item = new Item($id, $type, $contentType);
-		$item->wpObject = $wpObject;
-		$item->fieldConfigs = $this->loadFieldConfigs($contentType, $type);
-
-		// Mark WP defaults in configs
-		$defaults = Item::WP_DEFAULTS[$type] ?? [];
-		foreach ($defaults as $name) {
-			if (!isset($item->fieldConfigs[$name])) {
-				$item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true];
-			} else {
-				$item->fieldConfigs[$name]['_wp_default'] = true;
+		if ($this->content && !array_key_exists($this->content, self::$registrars)) {
+			$registrar = Registrar::getInstance($this->content);
+			if ($registrar) {
+				self::$registrars[$this->content] = $registrar->getFields();
 			}
 		}
 
-		return $item;
+		if ($this->objectType === 'post' && function_exists('wc_get_product') && in_array($this->content, ['product', 'product_variation'], true)) {
+			$this->product = wc_get_product($this->ID) ?: null;
+		}
+		$this->buildData();
+		if (!isset(self::$sanitizer)) {
+			self::$sanitizer = new Sanitizer();
+		}
+		if (!isset(self::$validator)) {
+			self::$validator = new Validator();
+		}
 	}
+		protected function buildData():void
+		{
+			//Get all Item meta
+			$this->fields = match ($this->objectType) {
+				'post'	=> get_post_meta($this->ID),
+				'term'	=> get_term_meta($this->ID),
+				'user'	=> get_user_meta($this->ID),
+				default	=> []
+			};
+			$this->fields = array_combine(
+				array_map('jvbNoBase', array_keys($this->fields)),
+				$this->fields
+			);
 
-	protected function loadFieldConfigs(?string $contentType, string $objectType): array
-	{
-		if (!$contentType && $objectType !== 'options') {
-			return [];
+			//Add in core fields
+			$obj = match ($this->objectType) {
+				'post'	=> get_post($this->ID),
+				'term'	=> get_term($this->ID, $this->content),
+				'user'	=> get_userdata($this->ID),
+				default => false
+			};
+			if ($obj) {
+				foreach (self::$coreFields[$this->objectType] as $name) {
+					$value =  match ($name) {
+						'post_thumbnail'	=> get_post_thumbnail_id($this->ID),
+						default	=> $obj->$name??false
+					};
+//					if (!$value) {
+//						$this->log('Could not find value for '.$name.', of '.$this->content);
+//					}
+					$this->fields[$name] = $value??'';
+				}
+			}
 		}
 
-		return jvbGetFields($contentType ?? 'options', $objectType);
+	public static function forPost(int $ID):self
+	{
+		if (array_key_exists($ID, self::$instances['post'])) {
+			return self::$instances['post'][$ID];
+		}
+		self::$instances['post'][$ID] = new self('post', $ID);
+		return self::$instances['post'][$ID];
 	}
 
-	// ─────────────────────────────────────────────────────────────
-	// Magic Methods for Fluent Access
-	// ─────────────────────────────────────────────────────────────
-
-	public function __get(string $name): mixed
+	public static function forTerm(int $ID):self
 	{
-		return $this->get($name);
+		if (array_key_exists($ID, self::$instances['term'])) {
+			return self::$instances['term'][$ID];
+		}
+		self::$instances['term'][$ID] = new self('term', $ID);
+		return self::$instances['term'][$ID];
 	}
 
-	public function __set(string $name, mixed $value): void
+	public static function forUser(int $ID):self
 	{
-		$this->set($name, $value);
+		if (array_key_exists($ID, self::$instances['user'])) {
+			return self::$instances['user'][$ID];
+		}
+		self::$instances['user'][$ID] = new self('user', $ID);
+		return self::$instances['user'][$ID];
 	}
 
-	public function __isset(string $name): bool
+	public static function forOptions(string $base = 'default'):self
 	{
-		return $this->item->hasField($name);
+		if (array_key_exists($base, self::$instances['options'])) {
+			return self::$instances['options'][$base];
+		}
+		self::$instances['options'][$base] = new self('options', $base);
+		return self::$instances['options'][$base];
 	}
 
-	// ─────────────────────────────────────────────────────────────
-	// Core API
-	// ─────────────────────────────────────────────────────────────
+	protected function resolveBasedType(): ?string {
+		return match ($this->objectType) {
+			'post' => get_post_type($this->ID) ?: null,
+			'term' => ($term = get_term($this->ID)) && !is_wp_error($term) ? $term->taxonomy : null,
+			'user' => jvbUserRole($this->ID),
+			default => null
+		};
+	}
 
-	/**
-	 * Get a field value
-	 */
-	public function get(string $name): mixed
-	{
-		// Return from loaded field if exists
-		if ($field = $this->item->getField($name)) {
-			return $field->get();
+
+
+
+
+	/***************************************************************************
+	 * GETTING FIELDS
+	***************************************************************************/
+	public function get(string $name): mixed {
+		if (array_key_exists($name, $this->fields)) {
+			return $this->fields[$name];
 		}
 
-		// Load from storage
-		$value = $this->storage->get($this->item, $name);
-		$config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
-
-		$field = new Field($name, $value, $config);
-		$this->item->setField($field);
-
+		if ($this->product && $this->isProductField($name)) {
+			$value = $this->getProductField($name);
+		} else if (in_array($name, static::$coreFields[$this->objectType], true)) {
+			$value = $this->getCoreField($name);
+		} else {
+			$value = $this->getMetaField($name);
+		}
+		$this->fields[$name] = $value;
 		return $value;
 	}
 
-	/**
-	 * Set a field value (validates & sanitizes)
-	 */
-	public function set(string $name, mixed $value): self
-	{
-		$config = $this->item->getFieldConfig($name);
-
-		if (!$config) {
-			// Allow setting unknown fields with minimal config
-			$config = ['type' => 'text', 'name' => $name];
+	public function getAll(?array $names = null): array {
+		$names = $names ?? $this->getAllFieldNames();
+		$out = [];
+		foreach ($names as $name) {
+			$out[$name] = $this->get($name);
 		}
-
-		$config['name'] = $name;
-
-		// Validate
-		if ($this->autoValidate && !$this->validator->validate($value, $config)) {
-			$field = $this->item->getField($name) ?? new Field($name, $value, $config);
-			$field->addError("Validation failed for {$name}");
-			$this->item->setField($field);
-			return $this;
-		}
-
-		// Sanitize
-		if ($this->autoSanitize) {
-			$value = $this->sanitizer->sanitize($value, $config);
-		}
-
-		// Get or create field
-		$field = $this->item->getField($name);
-
-		if ($field) {
-			$field->set($value);
-		} else {
-			// Need to load original to track dirty state
-			$original = $this->storage->get($this->item, $name);
-			$field = new Field($name, $original, $config);
-			$field->set($value);
-			$this->item->setField($field);
-		}
-
-		return $this;
+		return $out;
 	}
 
-	/**
-	 * Get multiple fields
-	 */
-	public function getAll(array $fields = []): array
-	{
-		if (empty($fields) || $fields === ['all']) {
-			$fields = array_keys($this->item->fieldConfigs);
+	protected function getCoreField(string $name): mixed {
+		return match ($this->objectType) {
+			'post' => $name === 'post_thumbnail'
+				? get_post_thumbnail_id($this->ID)
+				: (($p = get_post($this->ID)) ? ($p->$name ?? null) : null),
+			'term' => ($t = get_term($this->ID, $this->content)) && !is_wp_error($t) ? ($t->$name ?? null) : null,
+			'user' => ($u = get_userdata($this->ID)) ? ($u->$name ?? null) : null,
+		};
+	}
+
+	protected function getMetaField(string $name): mixed {
+		$key = $this->metaKey($name);
+		return match ($this->objectType) {
+			'post' => get_post_meta($this->ID, $key, true),
+			'term' => get_term_meta($this->ID, $key, true),
+			'user' => get_user_meta($this->ID, $key, true),
+			'options' => get_option($key)
+		};
+	}
+
+	/***************************************************************************
+	 * SETTING FIELDS
+	***************************************************************************/
+	public function set(string $name, mixed $value): bool {
+		$field = $this->getFieldConfig($name);
+		if ($field) {
+			if ($field['type']??'' === 'repeater' && str_contains($name, ':')) {
+				return $this->setByPath($name, $value);
+			} elseif ($field['type']??'' === 'group' && str_contains($name, '|')) {
+				return $this->setGroup($name, $value);
+			}
+
+			if (!self::$validator->validate($value, $field)) {
+				return false;
+			}
+
+			$value = self::$sanitizer->sanitize($value, $field);
 		}
 
-		// Load all from storage
-		$values = $this->storage->getAll($this->item, $fields);
+		if ($this->product && $this->isProductField($name)) {
+			return $this->setProductField($name, $value);
+		}
+		if (in_array($name, static::$coreFields[$this->objectType], true)) {
+			return $this->setCoreField($name, $value);
+		}
+		return $this->setMetaField($name, $value);
+	}
 
-		// Create Field instances
-		foreach ($values as $name => $value) {
-			if (!$this->item->getField($name)) {
-				$config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
-				$this->item->setField(new Field($name, $value, $config));
+	public function setAll(array $data): array {
+		$core = array_filter($data, function ($name) {
+			return in_array($name, self::$coreFields[$this->objectType]);
+		}, ARRAY_FILTER_USE_KEY);
+		$results = [];
+		if (!empty($core)) {
+			$data = array_filter($data, function ($name) {
+				return !in_array($name, self::$coreFields[$this->objectType]);
+			}, ARRAY_FILTER_USE_KEY);
+			$results = $this->setAllCoreFields($core);
+		}
+
+		foreach ($data as $name => $value) {
+			$results[$name] = $this->set($name, $value);
+		}
+		return $results;
+	}
+
+	protected function setAllCoreFields(array $data):array
+	{
+		$results = [];
+
+		$core = array_filter($data, function ($name) {
+			return in_array($name, self::$coreFields[$this->objectType]);
+		}, ARRAY_FILTER_USE_KEY);
+
+
+		if (count($core) !== count($data)) {
+			$notCore = array_filter($data, function ($name) {
+				return !in_array($name, self::$coreFields[$this->objectType]);
+			}, ARRAY_FILTER_USE_KEY);
+			$results = $this->setAll($notCore);
+		}
+
+		if (array_key_exists('post_thumbnail', $core)) {
+			$results['post_thumbnail'] = $this->setCoreField('post_thumbnail', $core['post_thumbnail']);
+			unset ($core['post_thumbnail']);
+		}
+
+		$return = match ($this->objectType) {
+			'post'	=> !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $core))),
+			'term'	=> !is_wp_error(wp_update_term($this->ID, $this->content, $core)),
+			'user'	=> !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $core))),
+		};
+		foreach ($core as $name => $value) {
+			$results[$name] = $return;
+		}
+		return $results;
+	}
+
+	protected function setCoreField(string $name, mixed $value): bool {
+		$success = match ($this->objectType) {
+			'post' => $name === 'post_thumbnail'
+				? (!empty($value) ? set_post_thumbnail($this->ID, (int)$value) : delete_post_thumbnail($this->ID))
+				: !is_wp_error(wp_update_post(['ID' => $this->ID, $name => $value])),
+			'term' => !is_wp_error(wp_update_term($this->ID, $this->content, [$name => $value])),
+			'user' => !is_wp_error(wp_update_user(['ID' => $this->ID, $name => $value])),
+			default => false
+		};
+		if ($success) {
+			$this->clearCache();
+			$this->fields[$name] = $value;
+		}
+		return $success;
+	}
+
+	protected function setMetaField(string $name, mixed $value): bool {
+		$key = $this->metaKey($name);
+		$result = match ($this->objectType) {
+			'post' => update_post_meta($this->ID, $key, $value),
+			'term' => update_term_meta($this->ID, $key, $value),
+			'user' => update_user_meta($this->ID, $key, $value),
+			'options' => update_option($key, $value)
+		};
+		if ($result) {
+			$this->fields[$name] = $value;
+			$this->clearCache();
+		}
+		return $result !== false;
+	}
+	/********************************************************************************
+	 * DELETING FIELDS
+	********************************************************************************/
+	public function delete(string $name):bool
+	{
+		if (in_array($name, static::$coreFields[$this->objectType])) {
+			return $this->deleteCoreField($name);
+		}
+
+		$field = $this->getFieldConfig($name);
+		if ($field) {
+			if ($this->isTaxonomy($field)) {
+				return !is_wp_error(wp_set_object_terms($this->ID, [], $this->content));
 			}
 		}
-
-		return $values;
+		$key = $this->metaKey($name);
+		return match ($this->objectType) {
+			'post'	=> delete_post_meta($this->ID, $key),
+			'term'	=> delete_term_meta($this->ID, $key),
+			'user'	=> delete_user_meta($this->ID, $key),
+			'option'=> delete_option($key)
+		};
 	}
-
-	/**
-	 * Set multiple fields
-	 */
-	public function setAll(array $data): self
+	public function deleteAll(array $fieldNames):array
 	{
-		foreach ($data as $name => $value) {
-			$this->set($name, $value);
+		$core = array_filter($fieldNames, function ($name) {
+			return in_array($name, self::$coreFields[$this->objectType]);
+		}, ARRAY_FILTER_USE_KEY);
+
+		$results = [];
+		if (!empty($core)) {
+			$fieldNames = array_filter($fieldNames, function ($name) {
+				return !in_array($name, self::$coreFields[$this->objectType]);
+			});
+			$results = $this->deleteAllCoreFields($core);
 		}
-		return $this;
+
+		foreach ($fieldNames as $name) {
+			$results[$name] = $this->delete($name);
+		}
+
+		return $results;
+	}
+	protected function deleteAllCoreFields(array $fieldNames):array
+	{
+		$results = [];
+		$core = array_filter($fieldNames, function ($name) {
+			return in_array($name, self::$coreFields[$this->objectType]);
+		}, ARRAY_FILTER_USE_KEY);
+
+
+		if (count($core) !== count($fieldNames)) {
+			$notCore = array_filter($fieldNames, function ($name) {
+				return !in_array($name, self::$coreFields[$this->objectType]);
+			}, ARRAY_FILTER_USE_KEY);
+			$results = $this->deleteAll($notCore);
+		}
+
+		if (array_key_exists('post_thumbnail', $fieldNames)) {
+			$results['post_thumbnail'] = delete_post_thumbnail($this->ID);
+			unset($fieldNames['post_thumbnail']);
+			unset($core['post_thumbnail']);
+		}
+
+		$updated = [];
+		foreach ($core as $name) {
+			$updated[$name] = $this->getCoreDefault($name);
+		}
+
+		$success = match ($this->objectType) {
+			'post'	=> !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $updated))),
+			'term'	=> !is_wp_error(wp_update_term($this->ID, $this->content, $updated)),
+			'user'	=> !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $updated)))
+		};
+
+		foreach ($core as $name) {
+			$results[$name] = $success;
+		}
+		return $results;
 	}
 
-	/**
-	 * Save all dirty fields to database
-	 */
-	public function save(bool $updateTimestamp = true): bool
+	protected function deleteCoreField(string $name):bool
 	{
-		if (!$this->item->isValid()) {
-			JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
-				'fields' => array_keys($this->item->getInvalidFields())
-			], 'warning');
+		if ($name === 'post_thumbnail') {
+			return delete_post_thumbnail($this->ID);
+		}
+		$default = $this->getCoreDefault($name);
+		return $this->setCoreField($name, $default);
+	}
+
+
+	/**************************************************************
+	 * WOOCOMMERCE PRODUCT SPECIFIC
+	**************************************************************/
+	protected function isProductField(string $name): bool {
+		return method_exists($this->product, 'get_'.$name) || method_exists($this->product, 'set_'.$name);
+	}
+
+	protected function getProductField(string $name): mixed {
+		$method = 'get_'.$name;
+		if ($name === 'variations') {
+			return $this->getVariations();
+		}
+		return method_exists($this->product, $method) ? $this->product->$method() : null;
+	}
+		protected function getVariations():array
+		{
+			$variations = $this->product->get_children();
+			$out = [];
+			foreach ($variations as $v) {
+				$out[] = self::forPost($v)->getAll();
+			}
+			return $out;
+		}
+
+	protected function setProductField(string $name, mixed $value): bool {
+		$method = 'set_'.$name;
+		if (!method_exists($this->product, $method)) return false;
+		$this->product->$method($value);
+		$this->product->save();
+		$this->fields[$name] = $value;
+		$this->clearCache();
+		return true;
+	}
+
+	protected function getAllProductFieldNames(): array
+	{
+		return [
+			'price',
+			'sku',
+			'sale',
+			'short_description',
+			'image_id',
+			'gallery_image_ids',
+			'stock',
+			'stock_quantity',
+			'stock_status',
+			'variations'
+		];
+	}
+
+	/*** Sanitization & Validation ***/
+//	protected function sanitize(mixed $value, array $field): mixed {
+//		return match ($field['type']) {
+//			'text', 'tel' => sanitize_text_field($value),
+//			'email' => sanitize_email($value),
+//			'url' => esc_url_raw($value),
+//			'textarea' => !empty($field['quill']) ? wp_kses_post($value) : sanitize_textarea_field($value),
+//			'number' => is_numeric($value) ? $value + 0 : 0,
+//			'true_false' => (bool)$value,
+//			'datetime' => sanitize_text_field($value),
+//			'upload', 'image', 'gallery' => !empty($field['multiple']) ? array_map('intval', (array)$value) : (int)$value,
+//			'checkbox', 'set' => array_map('sanitize_text_field', (array)$value),
+//			'radio', 'select' => sanitize_text_field($value),
+//			'selector', 'taxonomy', 'user', 'post' => array_map('intval', (array)$value),
+//			'repeater', 'tagList' => $this->sanitizeRepeater($value, $field),
+//			'group' => $this->sanitizeGroup($value, $field),
+//			default => is_array($value) ? array_map('sanitize_text_field', $value) : sanitize_text_field($value),
+//		};
+//	}
+
+//	protected function validate(string $name, mixed $value, array $field): bool|string {
+//		if (!empty($field['required']) && ($value === '' || $value === null)) {
+//			return $name.' is required';
+//		}
+//		if (isset($field['maxLength']) && is_string($value) && strlen($value) > $field['maxLength']) {
+//			return $name.' exceeds max length of '.$field['maxLength'];
+//		}
+//		if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
+//			return $name.' is below minimum of '.$field['min'];
+//		}
+//		if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
+//			return $name.' exceeds maximum of '.$field['max'];
+//		}
+//		if (in_array($field['type'], ['checkbox', 'set', 'radio', 'select'], true) && !empty($field['options'])) {
+//			foreach ((array)$value as $v) {
+//				if (!array_key_exists($v, $field['options'])) {
+//					return $name.': invalid option '.$v;
+//				}
+//			}
+//		}
+//		return true;
+//	}
+
+	/************************************************************************
+	 * REPEATER Helpers
+	************************************************************************/
+	public function repeater(string $name):Repeater
+	{
+		return new Repeater($this, $name);
+	}
+	protected function setByPath(string $path, mixed $value):bool
+	{
+		$parts = explode(':', $path, 3);
+		if (count($parts) !== 3) {
+			$this->log('Could not set value by path');
 			return false;
 		}
+		[$repeaterName, $rowIndex, $subField] = $parts;
+		$this->clearCache();
+		return $this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
+	}
 
-		// Check for field overrides before saving
-		foreach ($this->item->getDirtyFields() as $field) {
-			if ($this->checkOverrides($field)) {
-				$field->markClean();
-			}
+	protected function getByPath(string $path):mixed
+	{
+		$parts = explode(':', $path, 3);
+		if (count($parts) !== 3) {
+			return null;
 		}
 
-		return $this->storage->save($this->item, $updateTimestamp);
+		[$repeaterName, $rowIndex, $subField] = $parts;
+		return $this->repeater($repeaterName)->field((int) $rowIndex, $subField);
+	}
+	/****************************************************************************************
+	 * GROUP helpers
+	****************************************************************************************/
+
+	public function group(string $name): Group
+	{
+		return new Group($this, $name);
 	}
 
-	/**
-	 * Delete a field value
-	 */
-	public function delete(string $name): bool
+	protected function setGroup(string $name, mixed $value):bool
 	{
-		$result = $this->storage->delete($this->item, $name);
-
-		if ($result && $field = $this->item->getField($name)) {
-			$field->set($this->getDefaultValue($name));
-			$field->markClean();
-		}
-
-		return $result;
+		[$parent, $child] = explode('|', $name, 2);
+		$this->clearCache();
+		return $this->group($parent)->set($child, $value);
 	}
 
-	// ─────────────────────────────────────────────────────────────
-	// Utility Methods
-	// ─────────────────────────────────────────────────────────────
-
-	/**
-	 * Get all dirty (changed) fields
-	 */
-	public function getDirty(): array
+	/***************************************************************************************
+	 * UTILITY
+	***************************************************************************************/
+	protected function clearCache():void
 	{
-		return array_map(
-			fn(Field $f) => $f->value,
-			$this->item->getDirtyFields()
-		);
-	}
-
-	/**
-	 * Check if any fields have changed
-	 */
-	public function isDirty(): bool
-	{
-		return $this->item->hasDirtyFields();
-	}
-
-	/**
-	 * Discard all unsaved changes
-	 */
-	public function reset(): self
-	{
-		foreach ($this->item->fields as $field) {
-			$field->reset();
-		}
-		return $this;
-	}
-
-	/**
-	 * Get validation errors
-	 */
-	public function getErrors(): array
-	{
-		$errors = [];
-		foreach ($this->item->getInvalidFields() as $name => $field) {
-			$errors[$name] = $field->errors;
-		}
-		return $errors;
-	}
-
-	/**
-	 * Check if valid (no validation errors)
-	 */
-	public function isValid(): bool
-	{
-		return $this->item->isValid();
-	}
-
-	/**
-	 * Disable auto-validation for bulk operations
-	 */
-	public function withoutValidation(): self
-	{
-		$this->autoValidate = false;
-		return $this;
-	}
-
-	/**
-	 * Disable auto-sanitization
-	 */
-	public function withoutSanitization(): self
-	{
-		$this->autoSanitize = false;
-		return $this;
-	}
-
-	/**
-	 * Get the underlying item (for rendering, etc)
-	 */
-	public function item(): Item
-	{
-		return $this->item;
-	}
-
-	/**
-	 * Get field configuration
-	 */
-	public function config(string $name): ?array
-	{
-		return $this->item->getFieldConfig($name);
-	}
-
-	/**
-	 * Get all field configurations
-	 */
-	public function configs(): array
-	{
-		return $this->item->fieldConfigs;
-	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Protected Helpers
-	// ─────────────────────────────────────────────────────────────
-
-	protected function checkOverrides(Field $field): bool
-	{
-		$name = $field->name;
-		$type = $field->type();
-		$value = $field->value;
-
-		do_action('jvb_meta_update', $name, $value, $this->item->objectType);
-
-		$overrides = [
-			BASE . 'update_' . $name,
-			BASE . 'update_' . $type,
-			'jvb_update_' . $name,
-			'jvb_update_' . $type,
-		];
-
-		foreach ($overrides as $override) {
-			if (function_exists($override)) {
-				$override($this->item->id, $value);
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	protected function getDefaultValue(string $name): mixed
-	{
-		$config = $this->item->getFieldConfig($name);
-		$type = $config['type'] ?? 'text';
-
-		return match ($this->typeManager->getMetaType($type)) {
-			'object', 'array' => [],
-			'boolean' => false,
-			'integer' => 0,
-			default => '',
+		match ($this->objectType) {
+			'post'	=> Cache::clearPost($this->ID),
+			'term'	=> Cache::clearTerm($this->ID),
+			'user'	=> Cache::clearUser($this->ID),
 		};
 	}
+	protected function isTaxonomy(array $field):bool
+	{
+		$type = $field['type']??'';
+		return $type === 'taxonomy' || ($type === 'selector' &&$field['subtype']??'' === 'taxonomy');
+	}
+
+	protected function getAllFieldNames(): array {
+		$names = static::$coreFields[$this->objectType];
+		if ($this->hasRegistrar()) {
+			$names = array_merge($names, array_keys(self::$registrars[$this->content]));
+		}
+		if ($this->product) {
+			$names = array_merge($names, $this->getAllProductFieldNames());
+		}
+		return array_unique($names);
+	}
+
+	protected function hasRegistrar():bool
+	{
+		return array_key_exists($this->content, self::$registrars);
+	}
+
+	public function getFieldConfig(string $name): ?array {
+		if (!$this->hasRegistrar()) return null;
+		return self::$registrars[$this->content][$name] ?? null;
+	}
+
+	protected function getCoreDefault(string $name):string
+	{
+		return match($name) {
+			'post_status'				=> 'draft',
+			'post_date','post_modified'	=> date('mysql'),
+			'post_name'	=> sanitize_title($this->get('post_title')),
+			default => ''
+		};
+	}
+
+	protected function metaKey(string $name): string {
+		if ($this->product) {
+			if (in_array($name, $this->getAllProductFieldNames())) {
+				return $name;
+			}
+		}
+		return jvbCheckBase($name);
+	}
+
+	/**
+	 * Get item ID
+	 */
+	public function id(): int|string|null
+	{
+		return $this->ID;
+	}
+
+	public function config(string $name):?array
+	{
+		return $this->getFieldConfig($name);
+	}
+
+	protected function log(string $message, array $context = []):void
+	{
+		$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? '';
+		error_log('[Meta] '.$caller.' '.$message);
+		if (!empty($context)) {
+			error_log('    CONTEXT: '.print_r($context, true));
+		}
+	}
 }

--
Gitblit v1.10.0