From 275c0d74cd68677622a5431505c5c870c473063d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 29 Mar 2026 21:40:15 +0000
Subject: [PATCH] =Seems to be working, huzzah! Added some changes for on-this-page nav

---
 inc/meta/Meta.php |  640 +++++++++++++++++++++------------------------------------
 1 files changed, 236 insertions(+), 404 deletions(-)

diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 8984345..371a059 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -2,49 +2,48 @@
 namespace JVBase\meta;
 
 use JVBase\registrar\Registrar;
+use WP_Post;
+use WP_Term;
+use WP_User;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
-/**
- * Main facade for meta operations
- * Fluent API for getting/setting meta values with validation & sanitization
- *
- * Usage:
- *   $meta = Meta::forPost($id);
- *   $meta->price = 150;
- *   $meta->save();
- *
- *   Meta::forPost($id)->set('price', 150)->set('style', 'traditional')->save();
- */
 class Meta
 {
+	/**
+	 * @var string post, term, user, or options
+	 */
+	protected string $type;
+	/**
+	 * @var string the full slug, with BASE
+	 */
+	protected string $slug;
+
+	protected string $contentType;
 	protected Item $item;
 	protected Storage $storage;
 	protected Validator $validator;
 	protected Sanitizer $sanitizer;
+	protected array $fields;
+	protected WP_Post|WP_Term|WP_User|null $wpObject;
+	protected int|string $ID;
 	protected MetaTypeManager $typeManager;
+	protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]];
 
-	protected bool $autoValidate = true;
-	protected bool $autoSanitize = true;
-
-	/** @var array<string, callable[]> */
-	protected array $onChangeCallbacks = [];
-
-	/** @var array<string, callable> */
-	protected array $computed = [];
-
-	// ─────────────────────────────────────────────────────────────
-	// Factory Methods
-	// ─────────────────────────────────────────────────────────────
-
+	protected array $defaults = ['post_thumbnail'];
 	/**
 	 * Create Meta instance for a post
 	 */
 	public static function forPost(int $id): self
 	{
-		return new self($id, 'post');
+		if (array_key_exists($id, self::$instances['post'])) {
+			return self::$instances['post'][$id];
+		}
+		$new = new self($id, 'post');
+		self::$instances['post'][$id] = $new;
+		return $new;
 	}
 
 	/**
@@ -52,7 +51,12 @@
 	 */
 	public static function forTerm(int $id): self
 	{
-		return new self($id, 'term');
+		if (array_key_exists($id, self::$instances['term'])) {
+			return self::$instances['term'][$id];
+		}
+		$new = new self($id, 'term');
+		self::$instances['term'][$id] = $new;
+		return $new;
 	}
 
 	/**
@@ -60,194 +64,99 @@
 	 */
 	public static function forUser(int $id): self
 	{
-		return new self($id, 'user');
+		if (array_key_exists($id, self::$instances['user'])) {
+			return self::$instances['user'][$id];
+		}
+		$new = new self($id, 'user');
+		self::$instances['user'][$id] = $new;
+		return $new;
 	}
 
 	/**
 	 * Create Meta instance for options
 	 */
-	public static function forOptions(?string $baseKey = null): self
+	public static function forOptions(?string $baseKey = 'ajv'): self
 	{
-		$instance = new self($baseKey, 'options');
-		$instance->item->baseKey = $baseKey;
-		return $instance;
+		if (array_key_exists($baseKey, self::$instances['options'])) {
+			return self::$instances['options'][$baseKey];
+		}
+		$new = new self($baseKey, 'options');
+		self::$instances['options'][$baseKey] = $new;
+		return $new;
 	}
