[],'term' => [], 'user'=>[],'options'=>[]]; 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 = 'ajv'): self { 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 ***************************************************************/ public function __construct(int|string|null $id, string $type) { $this->validator = new Validator(); $this->sanitizer = new Sanitizer(); $this->typeManager = new MetaTypeManager(); $this->type = $type; $this->ID = $id; $this->buildData($id, $type); } protected function buildData(int|string|null $id, string $type):void { $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 }; $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 { $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); } } // ───────────────────────────────────────────────────────────── // 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); } 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); } $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) { error_log('Setting '.$name.' with value: '.print_r($value, true)); $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': $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 $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), '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 // ───────────────────────────────────────────────────────────── /** * 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; } // ───────────────────────────────────────────────────────────── // Protected Helpers // ───────────────────────────────────────────────────────────── /** * 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): mixed { $config = $this->item->getFieldConfig($name); $type = $config['type'] ?? 'text'; return match ($this->typeManager->getMetaType($type)) { 'object', 'array' => [], 'boolean' => false, 'integer' => 0, default => '', }; } }