From 2127b1bdd73ecd2423e443992da4b442f5a3c1a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 04 Feb 2026 21:19:25 +0000
Subject: [PATCH] =Major overhaul of MetaManager.php -> Meta.php and RestRouteManager.php -> Rest.php. Seems to work for JakeVan

---
 inc/meta/Meta.php |  295 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 278 insertions(+), 17 deletions(-)

diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 05810e6..1316684 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -1,8 +1,6 @@
 <?php
 namespace JVBase\meta;
 
-use Exception;
-
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -10,37 +8,62 @@
 /**
  * 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
 {
 	protected Item $item;
 	protected Storage $storage;
-	protected MetaValidator $validator;
-	protected MetaSanitizer $sanitizer;
+	protected Validator $validator;
+	protected Sanitizer $sanitizer;
 	protected MetaTypeManager $typeManager;
 
 	protected bool $autoValidate = true;
 	protected bool $autoSanitize = true;
 
+	/** @var array<string, callable[]> */
+	protected array $onChangeCallbacks = [];
+
+	/** @var array<string, callable> */
+	protected array $computed = [];
+
 	// ─────────────────────────────────────────────────────────────
 	// Factory Methods
 	// ─────────────────────────────────────────────────────────────
 
+	/**
+	 * Create Meta instance for a post
+	 */
 	public static function forPost(int $id): self
 	{
 		return new self($id, 'post');
 	}
 
+	/**
+	 * Create Meta instance for a term
+	 */
 	public static function forTerm(int $id): self
 	{
 		return new self($id, 'term');
 	}
 
+	/**
+	 * Create Meta instance for a user
+	 */
 	public static function forUser(int $id): self
 	{
 		return new self($id, 'user');
 	}
 
+	/**
+	 * Create Meta instance for options
+	 */
 	public static function forOptions(?string $baseKey = null): self
 	{
 		$instance = new self($baseKey, 'options');
@@ -48,6 +71,129 @@
 		return $instance;
 	}
 
+	/**
+	 * 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
 	// ─────────────────────────────────────────────────────────────
@@ -55,8 +201,8 @@
 	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);
@@ -118,7 +264,7 @@
 
 	public function __isset(string $name): bool
 	{
-		return $this->item->hasField($name);
+		return $this->item->hasField($name) || isset($this->computed[$name]);
 	}
 
 	// ─────────────────────────────────────────────────────────────
@@ -130,6 +276,11 @@
 	 */
 	public function get(string $name): mixed
 	{
+		// 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();
@@ -146,7 +297,7 @@
 	}
 
 	/**
-	 * Set a field value (validates & sanitizes)
+	 * Set a field value (validates & sanitizes by default)
 	 */
 	public function set(string $name, mixed $value): self
 	{
@@ -157,8 +308,6 @@
 			$config = ['type' => 'text', 'name' => $name];
 		}
 
-		$config['name'] = $name;
-
 		// Validate
 		if ($this->autoValidate && !$this->validator->validate($value, $config)) {
 			$field = $this->item->getField($name) ?? new Field($name, $value, $config);
@@ -174,17 +323,27 @@
 
 		// Get or create field
 		$field = $this->item->getField($name);
+		$oldValue = null;
 
 		if ($field) {
+			$oldValue = $field->value;
 			$field->set($value);
 		} else {
-			// Need to load original to track dirty state
+			// 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);
+			}
+		}
+
 		return $this;
 	}
 
@@ -259,12 +418,36 @@
 		return $result;
 	}
 
+	/**
+	 * 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);
+	}
+
 	// ─────────────────────────────────────────────────────────────
 	// Utility Methods
 	// ─────────────────────────────────────────────────────────────
 
 	/**
-	 * Get all dirty (changed) fields
+	 * Get all dirty (changed) field values
 	 */
 	public function getDirty(): array
 	{
@@ -287,9 +470,7 @@
 	 */
 	public function reset(): self
 	{
-		foreach ($this->item->fields as $field) {
-			$field->reset();
-		}
+		$this->item->resetAll();
 		return $this;
 	}
 
@@ -332,7 +513,17 @@
 	}
 
 	/**
-	 * Get the underlying item (for rendering, etc)
+	 * 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
 	{
@@ -355,11 +546,77 @@
 		return $this->item->fieldConfigs;
 	}
 
+	/**
+	 * Get item ID
+	 */
+	public function id(): int|string|null
+	{
+		return $this->item->id;
+	}
+
+	/**
+	 * Get object type (post, term, user, options)
+	 */
+	public function objectType(): string
+	{
+		return $this->item->objectType;
+	}
+
+	/**
+	 * Get content type (tattoo, artist, etc)
+	 */
+	public function contentType(): ?string
+	{
+		return $this->item->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
 	// ─────────────────────────────────────────────────────────────
 
-	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 +641,9 @@
 		return false;
 	}
 
+	/**
+	 * Get default value for a field type
+	 */
 	protected function getDefaultValue(string $name): mixed
 	{
 		$config = $this->item->getFieldConfig($name);
@@ -396,4 +656,5 @@
 			default => '',
 		};
 	}
+
 }

--
Gitblit v1.10.0