-
-	/**
-	 * Bulk load multiple posts with optional field preloading
-	 * @return array<int, Meta>
-	 */
-	public static function bulkForPosts(array $ids, array $preloadFields = []): array
-	{
-		return self::bulkFor($ids, 'post', $preloadFields);
-	}
-
-	/**
-	 * Bulk load multiple terms with optional field preloading
-	 * @return array<int, Meta>
-	 */
-	public static function bulkForTerms(array $ids, array $preloadFields = []): array
-	{
-		return self::bulkFor($ids, 'term', $preloadFields);
-	}
-
-	/**
-	 * Bulk load multiple users with optional field preloading
-	 * @return array<int, Meta>
-	 */
-	public static function bulkForUsers(array $ids, array $preloadFields = []): array
-	{
-		return self::bulkFor($ids, 'user', $preloadFields);
-	}
-
-	/**
-	 * Generic bulk loader
-	 * @return array<int, Meta>
-	 */
-	protected static function bulkFor(array $ids, string $type, array $preloadFields = []): array
-	{
-		if (empty($ids)) {
-			return [];
-		}
-
-		$metas = [];
-
-		// Create instances
-		foreach ($ids as $id) {
-			$metas[$id] = new self($id, $type);
-		}
-
-		// Preload fields if specified
-		if (!empty($preloadFields)) {
-			self::bulkPreload($metas, $type, $preloadFields);
-		}
-
-		return $metas;
-	}
-
-	/**
-	 * Bulk preload fields for multiple Meta instances
-	 * @param Meta[] $metas
-	 */
-	protected static function bulkPreload(array $metas, string $objectType, array $fields): void
-	{
-		if (empty($metas) || empty($fields)) {
-			return;
-		}
-
-		$ids = array_keys($metas);
-		$values = Storage::getBulkValues($ids, $objectType, $fields);
-
-		// Distribute results to Meta instances
-		foreach ($values as $id => $fieldValues) {
-			if (!isset($metas[$id])) {
-				continue;
-			}
-
-			$meta = $metas[$id];
-			foreach ($fieldValues as $name => $value) {
-				$config = $meta->config($name) ?? ['type' => 'text'];
-				$field = new Field($name, $value, $config);
-				$meta->item()->setField($field);
-			}
-		}
-	}
-
-	/**
-	 * Save multiple Meta instances efficiently
-	 * @param Meta[] $metas
-	 * @return array<int, bool>
-	 */
-	public static function saveBulk(array $metas, bool $updateTimestamp = true): array
-	{
-		// Validate all first
-		$invalid = [];
-		foreach ($metas as $id => $meta) {
-			if (!$meta->isValid()) {
-				$invalid[$id] = $meta->getErrors();
-			}
-		}
-
-		if (!empty($invalid)) {
-			JVB()->error()->log('meta', 'Bulk save has validation errors', [
-				'invalid_items' => $invalid
-			], 'warning');
-		}
-
-		// Filter to only valid metas
-		$validMetas = array_filter($metas, fn($m) => $m->isValid());
-
-		// Check overrides before bulk save
-		foreach ($validMetas as $meta) {
-			foreach ($meta->item()->getDirtyFields() as $field) {
-				if ($meta->checkOverrides($field)) {
-					$field->markClean();
-				}
-			}
-		}
-
-		$results = Storage::saveBulk($validMetas, $updateTimestamp);
-
-		// Mark invalid ones as failed
-		foreach ($invalid as $id => $errors) {
-			$results[$id] = false;
-		}
-
-		return $results;
-	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Constructor
-	// ─────────────────────────────────────────────────────────────
+	/***************************************************************
+	 * Constructor
+	***************************************************************/
 
 	public function __construct(int|string|null $id, string $type)
 	{
-		$this->storage = new Storage();
 		$this->validator = new Validator();
 		$this->sanitizer = new Sanitizer();
 		$this->typeManager = new MetaTypeManager();
-
-		$this->item = $this->buildItem($id, $type);
+		$this->type = $type;
+		$this->ID = $id;
+		$this->buildData($id, $type);
 	}
 
-	protected function buildItem(int|string|null $id, string $type): Item
+	protected function buildData(int|string|null $id, string $type):void
 	{
-		$contentType = null;
-		$wpObject = null;
+		$this->wpObject = match($type) {
+			'post' 	=> get_post($id),
+			'term'	=> get_term($id),
+			'user', 'integrations' => get_userdata($id),
+			default => null
+		};
+		$this->slug = match($type) {
+			'post'	=> $this->wpObject->post_type,
+			'term'	=> $this->wpObject->taxonomy,
+			'user'	=> jvbUserRole($id),
+			default => 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];
+		$registrar = Registrar::getInstance($this->slug);
+		$fields = $registrar ? $registrar->getFields() : [];
+		$meta = match($type) {
+			'post'	=> get_post_meta($id),
+			'term'	=> get_term_meta($id),
+			'user'	=> get_user_meta($id),
+			default => []
+		};
+		$meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta);
+
+		foreach ($fields as $fieldName => $config) {
+			$fieldName = jvbNoBase($fieldName);
+			if ($this->wpObject && property_exists($this->wpObject, $fieldName)) {
+				$config['wp'] = true;
+				$value = $this->wpObject->$fieldName;
+			} else if (in_array($fieldName, $this->defaults)) {
+				$config['wp'] = true;
+				switch ($fieldName) {
+					case 'post_thumbnail':
+						$value = get_post_thumbnail_id($this->ID);
+						break;
+				}
 			} else {
-				$item->fieldConfigs[$name]['_wp_default'] = true;
+				$value = array_key_exists(BASE.$fieldName, $meta) ? $meta[BASE.$fieldName] : $config['default']??'';
+				switch ($config['type']) {
+					case 'taxonomy':
+						if (!$config['isReference']??true) {
+							$config['wp'] = true;
+							$value = implode(',', wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
+						}
+						break;
+					case 'selector':
+						if (array_key_exists('subtype', $config) && $config['subtype'] === 'taxonomy' && !$config['isReference']??true) {
+							$config['wp'] = true;
+							$value = implode(',',wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
+						}
+						break;
+				}
+
 			}
+			$this->fields[$fieldName] = new Field($fieldName, $value, $config);
 		}
-
-		return $item;
-	}
-
-	protected function loadFieldConfigs(?string $contentType, string $objectType): array
-	{
-		if (!$contentType && $objectType !== 'options') {
-			return [];
-		}
-
-		return Registrar::getFieldsFor($contentType??'options');
 	}
 
 	// ─────────────────────────────────────────────────────────────
@@ -266,7 +175,7 @@
 
 	public function __isset(string $name): bool
 	{
-		return $this->item->hasField($name) || isset($this->computed[$name]);
+		return $this->item->hasField($name);
 	}
 
 	// ─────────────────────────────────────────────────────────────
@@ -283,77 +192,36 @@
 			return $this->getByPath($name);
 		}
 
-		// Check computed fields first
-		if (isset($this->computed[$name])) {
-			return ($this->computed[$name])($this);
-		}
-
-		// Return from loaded field if exists
-		if ($field = $this->item->getField($name)) {
-			return $field->get();
-		}
-
-		// 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);
-
-		return $value;
+		return $this->fields[$name]->get();
 	}
 
 	/**
 	 * Set a field value (validates & sanitizes by default)
 	 */
