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