Jake Vanderwerf
2026-08-30 f8ffc3dc1748b9af63123eb281696c0c30448715
inc/meta/Meta.php
@@ -1,399 +1,609 @@
<?php
namespace JVBase\meta;
use Exception;
use JVBase\managers\Cache;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
   exit;
}
/**
 * Main facade for meta operations
 * Fluent API for getting/setting meta values with validation & sanitization
 */
class Meta
{
   protected Item $item;
   protected Storage $storage;
   protected MetaValidator $validator;
   protected MetaSanitizer $sanitizer;
   protected MetaTypeManager $typeManager;
class Meta {
   /**
    * @var string post, term, user, or options
    */
   protected string $objectType;
   /**
    * @var string|null post type, user role, or taxonomy
    */
   protected ?string $content;
   /**
    * @var int|string <int>post, term, or user ID OR {string} option key base
    */
   protected int|string $ID;
   /**
    * @var array of Registrar's defined fields, if any
    */
   protected static array $registrars = [];
   /**
    * @var array Field values, stored in memory
    */
   protected array $fields;
   protected bool $autoValidate = true;
   protected bool $autoSanitize = true;
   protected ?\WC_Product $product = null;
   // ─────────────────────────────────────────────────────────────
   // Factory Methods
   // ─────────────────────────────────────────────────────────────
   protected static Validator $validator;
   protected static Sanitizer $sanitizer;
   public static function forPost(int $id): self
   {
      return new self($id, 'post');
   }
   protected static array $coreFields = [
      'post' => ['post_title', 'post_content', 'post_excerpt', 'post_status', 'post_date', 'post_modified', 'post_thumbnail', 'post_name'],
      'term' => ['name', 'description', 'parent'],
      'user' => ['first_name', 'last_name', 'user_email', 'display_name', 'user_login'],
      'options' => []
   ];
   public static function forTerm(int $id): self
   {
      return new self($id, 'term');
   }
   protected static array $instances = [
      'post'   => [],
      'term'   => [],
      'user'   => [],
      'options'=> [],
   ];
   public static function forUser(int $id): self
   {
      return new self($id, 'user');
   }
   private function __construct(string $objectType, int|string $ID) {
      $this->objectType = $objectType;
      $this->ID = $ID;
      $this->content = $this->resolveBasedType();
   public static function forOptions(?string $baseKey = null): self
   {
      $instance = new self($baseKey, 'options');
      $instance->item->baseKey = $baseKey;
      return $instance;
   }
   // ─────────────────────────────────────────────────────────────
   // Constructor
   // ─────────────────────────────────────────────────────────────
   public function __construct(int|string|null $id, string $type)
   {
      $this->storage = new Storage();
      $this->validator = new MetaValidator();
      $this->sanitizer = new MetaSanitizer();
      $this->typeManager = new MetaTypeManager();
      $this->item = $this->buildItem($id, $type);
   }
   protected function buildItem(int|string|null $id, string $type): Item
   {
      $contentType = null;
      $wpObject = 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];
         } else {
            $item->fieldConfigs[$name]['_wp_default'] = true;
      if ($this->content && !array_key_exists($this->content, self::$registrars)) {
         $registrar = Registrar::getInstance($this->content);
         if ($registrar) {
            self::$registrars[$this->content] = $registrar->getFields();
         }
      }
      return $item;
      if ($this->objectType === 'post' && function_exists('wc_get_product') && in_array($this->content, ['product', 'product_variation'], true)) {
         $this->product = wc_get_product($this->ID) ?: null;
      }
      $this->buildData();
      if (!isset(self::$sanitizer)) {
         self::$sanitizer = new Sanitizer();
      }
      if (!isset(self::$validator)) {
         self::$validator = new Validator();
      }
   }
      protected function buildData():void
      {
         //Get all Item meta
         $this->fields = match ($this->objectType) {
            'post'   => get_post_meta($this->ID),
            'term'   => get_term_meta($this->ID),
            'user'   => get_user_meta($this->ID),
            default  => []
         };
         $this->fields = array_combine(
            array_map('jvbNoBase', array_keys($this->fields)),
            $this->fields
         );
   protected function loadFieldConfigs(?string $contentType, string $objectType): array
   {
      if (!$contentType && $objectType !== 'options') {
         return [];
         //Add in core fields
         $obj = match ($this->objectType) {
            'post'   => get_post($this->ID),
            'term'   => get_term($this->ID, $this->content),
            'user'   => get_userdata($this->ID),
            default => false
         };
         if ($obj) {
            foreach (self::$coreFields[$this->objectType] as $name) {
               $value =  match ($name) {
                  'post_thumbnail'  => get_post_thumbnail_id($this->ID),
                  default  => $obj->$name??false
               };
//             if (!$value) {
//                $this->log('Could not find value for '.$name.', of '.$this->content);
//             }
               $this->fields[$name] = $value??'';
            }
         }
      }
      return jvbGetFields($contentType ?? 'options', $objectType);
   public static function forPost(int $ID):self
   {
      if (array_key_exists($ID, self::$instances['post'])) {
         return self::$instances['post'][$ID];
      }
      self::$instances['post'][$ID] = new self('post', $ID);
      return self::$instances['post'][$ID];
   }
   // ─────────────────────────────────────────────────────────────
   // Magic Methods for Fluent Access
   // ─────────────────────────────────────────────────────────────
   public function __get(string $name): mixed
   public static function forTerm(int $ID):self
   {
      return $this->get($name);
      if (array_key_exists($ID, self::$instances['term'])) {
         return self::$instances['term'][$ID];
      }
      self::$instances['term'][$ID] = new self('term', $ID);
      return self::$instances['term'][$ID];
   }
   public function __set(string $name, mixed $value): void
   public static function forUser(int $ID):self
   {
      $this->set($name, $value);
      if (array_key_exists($ID, self::$instances['user'])) {
         return self::$instances['user'][$ID];
      }
      self::$instances['user'][$ID] = new self('user', $ID);
      return self::$instances['user'][$ID];
   }
   public function __isset(string $name): bool
   public static function forOptions(string $base = 'default'):self
   {
      return $this->item->hasField($name);
      if (array_key_exists($base, self::$instances['options'])) {
         return self::$instances['options'][$base];
      }
      self::$instances['options'][$base] = new self('options', $base);
      return self::$instances['options'][$base];
   }
   // ─────────────────────────────────────────────────────────────
   // Core API
   // ─────────────────────────────────────────────────────────────
   protected function resolveBasedType(): ?string {
      return match ($this->objectType) {
         'post' => get_post_type($this->ID) ?: null,
         'term' => ($term = get_term($this->ID)) && !is_wp_error($term) ? $term->taxonomy : null,
         'user' => jvbUserRole($this->ID),
         default => null
      };
   }
   /**
    * Get a field value
    */
   public function get(string $name): mixed
   {
      // Return from loaded field if exists
      if ($field = $this->item->getField($name)) {
         return $field->get();
   /***************************************************************************
    * GETTING FIELDS
   ***************************************************************************/
   public function get(string $name): mixed {
      if (array_key_exists($name, $this->fields)) {
         return $this->fields[$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);
      if ($this->product && $this->isProductField($name)) {
         $value = $this->getProductField($name);
      } else if (in_array($name, static::$coreFields[$this->objectType], true)) {
         $value = $this->getCoreField($name);
      } else {
         $value = $this->getMetaField($name);
      }
      $this->fields[$name] = $value;
      return $value;
   }
   /**
    * Set a field value (validates & sanitizes)
    */
   public function set(string $name, mixed $value): self
   {
      $config = $this->item->getFieldConfig($name);
      if (!$config) {
         // Allow setting unknown fields with minimal config
         $config = ['type' => 'text', 'name' => $name];
   public function getAll(?array $names = null): array {
      $names = $names ?? $this->getAllFieldNames();
      $out = [];
      foreach ($names as $name) {
         $out[$name] = $this->get($name);
      }
      $config['name'] = $name;
      // Validate
      if ($this->autoValidate && !$this->validator->validate($value, $config)) {
         $field = $this->item->getField($name) ?? new Field($name, $value, $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);
      }
      return $this;
      return $out;
   }
   /**
    * Get multiple fields
    */
   public function getAll(array $fields = []): array
   {
      if (empty($fields) || $fields === ['all']) {
         $fields = array_keys($this->item->fieldConfigs);
   protected function getCoreField(string $name): mixed {
      return match ($this->objectType) {
         'post' => $name === 'post_thumbnail'
            ? get_post_thumbnail_id($this->ID)
            : (($p = get_post($this->ID)) ? ($p->$name ?? null) : null),
         'term' => ($t = get_term($this->ID, $this->content)) && !is_wp_error($t) ? ($t->$name ?? null) : null,
         'user' => ($u = get_userdata($this->ID)) ? ($u->$name ?? null) : null,
      };
   }
   protected function getMetaField(string $name): mixed {
      $key = $this->metaKey($name);
      return match ($this->objectType) {
         'post' => get_post_meta($this->ID, $key, true),
         'term' => get_term_meta($this->ID, $key, true),
         'user' => get_user_meta($this->ID, $key, true),
         'options' => get_option($key)
      };
   }
   /***************************************************************************
    * SETTING FIELDS
   ***************************************************************************/
   public function set(string $name, mixed $value): bool {
      $field = $this->getFieldConfig($name);
      if ($field) {
         if ($field['type']??'' === 'repeater' && str_contains($name, ':')) {
            return $this->setByPath($name, $value);
         } elseif ($field['type']??'' === 'group' && str_contains($name, '|')) {
            return $this->setGroup($name, $value);
         }
         if (!self::$validator->validate($value, $field)) {
            return false;
         }
         $value = self::$sanitizer->sanitize($value, $field);
      }
      // Load all from storage
      $values = $this->storage->getAll($this->item, $fields);
      if ($this->product && $this->isProductField($name)) {
         return $this->setProductField($name, $value);
      }
      if (in_array($name, static::$coreFields[$this->objectType], true)) {
         return $this->setCoreField($name, $value);
      }
      return $this->setMetaField($name, $value);
   }
      // 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));
   public function setAll(array $data): array {
      $core = array_filter($data, function ($name) {
         return in_array($name, self::$coreFields[$this->objectType]);
      }, ARRAY_FILTER_USE_KEY);
      $results = [];
      if (!empty($core)) {
         $data = array_filter($data, function ($name) {
            return !in_array($name, self::$coreFields[$this->objectType]);
         }, ARRAY_FILTER_USE_KEY);
         $results = $this->setAllCoreFields($core);
      }
      foreach ($data as $name => $value) {
         $results[$name] = $this->set($name, $value);
      }
      return $results;
   }
   protected function setAllCoreFields(array $data):array
   {
      $results = [];
      $core = array_filter($data, function ($name) {
         return in_array($name, self::$coreFields[$this->objectType]);
      }, ARRAY_FILTER_USE_KEY);
      if (count($core) !== count($data)) {
         $notCore = array_filter($data, function ($name) {
            return !in_array($name, self::$coreFields[$this->objectType]);
         }, ARRAY_FILTER_USE_KEY);
         $results = $this->setAll($notCore);
      }
      if (array_key_exists('post_thumbnail', $core)) {
         $results['post_thumbnail'] = $this->setCoreField('post_thumbnail', $core['post_thumbnail']);
         unset ($core['post_thumbnail']);
      }
      $return = match ($this->objectType) {
         'post'   => !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $core))),
         'term'   => !is_wp_error(wp_update_term($this->ID, $this->content, $core)),
         'user'   => !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $core))),
      };
      foreach ($core as $name => $value) {
         $results[$name] = $return;
      }
      return $results;
   }
   protected function setCoreField(string $name, mixed $value): bool {
      $success = match ($this->objectType) {
         'post' => $name === 'post_thumbnail'
            ? (!empty($value) ? set_post_thumbnail($this->ID, (int)$value) : delete_post_thumbnail($this->ID))
            : !is_wp_error(wp_update_post(['ID' => $this->ID, $name => $value])),
         'term' => !is_wp_error(wp_update_term($this->ID, $this->content, [$name => $value])),
         'user' => !is_wp_error(wp_update_user(['ID' => $this->ID, $name => $value])),
         default => false
      };
      if ($success) {
         $this->clearCache();
         $this->fields[$name] = $value;
      }
      return $success;
   }
   protected function setMetaField(string $name, mixed $value): bool {
      $key = $this->metaKey($name);
      $result = match ($this->objectType) {
         'post' => update_post_meta($this->ID, $key, $value),
         'term' => update_term_meta($this->ID, $key, $value),
         'user' => update_user_meta($this->ID, $key, $value),
         'options' => update_option($key, $value)
      };
      if ($result) {
         $this->fields[$name] = $value;
         $this->clearCache();
      }
      return $result !== false;
   }
   /********************************************************************************
    * DELETING FIELDS
   ********************************************************************************/
   public function delete(string $name):bool
   {
      if (in_array($name, static::$coreFields[$this->objectType])) {
         return $this->deleteCoreField($name);
      }
      $field = $this->getFieldConfig($name);
      if ($field) {
         if ($this->isTaxonomy($field)) {
            return !is_wp_error(wp_set_object_terms($this->ID, [], $this->content));
         }
      }
      return $values;
      $key = $this->metaKey($name);
      return match ($this->objectType) {
         'post'   => delete_post_meta($this->ID, $key),
         'term'   => delete_term_meta($this->ID, $key),
         'user'   => delete_user_meta($this->ID, $key),
         'option'=> delete_option($key)
      };
   }
   /**
    * Set multiple fields
    */
   public function setAll(array $data): self
   public function deleteAll(array $fieldNames):array
   {
      foreach ($data as $name => $value) {
         $this->set($name, $value);
      $core = array_filter($fieldNames, function ($name) {
         return in_array($name, self::$coreFields[$this->objectType]);
      }, ARRAY_FILTER_USE_KEY);
      $results = [];
      if (!empty($core)) {
         $fieldNames = array_filter($fieldNames, function ($name) {
            return !in_array($name, self::$coreFields[$this->objectType]);
         });
         $results = $this->deleteAllCoreFields($core);
      }
      return $this;
      foreach ($fieldNames as $name) {
         $results[$name] = $this->delete($name);
      }
      return $results;
   }
   protected function deleteAllCoreFields(array $fieldNames):array
   {
      $results = [];
      $core = array_filter($fieldNames, function ($name) {
         return in_array($name, self::$coreFields[$this->objectType]);
      }, ARRAY_FILTER_USE_KEY);
      if (count($core) !== count($fieldNames)) {
         $notCore = array_filter($fieldNames, function ($name) {
            return !in_array($name, self::$coreFields[$this->objectType]);
         }, ARRAY_FILTER_USE_KEY);
         $results = $this->deleteAll($notCore);
      }
      if (array_key_exists('post_thumbnail', $fieldNames)) {
         $results['post_thumbnail'] = delete_post_thumbnail($this->ID);
         unset($fieldNames['post_thumbnail']);
         unset($core['post_thumbnail']);
      }
      $updated = [];
      foreach ($core as $name) {
         $updated[$name] = $this->getCoreDefault($name);
      }
      $success = match ($this->objectType) {
         'post'   => !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $updated))),
         'term'   => !is_wp_error(wp_update_term($this->ID, $this->content, $updated)),
         'user'   => !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $updated)))
      };
      foreach ($core as $name) {
         $results[$name] = $success;
      }
      return $results;
   }
   /**
    * Save all dirty fields to database
    */
   public function save(bool $updateTimestamp = true): bool
   protected function deleteCoreField(string $name):bool
   {
      if (!$this->item->isValid()) {
         JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
            'fields' => array_keys($this->item->getInvalidFields())
         ], 'warning');
      if ($name === 'post_thumbnail') {
         return delete_post_thumbnail($this->ID);
      }
      $default = $this->getCoreDefault($name);
      return $this->setCoreField($name, $default);
   }
   /**************************************************************
    * WOOCOMMERCE PRODUCT SPECIFIC
   **************************************************************/
   protected function isProductField(string $name): bool {
      return method_exists($this->product, 'get_'.$name) || method_exists($this->product, 'set_'.$name);
   }
   protected function getProductField(string $name): mixed {
      $method = 'get_'.$name;
      if ($name === 'variations') {
         return $this->getVariations();
      }
      return method_exists($this->product, $method) ? $this->product->$method() : null;
   }
      protected function getVariations():array
      {
         $variations = $this->product->get_children();
         $out = [];
         foreach ($variations as $v) {
            $out[] = self::forPost($v)->getAll();
         }
         return $out;
      }
   protected function setProductField(string $name, mixed $value): bool {
      $method = 'set_'.$name;
      if (!method_exists($this->product, $method)) return false;
      $this->product->$method($value);
      $this->product->save();
      $this->fields[$name] = $value;
      $this->clearCache();
      return true;
   }
   protected function getAllProductFieldNames(): array
   {
      return [
         'price',
         'sku',
         'sale',
         'short_description',
         'image_id',
         'gallery_image_ids',
         'stock',
         'stock_quantity',
         'stock_status',
         'variations'
      ];
   }
   /*** Sanitization & Validation ***/
