wpdb = $wpdb; $this->object_id = $ID; $this->object_type = $type; if ($ID) { switch ($type) { case 'post': $this->data = get_post((string)$ID); break; case 'term': $this->data = get_term($ID); break; case 'user': case 'integrations': $this->data = get_user($ID); break; default: $this->data = null; break; } } $this->content = $content; $this->type_manager = new MetaTypeManager(); $this->validator = new MetaValidator(); $this->sanitizer = new MetaSanitizer(); $this->renderer = new MetaRenderer(); $this->form = new MetaForm(); } /** * @param string $field_type * * @return string */ public function getMetaType(string $field_type): string { return $this->type_manager->getMetaType($field_type); } /** * @param array $field * * @return string */ public function getSanitizeCallback(array $field): string { return $this->sanitizer->getCallback($field); } /** * @param string $name * * @return mixed */ public function getValue(string $name): mixed { //Get standard post fields first switch ($name) { case 'post_title': return $this->data->post_title ?? ''; case 'post_excerpt': return $this->data->post_excerpt ?? ''; case 'post_content': return $this->data->post_content ?? ''; case 'featured_image': case 'post_thumbnail': return get_post_thumbnail_id($this->object_id); case 'display_name': if (is_null($this->data) || !$this->data->display_name) { $user = get_userdata((int)get_post_meta($this->object_id, BASE . 'link', true)); return $user->display_name; } return $this->data->display_name ?? ''; case 'user_email': if (is_null($this->data) || !$this->data->display_name) { $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); return $user->user_email; } return $this->data->user_email ?? ''; case 'term_name': return htmlspecialchars_decode($this->data->name); } $meta_key = BASE . $name; switch ($this->object_type) { case 'post': return get_post_meta($this->object_id, $meta_key, true); case 'term': return get_term_meta($this->object_id, $meta_key, true); case 'user': case 'integrations': return get_user_meta($this->object_id, $meta_key, true); case 'options': return get_option($meta_key); default: return ''; } } /** * @param string $name * * @return bool */ public function deleteValue(string $name): bool { try { $meta_key = BASE . "{$name}"; $result = false; switch ($this->object_type) { case 'post': $config = $this->getFieldConfig($name); if ($config['type'] === 'taxonomy' && !array_key_exists('taxonomy_type', $config)) { $result = wp_set_post_terms($this->object_id, '', $config['taxonomy']); } else { $result = delete_post_meta((int)$this->object_id, $meta_key); } break; case 'term': $result = delete_term_meta($this->object_id, $meta_key); break; case 'user': case 'integrations': $result = delete_user_meta($this->object_id, $meta_key); break; } if ($result === false) { throw new Exception("Failed to delete meta value for {$this->field}"); } return true; } catch (Exception $e) { $this->handleError( $e->getMessage(), [ 'object_id' => $this->object_id, 'field' => $name, 'object_type' => $this->object_type, ] ); return false; } } /** * @param array $fields * * @return array */ public function batchDelete(array $fields): array { $results = []; foreach ($fields as $field) { $results[$field] = $this->deleteValue($field); } return $results; } /** * @param string $name * @param mixed $value * * @return bool */ public function updateValue(string $name, mixed $value): bool { try { // Get field definition $fields = $this->getFields(); $field_config = $fields[$name] ?? null; if (!$field_config) { throw new Exception("Field configuration not found for {$name}"); } $field_config['name'] = $name; // Validate value if (!$this->validator->validate($value, $field_config)) { error_log('Validation unsuccessful'); throw new Exception("Validation failed for {$name}"); } // Sanitize value $sanitized = $this->sanitizer->sanitize($value, $field_config); if ($this->checkOverrides($name, $sanitized, $field_config)) { return true; } switch ($name) { case 'post_title': $ID = true; if ($this->data->post_title !== $sanitized) { $ID = wp_update_post([ 'ID' => $this->object_id, 'post_title' => $sanitized ]); } return ($ID !== 0); case 'post_excerpt': $ID = true; if ($this->data->post_excerpt !== $sanitized) { $ID = wp_update_post([ 'ID' => $this->object_id, 'post_excerpt' => $sanitized ]); } return ($ID !== 0); case 'post_content': $ID = true; if ($this->data->post_content !== $sanitized) { $ID = wp_update_post([ 'ID' => $this->object_id, 'post_content' => $sanitized ]); } return ($ID !== 0); case 'featured_image': case 'post_thumbnail': $ID = true; $old = get_post_thumbnail_id($this->object_id); if ($old !== $sanitized) { $ID = set_post_thumbnail($this->object_id, $sanitized); } return ($ID !== false); case 'display_name': $ID = true; $object_id = $this->object_id; $displayName = $this->data->display_name; if (!$this->data->display_name) { $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); $object_id = $user->ID; $displayName = $user->display_name; } if ($displayName !== $sanitized) { $ID = wp_update_user([ 'ID' => $object_id, 'display_name' => $sanitized ]); $link = get_user_meta($object_id, BASE . 'link', true); wp_update_post([ 'ID' => $link, 'post_title' => $sanitized, ]); } return (!is_wp_error($ID)); case 'user_email': $ID = true; $object_id = $this->object_id; $email = $this->data->user_email; if (!$this->data->display_name) { $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); $object_id = $user->ID; $email = $user->user_email; } if ($email !== $sanitized) { $ID = wp_update_user([ 'ID' => $object_id, 'user_email' => $sanitized ]); } return (!is_wp_error($ID)); case 'term_name': $ID = true; $name = $this->data->name; if ($name !== $sanitized) { $ID = wp_update_term($this->data->term_id, $this->data->taxonomy, [ 'name' => $sanitized, 'slug' => sanitize_title($sanitized) ]); } } if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) { error_log('Attempting to set taxonomies: ' . print_r($this->object_id, true)); error_log('Sanitized data: ' . print_r($sanitized, true)); error_log('Taxonomy: ' . print_r($field_config['taxonomy'], true)); $set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false); error_log('Set post terms: ' . print_r($set, true)); } if ($field_config['type'] === 'location' && empty($sanitized)) { $this->addMeta('has_map', false); } // Store value $meta_key = BASE . $name; $result = false; switch ($this->object_type) { case 'post': $result = update_post_meta($this->object_id, $meta_key, $sanitized); break; case 'term': $result = update_term_meta($this->object_id, $meta_key, $sanitized); break; case 'user': case 'integrations': $result = update_user_meta($this->object_id, $meta_key, $sanitized); break; case 'options': $result = update_option($meta_key, $sanitized); } if ($result === false) { throw new Exception("Failed to update meta value for {$name}"); } return true; } catch (Exception $e) { JVB()->error()->log( 'meta_manager', $e->getMessage(), [ 'object_id' => $this->object_id, 'field' => $name, 'object_type' => $this->object_type ], 'error' ); return false; } } /********************* REPEATER HELPERS * mainly for when we need to update a processed image to a repeater row field *********************/ /** * Update a specific field within a repeater row * * @param string $field_string The field string in format "repeater_name:row_index:field_name" * @param mixed $value The value to set for the field * @return bool Success status */ public function updateRepeaterRowField(string $field_string, mixed $value): bool { // Parse the field string $parsed = $this->parseRepeaterFieldString($field_string); if (!$parsed) { error_log('[MetaManager] Invalid repeater field string: ' . $field_string); return false; } $repeater_name = $parsed['repeater']; $row_index = $parsed['row_index']; $field_name = $parsed['field']; // Get current repeater data $repeater_data = $this->getValue($repeater_name); // Initialize as array if empty or not an array if (!is_array($repeater_data)) { $repeater_data = []; } // Ensure row exists if (!isset($repeater_data[$row_index])) { $repeater_data[$row_index] = []; } // Update the specific field in the row $repeater_data[$row_index][$field_name] = $value; // Save the updated repeater data $success = $this->updateValue($repeater_name, $repeater_data); if ($success) { error_log(sprintf( '[MetaManager] Updated repeater field: %s[%d][%s] = %s', $repeater_name, $row_index, $field_name, is_scalar($value) ? $value : json_encode($value) )); } return $success; } /** * Parse a repeater field string * * @param string $field_string Format: "repeater_name:row_index:field_name" or "repeater_name:row_index:field_name:sub_field" * @return array|false Parsed components or false if invalid */ public function parseRepeaterFieldString(string $field_string): array|false { $parts = explode(':', $field_string); if (count($parts) < 3) { return false; } // Handle nested repeaters (4+ parts) if (count($parts) === 3) { return [ 'repeater' => $parts[0], 'row_index' => (int)$parts[1], 'field' => $parts[2], 'nested' => false ]; } elseif (count($parts) === 4) { // Nested repeater or sub-field return [ 'repeater' => $parts[0], 'row_index' => (int)$parts[1], 'field' => $parts[2], 'sub_field' => $parts[3], 'nested' => true ]; } return false; } protected function checkOverrides(string $name, mixed $sanitized, array $config): bool { do_action('jvb_meta_update', $name, $sanitized, $this->object_type); //check for overrides by field name or type $type = $config['type'] ?? false; $overrides = [ 'update_' . $name, 'update_' . $type ]; foreach ($overrides as $override) { $override = BASE . $override; if (function_exists($override)) { $override($this->object_id, $sanitized); return true; } $override = 'jvb_' . $override; if (function_exists($override)) { $override($this->object_id, $sanitized); return true; } } return false; } protected function getFields(): array { if (!empty($this->fields)) { return $this->fields; } $type = false; switch ($this->object_type) { case 'post': $type = get_post_type((int)$this->object_id); break; case 'term': $type = get_term((int)$this->object_id)->taxonomy; break; case 'user': $type = jvbUserRole((int)$this->object_id); break; case 'options': return jvbGetFields('options'); } if (!$type) { return []; } return jvbGetFields($type, $this->object_type); } protected function getObjectType(): string|false { switch ($this->object_type) { case 'post': $type = get_post_type((int)$this->object_id); break; case 'term': $type = get_term((int)$this->object_id)->taxonomy; break; case 'user': $type = jvbUserRole((int)$this->object_id); break; case 'options': $type = 'options'; break; default: return false; } return $type; } protected function getSections():array { $type = false; switch ($this->object_type) { case 'post': $type = get_post_type((int)$this->object_id); break; case 'term': $type = get_term((int)$this->object_id)->taxonomy; break; case 'user': $type = jvbUserRole((int)$this->object_id); break; case 'options': $type = 'options'; break; } if (!$type) { return []; } return jvbGetSections($type, $this->object_type); } protected function getRegistry():mixed { switch ($this->object_type) { case 'post': return JVB_CONTENT[jvbNoBase(get_post_type((int)$this->object_id))]??null; case 'term': $term = get_term((int)$this->object_id); return JVB_TAXONOMY[jvbNoBase($term->taxonomy)]??null; case 'user': return JVB_USER; } return null; } /** * @param string $message * @param array $context * @param string $level * * @return void */ protected function handleError(string $message, array $context = [], string $level = E_USER_WARNING):void { $class = get_class($this); $formatted = sprintf('[%s] %s', $class, $message); // Log to ErrorHandler if available JVB()->error()->log( 'meta_manager', $message, $context, 'error' ); if (defined('WP_DEBUG') && WP_DEBUG) { trigger_error($formatted, $level); } } public function setFieldConfig(array $fields):void { $this->fields = $fields; } protected function getFieldConfig(string $name):array|false { $fields = $this->getFields(); if (array_key_exists($name, $fields)) { return $fields[$name]; } // For nested fields $result = null; $found = false; array_walk_recursive($fields, function ($value, $k) use ($name, &$result, &$found) { if (!$found && $k === $name) { $result = $value; $found = true; } }); return $found ? $result : false; } public function render(string $type, string $name, array|null $config = null, bool $showHidden = false, $return = false, bool $hideEmpty = true):mixed { if (!apply_filters('jvbShouldRenderMeta', true, $name, $type, $this->getObjectType())) { return false; } if (!$config) { $config = $this->getFieldConfig($name); if (!$config) { return false; } } if (jvbCheck('hidden', $config) && !$showHidden) { return false; } if ($this->object_type === 'form') { $value = $this->getDefaultValue($config['type']); } else { try { $value =$this->getValue($name); } catch (Exception $e) { $value = $this->getDefaultValue($name); } } if ($config['type'] === 'location'){ $this->addMeta('has_map', true); } //test for form or frontend $out = ''; switch ($type) { case 'form': $out = $this->form->render($name, $value, $config, $showHidden, true); $out = apply_filters('jvbRenderFormMeta', $out, $name, $config, $value, $this->getObjectType()); break; case 'render': $out = $this->renderer->render($name, $value, $config, true); if (empty($out) && !$hideEmpty) { $out = $this->getEmptyTemplate($config['type'], $name); } $out = apply_filters('jvbRenderFrontendMeta', $out, $name, $config, $value, $this->getObjectType()); } if (!$return) { echo $out; } return $out; } public function renderForm(string $endpoint, array $options = [], array $fields = [], false|array $sections = [], bool $return = false):mixed { $ID = (array_key_exists('form-id', $options)) ? $options['form-id'] : $endpoint; ob_start(); $classes = (array_key_exists('classes', $options)) ? ' class="'.implode($options['classes']).'"' : ''; echo '
'; echo (array_key_exists('heading', $options)) ? '

'.$options['heading'].'

' : ''; if (array_key_exists('description', $options)) { if (is_array($options['description'])) { foreach ($options['description'] as $d) { echo '

'.$d.'

'; } } else { echo '

'.$options['description'].'

'; } } if (empty($fields)) { $fields = ($this->content) ? jvbGetFields($this->content, $this->object_type) : $this->getFields(); } if ($sections !== false && empty($sections)) { $sections = ($this->content) ? jvbGetSections($this->content, $this->object_type) : $this->getSections(); } if (!empty($sections)){ $tabs = []; foreach ($sections as $slug => $title) { $tabs[$slug] = [ 'title' => $title, 'content' => '', 'description' => jvbSectionDescription($slug)??'', ]; $icon = jvbSectionIcon($slug); if ($icon !== '') { $tabs[$slug]['icon'] = $icon; } } } else { $tabs = false; } $first = ['post_thumbnail', 'post_title', 'price']; foreach ($first as $f) { if (array_key_exists($f, $fields)) { if ($tabs) { $tabs['basic']['content'] .= $this->render('form', $f, $fields[$f], false, true); } else { $this->render('form', $f, $fields[$f]); } unset($fields[$f]); } } foreach ($fields as $n => $config) { if ($tabs) { $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic'; $tabs[$section]['content'] .= $this->render('form', $n, $config, false, true); } else { $this->render('form', $n, $config); } } if ($tabs) { jvbRenderTabs($tabs); } echo (jvbCheck('submit', $options)) ? '' : ''; echo '
'; $out = ob_get_clean(); if (!$return) { echo $out; } return $out; } function getEmptyTemplate($type, $name):string { $template = '