Jake Vanderwerf
2026-09-04 32ce5c542c90be348895328078e392dde2052957
inc/meta/MetaOld.php
New file
@@ -0,0 +1,744 @@
<?php
namespace JVBase\meta;
use JVBase\base\options;
use JVBase\registrar\Registrar;
use WP_Post;
use WP_Term;
use WP_User;
if (!defined('ABSPATH')) {
   exit;
}
class MetaOld
{
   /**
    * @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|false|null $wpObject;
   protected int|string $ID;
   protected MetaTypeManager $typeManager;
   protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'option'=>[]];
   protected array $defaults = ['post_thumbnail'];
   /**
    * Create Meta instance for a post
    */
   public static function forPost(int $id): self
   {
      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
   {
      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
   {
      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 = BASE): self
   {
      if (array_key_exists($baseKey, self::$instances['option'])) {
         return self::$instances['option'][$baseKey];
      }
      $new = new self($baseKey, 'option');
      self::$instances['option'][$baseKey] = $new;
      return $new;
   }
   /***************************************************************
    * Constructor
   ***************************************************************/
   public function __construct(int|string|null $id, string $type, array $fields = [])
   {
      $this->validator = new Validator();
      $this->sanitizer = new Sanitizer();
      $this->typeManager = new MetaTypeManager();
      $this->type = $type;
      $this->ID = $id;
      $this->buildData($id, $type, $fields);
   }
   protected function buildData(int|string|null $id, string $type, array $fields):void
   {
      $this->wpObject = match($type) {
         'post'   => get_post($id),
         'term'   => get_term($id),
         'user', 'integrations' => get_userdata($id),
         default => false
      };
      $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() : $fields;
      if ($this->type === 'post' && $this->wpObject && $this->wpObject->post_type === 'product') {
         $fields = $this->getProductFields();
      }
      if ($this->type == 'option') {
         $options = options::getInstance();
         $fields = $options->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);
      $this->fields = [];
      foreach ($fields as $fieldName => $config) {
         $fieldName = jvbNoBase($fieldName);
         $value = '';
         if ($this->type === 'option') {
            $value = get_option(BASE . $fieldName, $config['default'] ?? '');
         } else if ($this->wpObject && $this->wpObject->post_type === 'product') {
            if (property_exists($this->wpObject, $fieldName)) {
               if ($fieldName === 'post_thumbnail') {
                  $value = get_post_thumbnail_id($this->ID);
               } else {
                  $value = $this->wpObject->$fieldName;
               }
            } else {
               $product = wc_get_product($this->wpObject->ID);
               $value = match ($fieldName) {
                  'sku' => $product->get_sku(),
                  '_price' => $product->get_price(),
                  'description'=> $product->get_short_description(),
                  'sale'   => $product->get_sale_price(),
                  'post_thumbnail'  => $product->get_image_id(),
                  '_product_images' => $product->get_gallery_image_ids(),
                  '_stock' => $product->get_stock_quantity(),
                  '_stock_status'   => $product->get_stock_status(),
                  default => '',
               };
            }
         } else if (
            ($this->wpObject && property_exists($this->wpObject, $fieldName))
            || ($type === 'user' && $this->wpObject && property_exists($this->wpObject->data, $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 {
            $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);
      }
   }
      protected function getProductFields():array
      {
         $fields = [];
         if (class_exists('WooCommerce') && $this->type === 'post' && $this->wpObject->post_type??'' === 'product') {
            $fields['post_title'] = [
               'type'   => 'text',
               'label'  => 'Title',
            ];
            $fields['post_content'] = [
               'type'   => 'text',
               'label'  => 'Content',
            ];
            $fields['post_excerpt'] = [
               'type'   => 'text',
               'label'  => 'Short Description',
            ];
            $fields['post_thumbnail'] = [
               'type'   => 'upload',
               'multiple'  => false,
               'label'  => 'Image',
            ];
            $fields['_product_images'] = [
               'type'   => 'upload',
               'label'  => 'Gallery'
            ];
            $fields['ID'] = [
               'type'   => 'text',
               'label'  => 'ID',
            ];
            $fields['_stock'] = [
               'type' => 'number',
               'label' => 'Stock',
               'default'   => null
            ];
            $fields['_stock_status'] = [
               'type'   => 'select',
               'label'  => 'Stock Status',
               'default'   => 'instock',
               'options'   => [
                  'instock'   => 'In Stock',
                  'outofstock'   => 'Out of Stock',
                  'onbackorder'     => 'On Backorder'
               ]
            ];
            $fields['_price'] = [
               'type'      => 'text',
               'subtype'   => 'number',
               'label'     => 'Price'
            ];
            $product = wc_get_product($this->ID);
            $fields['sku'] = [
               'type'   => 'text',
               'label'  => 'SKU',
            ];
            $out = [];
            if ($product && $product->is_type('variable')) {
               $var = $product->get_children();
               $var = array_filter(array_map(function($v) {
                  return wc_get_product($v);
               }, $var));
               $title= $product->get_title();
               foreach ($var as $variation) {
                  $out[] = [
                     'id'  => $variation->get_id(),
                     'sku' => $variation->get_sku(),
                     'title'  => str_replace($title.' - ', '', $variation->get_name()),
                     'price'  => $variation->get_price(),
                     'description'=> $variation->get_short_description(),
                     'sale'   => $variation->get_sale_price(),
                     'image'  => $variation->get_image_id(),
                     'stock'  => $variation->get_stock_quantity(),
                     'stock_status' => $variation->get_stock_status()
                  ];
               }
            }
            if (!empty($out)) {
               $this->fields['variations'] = new Field('variations', $out, [
                  'type'   => 'repeater',
                  'label'  => 'Variations',
                  'fields' => [
                     'id'  => [
                        'type'   => 'text',
                        'subtype'   => 'number',
                        'label'  => 'Variation ID',
                     ],
                     'sku' => [
                        'type'   => 'text',
                        'label'  => 'Stock Keeping Unit (SKU)'
                     ],
                     'title'  => [
                        'type'   => 'text',
                        'label'  => 'Variation Title'
                     ],
                     'price'  => [
                        'type'   => 'text',
                        'subtype'   => 'number',
                        'label'  => 'Variation Price'
                     ],
                     'sale'   => [
                        'type'   => 'text',
                        'subtype'   => 'number',
                        'label'  => 'Sale Price',
                     ],
                     'image'  => [
                        'type'   => 'text',
                        'subtype'   => 'number',
                        'label'  => 'Variation Image ID'
                     ],
                     'stock'  => [
                        'type'   => 'text',
                        'label'  => 'Stock Quantity'
                     ],
                     'stock_status' => [
                        'type'   => 'text',
                        'label'  => 'Stock Status'
                     ]
                  ]
               ]);
            }
         }
         return $fields;
      }
   // ─────────────────────────────────────────────────────────────
   // Magic Methods for Fluent Access
   // ─────────────────────────────────────────────────────────────
   public function __get(string $name): mixed
   {
      return $this->get($name);
   }
   public function __set(string $name, mixed $value): void
   {
      $this->set($name, $value);
   }
   public function __isset(string $name): bool
   {
      return $this->item->hasField($name);
   }
   // ─────────────────────────────────────────────────────────────
   // Core API
   // ─────────────────────────────────────────────────────────────
   /**
    * Get a field value
    */
   public function get(string $name): mixed
   {
      // Handle repeater subfield path
      if (str_contains($name, ':')) {
         return $this->getByPath($name);
      }
      // Group shortcut path
      if (str_contains($name, '|')) {
         [$parent, $child] = explode('|', $name, 2);
         return $this->group($parent)->get($child);
      }
      if (!array_key_exists($name, $this->fields)) {
         error_log('[Meta]::get Attempted to get unregistered field: '.$name);
         return '';
      }
      if (is_null($this->fields[$name])) {
         error_log('[Meta]::get Field does not seem to be setup yet: '.$name);
         return '';
      }
      return $this->fields[$name]->get()??'';
   }
   /**
    * Set a field value (validates & sanitizes by default)
    */
   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);
      }
      // Group shortcut
      if (str_contains($name, '|')) {
         return $this->setGroup($name, $value, $autosave);
      }
      $field = $this->fields[$name]??false;
      if (!$field) {
         error_log('No config found for field: '.$name);
         return $this;
      }
      // Validate
      if (!$this->validator->validate($value, $field->config)) {
         $field->addError("Validation failed for {$name}");
         return $this;
      }
      // Sanitize
      $value = $this->sanitizer->sanitize($value, $field->config);
      $field->set($value);
      if ($autosave && $field->isDirty) {
         $this->save();
      }
      return $this;
   }
   /**
    * Get multiple fields
    */
   public function getAll(array $fields = []): array
   {
      if (empty($fields) || $fields === ['all']) {
         $fields = array_keys($this->fields);
      }
      $fields = array_filter($this->fields, function ($field) use ($fields) {
         return in_array($field, $fields);
      }, ARRAY_FILTER_USE_KEY);
      return array_map(function ($field) {
         return $field->value;
      }, $fields);
   }
   /**
    * Set multiple fields
    */
   public function setAll(array $data):bool
   {
      foreach ($data as $name => $value) {
         $this->set($name, $value, false);
      }
      return $this->save();
   }
   /**
    * Save all dirty fields to database
    */
   public function save(): bool
   {
      $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;
      });
      $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',
            'option' => 'update_option',
            default => false,
         };
         if (!$function) {
            error_log('[Meta]::save() Invalid type, cannot save: '.$this->type);
            return false;
         }
         foreach ($custom as $field) {
            if ($this->type === 'option') {
               $result = update_option(BASE.$field->name, $field->value);
            } else {
               $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 $success;
   }
   /**
    * Delete a field value
    */
   public function delete(string $name): bool
   {
      $field = $this->fields[$name]??false;
      if (!$field) {
         error_log('[Meta]::delete Could not delete field '.$name.': not registered');
         return true;
      }
      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),
         'option' => 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
   {
      $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);
   }
   public function group(string $name): Group
   {
      return new Group($this, $name);
   }
   protected function setGroup(string $name, mixed $value, bool $autosave = true): self
   {
      $parent = strtok($name, '|');
      $child = str_replace($parent . '|', '', $name);
      $this->group($parent)->set($child, $value, $autosave);
      return $this;
   }
   // ─────────────────────────────────────────────────────────────
   // Utility Methods
   // ─────────────────────────────────────────────────────────────
   /**
    * Discard all unsaved changes
    */
   public function reset(): self
   {
      $this->item->resetAll();
      return $this;
   }
   /**
    * Get field configuration
    */
   public function config(string $name): ?array
   {
      return $this->fields[$name]->config??null;
   }
   /**
    * Get item ID
    */
   public function id(): int|string|null
   {
      return $this->ID;
   }
   /**
    * Get object type (post, term, user, options)
    */
   public function objectType(): string
   {
      return $this->type;
   }
   /**
    * Get content type (tattoo, artist, etc)
    */
   public function contentType(): ?string
   {
      return $this->contentType;
   }
   public function item():Item
   {
      return $this->item;
   }
   /**
    * Check for field update overrides
    */
   public 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;
   }
   /**
    * Get default value for a field type
    */
   protected function getDefaultValue(string $name): string|int|bool|array
   {
      $config = $this->item->getFieldConfig($name);
      $type = $config['type'] ?? 'text';
      return match ($this->typeManager->getMetaType($type)) {
         'object', 'array' => [],
         'boolean' => false,
         'integer' => 0,
         default => '',
      };
   }
}