-	public function set(string $name, mixed $value): self
+	public function set(string $name, mixed $value, $autosave = true): self
 	{
 		// Handle repeater subfield path (e.g., "services:2:image")
 		if (str_contains($name, ':')) {
 			return $this->setByPath($name, $value);
 		}
 
-		$config = $this->item->getFieldConfig($name);
-
-		if (!$config) {
-			// Allow setting unknown fields with minimal config
-			$config = ['type' => 'text', 'name' => $name];
+		$field = $this->fields[$name]??false;
+		if (!$field) {
+			error_log('No config found for field: '.$name);
+			return $this;
 		}
 
 		// Validate
-		if ($this->autoValidate && !$this->validator->validate($value, $config)) {
-			$field = $this->item->getField($name) ?? new Field($name, $value, $config);
+		if (!$this->validator->validate($value, $field->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);
-		$oldValue = null;
-
-		if ($field) {
-			$oldValue = $field->value;
-			$field->set($value);
-		} else {
-			// Load original to track dirty state
-			$original = $this->storage->get($this->item, $name);
-			$oldValue = $original;
-			$field = new Field($name, $original, $config);
-			$field->set($value);
-			$this->item->setField($field);
-		}
-
-		// Fire change callbacks
-		if (isset($this->onChangeCallbacks[$name]) && $oldValue !== $value) {
-			foreach ($this->onChangeCallbacks[$name] as $callback) {
-				$callback($value, $oldValue, $this);
-			}
+		$value = $this->sanitizer->sanitize($value, $field->config);
+		$field->set($value);
+		if ($autosave && $field->isDirty) {
+			$this->save();
 		}
 
 		return $this;
@@ -365,54 +233,123 @@
 	public function getAll(array $fields = []): array
 	{
 		if (empty($fields) || $fields === ['all']) {
-			$fields = array_keys($this->item->fieldConfigs);
+			$fields = array_keys($this->fields);
 		}
+		$fields = array_filter($this->fields, function ($field) use ($fields) {
+			return in_array($field, $fields);
+		}, ARRAY_FILTER_USE_KEY);
 
-		// Load all from storage
-		$values = $this->storage->getAll($this->item, $fields);
-
-		// 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));
-			}
-		}
-
-		return $values;
+		return array_map(function ($field) {
+			return $field->value;
+		}, $fields);
 	}
 
 	/**
 	 * Set multiple fields
 	 */
-	public function setAll(array $data): self
+	public function setAll(array $data):bool
 	{
 		foreach ($data as $name => $value) {
-			$this->set($name, $value);
+			error_log('Setting '.$name.' with value: '.print_r($value, true));
+			$this->set($name, $value, false);
 		}
-		return $this;
+		return $this->save();
 	}
 
 	/**
 	 * Save all dirty fields to database
 	 */
-	public function save(bool $updateTimestamp = true): bool
+	public function save(): bool
 	{
-		if (!$this->item->isValid()) {
-			JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
-				'fields' => array_keys($this->item->getInvalidFields())
-			], 'warning');
-			return false;
-		}
+		$dirtyFields = array_filter($this->fields, function ($field) {
+			return $field->isDirty;
+		});
+		$defaults = array_filter($dirtyFields, function ($field) {
+			return $field->isDefault;
+		});
+		$custom = array_filter($dirtyFields, function($field) {
+			return !$field->isDefault;
+		});
 
-		// Check for field overrides before saving
-		foreach ($this->item->getDirtyFields() as $field) {
-			if ($this->checkOverrides($field)) {
-				$field->markClean();
+		$success = true;
+
+		if (!empty($defaults)) {
+			$result = false;
+			switch ($this->type) {
+				case 'post':
+					//Deal with field exceptions, first, that cannot be set via wp_update_post
+					foreach ($defaults as $fieldName => $field) {
+						switch ($fieldName) {
+							case 'post_thumbnail':
+								$result = set_post_thumbnail($this->ID, $field->value);
+								unset($defaults[$fieldName]);
+								break;
+						}
+						//If it's a taxonomy, we use wp_set_object_terms
+						if (
+							($field->config['type'] === 'selector' && $field->config['subtype'] === 'taxonomy')
+							|| $field->config['type'] === 'taxonomy') {
+
+							$result = wp_set_object_terms($this->ID, array_map('absint', explode(',', $field->value)), jvbCheckBase($field->config['taxonomy']));
+							unset($defaults[$fieldName]);
+						}
+					}
+
+
+
+					if (!empty($defaults)) {
+						error_log('Remaining fields: '.print_r($defaults, true));
+						$defaults = array_map(function ($field) {
+							return $field->value;
+						}, $defaults);
+						error_log('Remaining values to save: '.print_r($defaults, true));
+						$data = array_merge([
+							'post_type'	=> $this->slug,
+							'ID'		=> $this->ID
+						], $defaults);
+						error_log('Updating post: '.print_r($data, true));
+						$result = wp_update_post($data);
+					}
+					break;
+				case 'term':
+					$result = wp_update_term($this->ID, $this->slug, $defaults);
+					break;
+				case 'user':
+					$data = array_merge([
+						'ID'	=> $this->ID
+					], $defaults);
+					$result = wp_update_user($data);
+					break;
+			}
+			if (!$result || is_wp_error($result)) {
+				$success = false;
+			}
+		}
+		if (!empty($custom)) {
+			$function = match ($this->type) {
+				'post' => 'update_post_meta',
+				'term' => 'update_term_meta',
+				'user' => 'update_user_meta',
+				'options', 'option' => 'update_option',
+				default => false,
+			};
+			if (!$function) {
+				error_log('[Meta]::save() Invalid type, cannot save: '.$this->type);
+				return false;
+			}
+			foreach ($custom as $field) {
+				$result = $function($this->ID, BASE.$field->name, $field->value);
+				if (!$result) {
+					error_log('Problem saving field: '.$field->name.' with value: '.print_r($field->value, true));
+				}
+			}
+			if ($this->type === 'term' && Registrar::getInstance($this->slug)->hasFeature('is_content')) {
+				update_term_meta($this->ID, BASE.'date_modified', date('Y-m-d H:i:s'));
 			}
 		}
 
-		return $this->storage->save($this->item, $updateTimestamp);
+
+		return $success;
 	}
 
 	/**
@@ -420,16 +357,28 @@
 	 */
 	public function delete(string $name): bool
 	{
-		$result = $this->storage->delete($this->item, $name);
-
-		if ($result && $field = $this->item->getField($name)) {
-			$field->set($this->getDefaultValue($name));
-			$field->markClean();
+		$field = $this->fields[$name]??false;
+		if (!$field) {
+			error_log('[Meta]::delete Could not delete field '.$name.': not registered');
+			return true;
 		}
 
-		return $result;
+		if ($field->isTaxonomy()) {
+			wp_set_object_terms($this->ID, [], $this->slug);
+			return true;
+		}
+
+		return match ($this->type) {
+			'post'	=> delete_post_meta($this->ID, BASE.$name),
+			'term'	=> delete_term_meta($this->ID, BASE.$name),
+			'user', 'integrations' => delete_user_meta($this->ID, BASE.$name),
+			'options'	=> delete_option(BASE.$name),
+			default => false
+		};
 	}
 
+
+
 	/**
 	 * Delete multiple field values
 	 */
@@ -442,9 +391,9 @@
 		return $results;
 	}
 
-	// ─────────────────────────────────────────────────────────────
-	// Repeater Access
-	// ─────────────────────────────────────────────────────────────
+	/*****************************************************************
+	 * Repeater Access
+	*****************************************************************/
 
 	/**
 	 * Get repeater accessor for fluent repeater operations
@@ -456,6 +405,7 @@
 
 	protected function setByPath(string $path, mixed $value): self
 	{
+		error_log('Setting by path: '.$path.', with value: '.print_r($value, true));
 		$parts = explode(':', $path, 3);
 		if (count($parts) !== 3) {
 			return $this;
@@ -482,24 +432,6 @@
 	// Utility Methods
 	// ─────────────────────────────────────────────────────────────
 
-	/**
-	 * Get all dirty (changed) field values
-	 */
-	public function getDirty(): array
-	{
-		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
@@ -510,84 +442,22 @@
 		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;
-	}
-
-	/**
-	 * Re-enable validation and sanitization
-	 */
-	public function withDefaults(): self
-	{
-		$this->autoValidate = true;
-		$this->autoSanitize = true;
-		return $this;
-	}
-
-	/**
-	 * Get the underlying Item
-	 */
-	public function item(): Item
-	{
-		return $this->item;
-	}
 
 	/**
 	 * Get field configuration
 	 */
 	public function config(string $name): ?array
 	{
-		return $this->item->getFieldConfig($name);
+		return $this->fields[$name]->config??null;
 	}
 
-	/**
-	 * Get all field configurations
-	 */
-	public function configs(): array
-	{
-		return $this->item->fieldConfigs;
-	}
 
 	/**
 	 * Get item ID
 	 */
 	public function id(): int|string|null
 	{
-		return $this->item->id;
+		return $this->ID;
 	}
 
 	/**
@@ -595,7 +465,7 @@
 	 */
 	public function objectType(): string
 	{
-		return $this->item->objectType;
+		return $this->type;
 	}
 
 	/**
@@ -603,47 +473,9 @@
 	 */
 	public function contentType(): ?string
 	{
-		return $this->item->contentType;
+		return $this->contentType;
 	}
 
-	/**
-	 * Eager load all fields
-	 */
-	public function eager(): self
-	{
-		$this->getAll();
-		return $this;
-	}
-
-	/**
-	 * Convert loaded fields to array
-	 */
-	public function toArray(): array
-	{
-		return $this->item->toArray();
-	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Event Callbacks
-	// ─────────────────────────────────────────────────────────────
-
-	/**
-	 * Register callback for field changes
-	 */
-	public function onChange(string $field, callable $callback): self
-	{
-		$this->onChangeCallbacks[$field][] = $callback;
-		return $this;
-	}
-
-	/**
-	 * Register computed/virtual field
-	 */
-	public function computed(string $name, callable $getter): self
-	{
-		$this->computed[$name] = $getter;
-		return $this;
-	}
 
 	// ─────────────────────────────────────────────────────────────
 	// Protected Helpers

--
Gitblit v1.10.0