From 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 28 May 2026 18:19:57 +0000
Subject: [PATCH] =New Gitbit setpu

---
 inc/meta/Meta.php |  543 +++++++++++++++++++++++++++++++++--------------------
 1 files changed, 339 insertions(+), 204 deletions(-)

diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 05810e6..744fae5 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -1,105 +1,166 @@
 <?php
 namespace JVBase\meta;
 
-use Exception;
+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
- */
 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 MetaValidator $validator;
-	protected MetaSanitizer $sanitizer;
+	protected Validator $validator;
+	protected Sanitizer $sanitizer;
+	protected array $fields;
+	protected WP_Post|WP_Term|WP_User|false|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;
-
-	// ─────────────────────────────────────────────────────────────
-	// 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;
 	}
 
+	/**
+	 * Create Meta instance for a term
+	 */
 	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;
 	}
 
+	/**
+	 * Create Meta instance for a user
+	 */
 	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;
 	}
 
-	public static function forOptions(?string $baseKey = null): self
+	/**
+	 * Create Meta instance for options
+	 */
+	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;
 	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Constructor
-	// ─────────────────────────────────────────────────────────────
+	/***************************************************************
+	 * Constructor
+	***************************************************************/
 
 	public function __construct(int|string|null $id, string $type)
 	{
-		$this->storage = new Storage();
-		$this->validator = new MetaValidator();
-		$this->sanitizer = new MetaSanitizer();
+		$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 => false
+		};
 
-		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]
-			};
+		$this->slug = match($type) {
+			'post'	=> $this->wpObject->post_type,
+			'term'	=> $this->wpObject->taxonomy,
+			'user'	=> jvbUserRole($id),
+			default => null
+		};
+
+
+
+		$registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false;
+		$fields = $registrar ? $registrar->getFields() : [];
+		$meta = match($type) {
+			'post'	=> get_post_meta($id),
+			'term'	=> get_term_meta($id),
+			'user'	=> get_user_meta($id),
+			default => []
+		};
+		if (!$meta) {
+			$meta = [];
 		}
+		$meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta);
 
-		$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];
+		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 jvbGetFields($contentType ?? 'options', $objectType);
 	}
 
 	// ─────────────────────────────────────────────────────────────
@@ -130,59 +191,43 @@
 	 */
 	public function get(string $name): mixed
 	{
-		// Return from loaded field if exists
-		if ($field = $this->item->getField($name)) {
-			return $field->get();
+		// Handle repeater subfield path
+		if (str_contains($name, ':')) {
+			return $this->getByPath($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);
-
-		return $value;
+		if (!array_key_exists($name, $this->fields)) {
+			error_log('[Meta]::get Attempted to get unregistered field: '.$name);
+		}
+		return $this->fields[$name]->get()??'';
 	}
 
 	/**
-	 * Set a field value (validates & sanitizes)
+	 * 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
 	{
-		$config = $this->item->getFieldConfig($name);
-
-		if (!$config) {
-			// Allow setting unknown fields with minimal config
-			$config = ['type' => 'text', 'name' => $name];
+		// Handle repeater subfield path (e.g., "services:2:image")
+		if (str_contains($name, ':')) {
+			return $this->setByPath($name, $value);
 		}
 
-		$config['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);
-
-		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);
+		$value = $this->sanitizer->sanitize($value, $field->config);
+		$field->set($value);
+		if ($autosave && $field->isDirty) {
+			$this->save();
 		}
 
 		return $this;
@@ -194,54 +239,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);
+			$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':
+					$termDefaults = array_map(fn($field) => $field->value, $defaults);
+					$result = wp_update_term($this->ID, $this->slug, $termDefaults);
+					break;
+				case 'user':
+					$userDefaults = array_map(fn($field) => $field->value, $defaults);
+					$data = array_merge(['ID' => $this->ID], $userDefaults);
+					$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));
+				}
+			}
+			//Now handled directly from Registrar
+//			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;
 	}
 
 	/**
@@ -249,117 +363,134 @@
 	 */
 	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
+	 */
+	public function deleteAll(array $names): array
+	{
+		$results = [];
+		foreach ($names as $name) {
+			$results[$name] = $this->delete($name);
+		}
+		return $results;
+	}
+
+	/*****************************************************************
+	 * Repeater Access
+	*****************************************************************/
+
+	/**
+	 * Get repeater accessor for fluent repeater operations
+	 */
+	public function repeater(string $name): Repeater
+	{
+		return new Repeater($this, $name);
+	}
+
+	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;
+		}
+
+		[$repeaterName, $rowIndex, $subField] = $parts;
+		$this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
+
+		return $this;
+	}
+
+	protected function getByPath(string $path): mixed
+	{
+		$parts = explode(':', $path, 3);
+		if (count($parts) !== 3) {
+			return null;
+		}
+
+		[$repeaterName, $rowIndex, $subField] = $parts;
+		return $this->repeater($repeaterName)->field((int) $rowIndex, $subField);
 	}
 
 	// ─────────────────────────────────────────────────────────────
 	// Utility Methods
 	// ─────────────────────────────────────────────────────────────
 
-	/**
-	 * Get all dirty (changed) fields
-	 */
-	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
 	 */
 	public function reset(): self
 	{
-		foreach ($this->item->fields as $field) {
-			$field->reset();
-		}
+		$this->item->resetAll();
 		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);
+		return $this->fields[$name]->config??null;
+	}
+
+
+	/**
+	 * Get item ID
+	 */
+	public function id(): int|string|null
+	{
+		return $this->ID;
 	}
 
 	/**
-	 * Get all field configurations
+	 * Get object type (post, term, user, options)
 	 */
-	public function configs(): array
+	public function objectType(): string
 	{
-		return $this->item->fieldConfigs;
+		return $this->type;
 	}
 
+	/**
+	 * Get content type (tattoo, artist, etc)
+	 */
+	public function contentType(): ?string
+	{
+		return $this->contentType;
+	}
+
+
 	// ─────────────────────────────────────────────────────────────
 	// Protected Helpers
 	// ─────────────────────────────────────────────────────────────
 
-	protected function checkOverrides(Field $field): bool
+	/**
+	 * Check for field update overrides
+	 */
+	public function checkOverrides(Field $field): bool
 	{
 		$name = $field->name;
 		$type = $field->type();
@@ -384,6 +515,9 @@
 		return false;
 	}
 
+	/**
+	 * Get default value for a field type
+	 */
 	protected function getDefaultValue(string $name): mixed
 	{
 		$config = $this->item->getFieldConfig($name);
@@ -396,4 +530,5 @@
 			default => '',
 		};
 	}
+
 }

--
Gitblit v1.10.0