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