// protected function sanitize(mixed $value, array $field): mixed {
//    return match ($field['type']) {
//       'text', 'tel' => sanitize_text_field($value),
//       'email' => sanitize_email($value),
//       'url' => esc_url_raw($value),
//       'textarea' => !empty($field['quill']) ? wp_kses_post($value) : sanitize_textarea_field($value),
//       'number' => is_numeric($value) ? $value + 0 : 0,
//       'true_false' => (bool)$value,
//       'datetime' => sanitize_text_field($value),
//       'upload', 'image', 'gallery' => !empty($field['multiple']) ? array_map('intval', (array)$value) : (int)$value,
//       'checkbox', 'set' => array_map('sanitize_text_field', (array)$value),
//       'radio', 'select' => sanitize_text_field($value),
//       'selector', 'taxonomy', 'user', 'post' => array_map('intval', (array)$value),
//       'repeater', 'tagList' => $this->sanitizeRepeater($value, $field),
//       'group' => $this->sanitizeGroup($value, $field),
//       default => is_array($value) ? array_map('sanitize_text_field', $value) : sanitize_text_field($value),
//    };
// }
// protected function validate(string $name, mixed $value, array $field): bool|string {
//    if (!empty($field['required']) && ($value === '' || $value === null)) {
//       return $name.' is required';
//    }
//    if (isset($field['maxLength']) && is_string($value) && strlen($value) > $field['maxLength']) {
//       return $name.' exceeds max length of '.$field['maxLength'];
//    }
//    if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
//       return $name.' is below minimum of '.$field['min'];
//    }
//    if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
//       return $name.' exceeds maximum of '.$field['max'];
//    }
//    if (in_array($field['type'], ['checkbox', 'set', 'radio', 'select'], true) && !empty($field['options'])) {
//       foreach ((array)$value as $v) {
//          if (!array_key_exists($v, $field['options'])) {
//             return $name.': invalid option '.$v;
//          }
//       }
//    }
//    return true;
// }
   /************************************************************************
    * REPEATER Helpers
   ************************************************************************/
   public function repeater(string $name):Repeater
   {
      return new Repeater($this, $name);
   }
   protected function setByPath(string $path, mixed $value):bool
   {
      $parts = explode(':', $path, 3);
      if (count($parts) !== 3) {
         $this->log('Could not set value by path');
         return false;
      }
      [$repeaterName, $rowIndex, $subField] = $parts;
      $this->clearCache();
      return $this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
   }
      // Check for field overrides before saving
      foreach ($this->item->getDirtyFields() as $field) {
         if ($this->checkOverrides($field)) {
            $field->markClean();
         }
   protected function getByPath(string $path):mixed
   {
      $parts = explode(':', $path, 3);
      if (count($parts) !== 3) {
         return null;
      }
      return $this->storage->save($this->item, $updateTimestamp);
      [$repeaterName, $rowIndex, $subField] = $parts;
      return $this->repeater($repeaterName)->field((int) $rowIndex, $subField);
   }
   /****************************************************************************************
    * GROUP helpers
   ****************************************************************************************/
   public function group(string $name): Group
   {
      return new Group($this, $name);
   }
   /**
    * Delete a field value
    */
   public function delete(string $name): bool
   protected function setGroup(string $name, mixed $value):bool
   {
      $result = $this->storage->delete($this->item, $name);
      if ($result && $field = $this->item->getField($name)) {
         $field->set($this->getDefaultValue($name));
         $field->markClean();
      }
      return $result;
      [$parent, $child] = explode('|', $name, 2);
      $this->clearCache();
      return $this->group($parent)->set($child, $value);
   }
   // ─────────────────────────────────────────────────────────────
   // Utility Methods
   // ─────────────────────────────────────────────────────────────
   /**
    * Get all dirty (changed) fields
    */
   public function getDirty(): array
   /***************************************************************************************
    * UTILITY
   ***************************************************************************************/
   protected function clearCache():void
   {
      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();
      }
      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);
   }
   /**
    * Get all field configurations
    */
   public function configs(): array
   {
      return $this->item->fieldConfigs;
   }
   // ─────────────────────────────────────────────────────────────
   // Protected Helpers
   // ─────────────────────────────────────────────────────────────
   protected function checkOverrides(Field $field): bool
   {
      $name = $field->name;
      $type = $field->type();
      $value = $field->value;
      do_action('jvb_meta_update', $name, $value, $this->item->objectType);
      $overrides = [
         BASE . 'update_' . $name,
         BASE . 'update_' . $type,
         'jvb_update_' . $name,
         'jvb_update_' . $type,
      ];
      foreach ($overrides as $override) {
         if (function_exists($override)) {
            $override($this->item->id, $value);
            return true;
         }
      }
      return false;
   }
   protected function getDefaultValue(string $name): mixed
   {
      $config = $this->item->getFieldConfig($name);
      $type = $config['type'] ?? 'text';
      return match ($this->typeManager->getMetaType($type)) {
         'object', 'array' => [],
         'boolean' => false,
         'integer' => 0,
         default => '',
      match ($this->objectType) {
         'post'   => Cache::clearPost($this->ID),
         'term'   => Cache::clearTerm($this->ID),
         'user'   => Cache::clearUser($this->ID),
      };
   }
   protected function isTaxonomy(array $field):bool
   {
      $type = $field['type']??'';
      return $type === 'taxonomy' || ($type === 'selector' &&$field['subtype']??'' === 'taxonomy');
   }
   protected function getAllFieldNames(): array {
      $names = static::$coreFields[$this->objectType];
      if ($this->hasRegistrar()) {
         $names = array_merge($names, array_keys(self::$registrars[$this->content]));
      }
      if ($this->product) {
         $names = array_merge($names, $this->getAllProductFieldNames());
      }
      return array_unique($names);
   }
   protected function hasRegistrar():bool
   {
      return array_key_exists($this->content, self::$registrars);
   }
   public function getFieldConfig(string $name): ?array {
      if (!$this->hasRegistrar()) return null;
      return self::$registrars[$this->content][$name] ?? null;
   }
   protected function getCoreDefault(string $name):string
   {
      return match($name) {
         'post_status'           => 'draft',
         'post_date','post_modified'   => date('mysql'),
         'post_name' => sanitize_title($this->get('post_title')),
         default => ''
      };
   }
   protected function metaKey(string $name): string {
      if ($this->product) {
         if (in_array($name, $this->getAllProductFieldNames())) {
            return $name;
         }
      }
      return jvbCheckBase($name);
   }
   protected function log(string $message, array $context = []):void
   {
      $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? '';
      error_log('[Meta] '.$caller.' '.$message);
      if (!empty($context)) {
         error_log('    CONTEXT: '.print_r($context, true));
      }
   }
}