[ 'post_title', 'post_excerpt', 'post_content', 'post_date', 'post_status', 'post_modified', 'post_thumbnail', 'menu_order' ], 'user' => [ 'first_name', 'last_name', 'display_name', 'description', 'user_email', ], 'term' => [ 'term_name', 'description' ] ]; public function __construct(int|string|null $ID = null, ?string $type = null, ?string $content = null) { global $wpdb; $this->wpdb = $wpdb; $this->object_id = is_int($ID) ? $ID : null; $this->object_type = $type; if ($ID) { switch ($type) { case 'post': $this->data = get_post((string)$ID); $this->content = jvbNoBase($this->data->post_type); $registrar = Registrar::getInstance($this->content); $this->isTimeline = $registrar && $registrar->hasFeature('is_timeline'); break; case 'term': $this->data = get_term($ID); $this->content = jvbNoBase($this->data->taxonomy); break; case 'user': case 'integrations': $this->data = get_user($ID); $this->content = jvbUserRole($ID); break; case 'options': $this->baseKey = $ID; $this->data = null; break; default: $this->data = null; break; } } $this->type_manager = new MetaTypeManager(); $this->validator = new Validator(); $this->sanitizer = new Sanitizer(); $this->renderer = new Render(); } /** * @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 if (array_key_exists($this->object_type, $this->wpDefaults)) { $defaults = $this->wpDefaults[$this->object_type]; if (in_array($name, $defaults)) { if (in_array($name, ['featured_image', 'post_thumbnail'])) { return get_post_thumbnail_id($this->object_id); } return match ($this->object_type) { 'term' => $this->getTermField($name), 'post' => $this->getPostField($name), 'user' => $this->getUserField($name), default => '' }; } } $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': $key = $this->baseKey ? BASE . $this->baseKey . '_' . $name : BASE . $name; return get_option($key); default: return ''; } } protected function getTermField(string $name): mixed { // WordPress handles entity decoding and filters return match ($name) { 'term_name' => get_term_field('name', $this->object_id), 'description' => get_term_field('description', $this->object_id), default => '' }; } protected function getPostField(string $name): mixed { return match ($name) { 'post_title' => get_the_title($this->object_id), 'post_excerpt' => get_the_excerpt($this->object_id), 'post_content' => get_post_field('post_content', $this->object_id), default => $this->data->$name ?? '' }; } protected function getUserField(string $name): mixed { return match ($name) { 'display_name' => get_the_author_meta('display_name', $this->object_id), 'user_email' => get_the_author_meta('user_email', $this->object_id), 'first_name' => get_the_author_meta('first_name', $this->object_id), 'last_name' => get_the_author_meta('last_name', $this->object_id), default => $this->data->$name ?? '' }; } /** * @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 $updatePost = true): 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)) { 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; } if (array_key_exists($this->object_type, $this->wpDefaults)) { $check = $this->wpDefaults[$this->object_type]; if (in_array($name, $check)) { $ID = true; if (in_array($name, ['featured_image', 'post_thumbnail'])) { $old = get_post_thumbnail_id($this->object_id); if ($old !== $sanitized) { $ID = set_post_thumbnail($this->object_id, $sanitized); } return $ID !== false; } $old = $this->data->$name; if ($old !== $sanitized) { switch ($this->object_type) { case 'post': $ID = jvb_update_post([ 'ID' => $this->object_id, $name => $sanitized ]); break; case 'term': $data = [$name => $sanitized]; if ($name === 'term_name') { $data['slug'] = sanitize_title($sanitized); } $ID = wp_update_term( $this->data->term_id, $this->data->taxonomy, $data ); break; case 'user': $ID = wp_update_user([ 'ID' => $this->object_id, $name => $sanitized ]); if ($name === 'display_name') { $link = get_user_meta($this->object_id, BASE.'link', true); if ($link !== '') { jvb_update_post([ 'ID' => $link, 'post_title' => $sanitized ]); } } break; } } return $ID !== false; } } if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) { if (empty(trim($sanitized))) { // Clear all terms when value is empty wp_set_object_terms($this->object_id, [], jvbCheckBase($field_config['taxonomy']), false); } else { $term_ids = array_map('intval', array_filter(explode(',', $sanitized))); wp_set_object_terms($this->object_id, $term_ids, jvbCheckBase($field_config['taxonomy']), false); } } 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': $key = $this->baseKey ? BASE . $this->baseKey . '_' . $name : BASE . $name; return update_option($key, $sanitized); } if ($result === false) { throw new Exception("Failed to update meta value for {$name}"); } if ($updatePost && $this->object_type === 'post') { //Flush the cache for this post. jvb_update_post([ 'ID' => $this->object_id, ]); } 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 Registrar::getFieldsFor('options'); } if (!$type) { return []; } $this->fields = Registrar::getFieldsFor($type); return $this->fields; } 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 Registrar::getInstance($type)->getSections()??[]; } /** * @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 = Form::render($name, $value, $config); $out = apply_filters('jvbRenderFormMeta', $out, $name, $config, $value, $this->getObjectType()); break; case 'render': $out = $this->renderer->render($name, $value, $config); 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 '
'; $out = ob_get_clean(); if (!$return) { echo $out; } return $out; } function getEmptyTemplate($type, $name):string { $template = ''; $out = ''; switch ($type) { case 'text': case 'textarea': case 'number': $out = ''; break; case 'url': case 'email': $out = ''.jvbIcon('link').''; break; case 'set': case 'checkbox': case 'radio': case 'taxonomy': case 'user': $out = ''.jvbIcon('calendar').'
'; break; case 'time': $out = ''.jvbIcon('clock').'
'; break; case 'true_false': $out = ''; break; default: return ''; } return apply_filters('jvbMetaTypeTemplate', $out, $type); } public function getDefaultValue(string $type):mixed { return match ($this->type_manager->getMetaType($type)) { 'object', 'array' => [], 'boolean' => false, 'integer' => 0, default => '', }; } /******************************************************************* * * BULK SUPPORT * ******************************************************************/ public function getAll(array $fields = []) :array { $fields = (empty($fields) || $fields[0] === 'all') ? array_keys($this->getFields()) : $fields; if (empty($fields) || !$this->object_id || !$this->object_type) { return []; } $check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : []; $setFields = array_intersect($check, $fields); foreach ($setFields as $f) { unset($fields[array_search($f, $fields)]); } // Prepare meta keys with BASE prefix $meta_keys = array_map(function($field) { return BASE . $field; }, $fields); if (!empty($fields)) { // Build placeholders for IN clause $placeholders = implode(',', array_fill(0, count($meta_keys), '%s')); // Determine table based on object type switch ($this->object_type) { case 'post': $table = $this->wpdb->postmeta; $id_column = 'post_id'; break; case 'term': $table = $this->wpdb->termmeta; $id_column = 'term_id'; break; case 'user': $table = $this->wpdb->usermeta; $id_column = 'user_id'; break; default: return []; } // Prepare and execute query $query = $this->wpdb->prepare( "SELECT meta_key, meta_value FROM {$table} WHERE {$id_column} = %d AND meta_key IN ({$placeholders})", array_merge([$this->object_id], $meta_keys) ); $results = $this->wpdb->get_results($query, ARRAY_A); // Format results, removing BASE prefix from keys $values = []; foreach ($results as $row) { $key = str_replace(BASE, '', $row['meta_key']); $values[$key] = maybe_unserialize($row['meta_value']); } // Include any requested fields that don't have values as empty foreach ($fields as $field) { if (!isset($values[$field])) { $values[$field] = ''; } } } if (!empty($setFields)) { foreach ($setFields as $field) { if ($field === 'post_thumbnail') { $values[$field] = get_post_thumbnail_id($this->object_id); } else { $values[$field] = $this->data->$field; } } } return $values; } protected function addMeta($field, $value): bool { switch ($this->object_type) { case 'post': return update_post_meta($this->object_id, BASE.$field, $value); case 'term': return update_term_meta($this->object_id, BASE.$field, $value); case 'user': case 'integrations': return update_user_meta($this->object_id, BASE.$field, $value); case 'option': return update_option(BASE.$field, $value); } return false; } public function setAll(array $fields, bool $updatePost = true):bool { if (empty($fields) || !$this->object_type) { return false; } if ($this->object_type !== 'options' && !$this->object_id) { return false; } // Determine table based on object type $check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : []; switch ($this->object_type) { case 'post': $table = $this->wpdb->postmeta; $id_column = 'post_id'; break; case 'term': $table = $this->wpdb->termmeta; $id_column = 'term_id'; break; case 'user': case 'integrations': $table = $this->wpdb->usermeta; $id_column = 'user_id'; break; case 'options': try { $results = []; foreach ($fields as $field => $value) { // Get field configuration for sanitization $field_config = $this->getFieldConfig($field); // Sanitize value $sanitized = $this->sanitizer->sanitize($value, $field_config); if ($this->checkOverrides($field, $sanitized, $field_config)) { continue; } $results[] = update_option(BASE.$field, $sanitized); } return true; } catch (Exception $e) { return false; } default: return false; } $setFields = array_intersect($check, array_keys($fields)); $temp = []; foreach ($setFields as $f) { $temp[$f] = $fields[$f]; unset($fields[$f]); } $setFields = $temp; $success = true; $this->wpdb->query('START TRANSACTION'); try { if (!empty($fields)) { foreach ($fields as $field => $value) { // Get field configuration for sanitization $field_config = $this->getFieldConfig($field); // Sanitize value $sanitized = $this->sanitizer->sanitize($value, $field_config); if ($this->checkOverrides($field, $sanitized, $field_config)) { continue; } if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) { if (empty(trim($sanitized))) { // Clear all terms when value is empty wp_set_object_terms($this->object_id, [], jvbCheckBase($field_config['taxonomy']), false); } else { $term_ids = array_map('intval', array_filter(explode(',', $sanitized))); wp_set_object_terms($this->object_id, $term_ids, jvbCheckBase($field_config['taxonomy']), false); } } if ($field_config['type'] === 'location' && empty($sanitized)) { $this->addMeta('has_map', false); } $meta_key = BASE . $field; // Check if meta exists $exists = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$id_column} = %d AND meta_key = %s", $this->object_id, $meta_key )); if ($exists) { // Update existing $result = $this->wpdb->update( $table, ['meta_value' => maybe_serialize($sanitized)], [ $id_column => $this->object_id, 'meta_key' => $meta_key ], ['%s'], ['%d', '%s'] ); } else { // Insert new $result = $this->wpdb->insert( $table, [ $id_column => $this->object_id, 'meta_key' => $meta_key, 'meta_value' => maybe_serialize($sanitized) ], ['%d', '%s', '%s'] ); } if ($result === false) { $success = false; break; } } if ($success) { $this->wpdb->query('COMMIT'); // Clear cache for this object $this->clearMetaCache(); } else { $this->wpdb->query('ROLLBACK'); } } if (!empty($setFields)) { foreach ($setFields as $field => $value) { $field_config = $this->getFieldConfig($field); if ($field_config) { $setFields[$field] = $this->sanitizer->sanitize($value, $field_config); } if ($field === 'post_date') { $datetime = strtotime($setFields[$field]); if ($datetime !== false) { $setFields[$field] = date('Y-m-d H:i:s', $datetime); } else { $setFields[$field] = date('Y-m-d H:i:s', time()); } $setFields['post_date_gmt'] = get_gmt_from_date($setFields[$field]); $setFields['edit_date'] = true; } } switch ($this->object_type) { case 'post': if (array_key_exists('post_thumbnail', $setFields)) { set_post_thumbnail($this->object_id, $setFields['post_thumbnail']); unset($setFields['post_thumbnail']); } if (!empty($setFields)) { $result = jvb_update_post(array_merge(['ID' => $this->object_id], $setFields)); } break; case 'user': case 'integrations': wp_update_user(array_merge(['ID' => $this->object_id], $setFields)); break; case 'term': wp_update_term($this->object_id, $this->data->taxonomy, $setFields); break; } } elseif ($updatePost && $this->object_type === 'post' && !empty($this->object_id)) { //Update the 'post modified' date with meta updates, for filtering jvb_update_post(['ID' => $this->object_id]); } } catch (Exception $e) { $this->wpdb->query('ROLLBACK'); JVB()->error()->log( 'meta_manager', 'Batch update failed: ' . $e->getMessage(), [ 'object_id' => $this->object_id, 'object_type' => $this->object_type, 'fields' => array_keys($fields) ], 'error' ); return false; } return $success; } /** * Get multiple field values for multiple objects * * @param array $object_ids Array of object IDs * @param array $fields Array of field names (without BASE prefix) * @param string $object_type Type of objects (post, term, user) * @return array Multi-dimensional array [object_id][field] => value */ public static function getBulkValues(array $object_ids, array $fields, string $object_type): array { if (empty($object_ids) || empty($fields)) { return []; } global $wpdb; // Prepare meta keys with BASE prefix $meta_keys = array_map(function($field) { return BASE . $field; }, $fields); // Build placeholders $id_placeholders = implode(',', array_fill(0, count($object_ids), '%d')); $key_placeholders = implode(',', array_fill(0, count($meta_keys), '%s')); // Determine table based on object type switch ($object_type) { case 'post': $table = $wpdb->postmeta; $id_column = 'post_id'; break; case 'term': $table = $wpdb->termmeta; $id_column = 'term_id'; break; case 'user': case 'integrations': $table = $wpdb->usermeta; $id_column = 'user_id'; break; default: return []; } // Prepare and execute query $query = $wpdb->prepare( "SELECT {$id_column} as object_id, meta_key, meta_value FROM {$table} WHERE {$id_column} IN ({$id_placeholders}) AND meta_key IN ({$key_placeholders}) ORDER BY {$id_column}, meta_key", array_merge($object_ids, $meta_keys) ); $results = $wpdb->get_results($query, ARRAY_A); // Format results $values = []; foreach ($object_ids as $id) { $values[$id] = []; foreach ($fields as $field) { $values[$id][$field] = ''; } } foreach ($results as $row) { $object_id = (int)$row['object_id']; $key = str_replace(BASE, '', $row['meta_key']); $values[$object_id][$key] = maybe_unserialize($row['meta_value']); } return $values; } /** * Set multiple field values for multiple objects * * @param array $data Multi-dimensional array [object_id][field] => value * @param string $object_type Type of objects (post, term, user) * @return array Array of results [object_id] => bool success */ public static function setBulkValues(array $data, string $object_type): array { if (empty($data)) { return []; } global $wpdb; // Determine table based on object type switch ($object_type) { case 'post': $table = $wpdb->postmeta; $id_column = 'post_id'; break; case 'term': $table = $wpdb->termmeta; $id_column = 'term_id'; break; case 'user': case 'integrations': $table = $wpdb->usermeta; $id_column = 'user_id'; break; default: return []; } $results = []; $wpdb->query('START TRANSACTION'); try { // Collect all meta keys to check existence in one query $all_checks = []; foreach ($data as $object_id => $fields) { foreach ($fields as $field => $value) { $all_checks[] = [ 'object_id' => $object_id, 'meta_key' => BASE . $field ]; } } // Build query to check existing meta if (!empty($all_checks)) { $check_values = []; foreach ($all_checks as $check) { $check_values[] = $wpdb->prepare("(%d, %s)", $check['object_id'], $check['meta_key'] ); } $existing_query = "SELECT {$id_column} as object_id, meta_key FROM {$table} WHERE ({$id_column}, meta_key) IN (" . implode(',', $check_values) . ")"; $existing = $wpdb->get_results($existing_query, ARRAY_A); // Create lookup for existing meta $exists_lookup = []; foreach ($existing as $row) { $exists_lookup[$row['object_id'] . '_' . $row['meta_key']] = true; } } $object_ids = []; // Process each object foreach ($data as $object_id => $fields) { $object_ids[] = $object_id; $object_success = true; // Create temporary MetaManager instance for sanitization $temp_meta = new self($object_id, $object_type); foreach ($fields as $field => $value) { // Get field configuration $field_config = $temp_meta->getFieldConfig($field); // Sanitize value $sanitized = $temp_meta->sanitizer->sanitize($value, $field_config); $temp = new self(null, $object_type); if ($temp->checkOverrides($field, $sanitized, $field_config)) { $results[$object_id] = true; continue; } $meta_key = BASE . $field; $lookup_key = $object_id . '_' . $meta_key; if (isset($exists_lookup[$lookup_key])) { // Update existing $result = $wpdb->update( $table, ['meta_value' => maybe_serialize($sanitized)], [ $id_column => $object_id, 'meta_key' => $meta_key ], ['%s'], ['%d', '%s'] ); } else { // Insert new $result = $wpdb->insert( $table, [ $id_column => $object_id, 'meta_key' => $meta_key, 'meta_value' => maybe_serialize($sanitized) ], ['%d', '%s', '%s'] ); } if ($result === false) { $object_success = false; } } $results[$object_id] = $object_success; } // Check if all succeeded $all_success = !in_array(false, $results, true); if ($all_success) { $wpdb->query('COMMIT'); // Clear cache for all affected objects self::clearBulkMetaCache($object_ids, $object_type); } else { $wpdb->query('ROLLBACK'); } } catch (Exception $e) { $wpdb->query('ROLLBACK'); JVB()->error()->log( 'meta_manager', 'Bulk update failed: ' . $e->getMessage(), ['object_type' => $object_type], 'error' ); // Mark all as failed foreach (array_keys($data) as $object_id) { $results[$object_id] = false; } } return $results; } /** * Clear meta cache for current object */ protected function clearMetaCache(): void { switch ($this->object_type) { case 'post': clean_post_cache($this->object_id); break; case 'term': clean_term_cache($this->object_id); break; case 'user': case 'integrations': clean_user_cache($this->object_id); break; } } /** * Clear meta cache for multiple objects */ protected static function clearBulkMetaCache(array $object_ids, string $object_type): void { foreach ($object_ids as $id) { switch ($object_type) { case 'post': clean_post_cache($id); break; case 'term': clean_term_cache($id); break; case 'user': case 'integrations': clean_user_cache($id); break; } } } private function getOrCreateTerm(string $termName, string $taxonomy):?int { $taxonomy = jvbCheckBase($taxonomy); $term = get_term_by('name', $termName, $taxonomy); if (!$term) { $result = wp_insert_term($termName, $taxonomy); if (is_wp_error($result)) { return null; } $termID = $result['term_id']; } else { $termID = $term->term_id; } if ($termID) { return $termID; } return null; } }