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); break; case 'term': $this->data = get_term($ID); break; case 'user': case 'integrations': $this->data = get_user($ID); break; case 'options': $this->baseKey = $ID; $this->data = null; 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': $key = $this->baseKey ? BASE . $this->baseKey . '_' . $name : BASE . $name; return get_option($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))) { $set = wp_set_post_terms($this->object_id, $sanitized, 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}"); } 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 '
'; $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 ''; } $out = apply_filters('jvbMetaTypeTemplate', $out, $type); return $out; } 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 []; } switch ($this->object_type) { case 'user': $check = $this->userFields; break; case 'term': $check = $this->termFields; break; case 'post': $check = $this->postFields; break; default: $check = []; } $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 { if (empty($fields) || !$this->object_type) { return false; } if ($this->object_type !== 'options' && !$this->object_id) { return false; } // Determine table based on object type switch ($this->object_type) { case 'post': $table = $this->wpdb->postmeta; $id_column = 'post_id'; $check = $this->postFields; break; case 'term': $table = $this->wpdb->termmeta; $id_column = 'term_id'; $check = $this->termFields; break; case 'user': case 'integrations': $table = $this->wpdb->usermeta; $id_column = 'user_id'; $check = $this->userFields; 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[array_search($f, $fields)]); } $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)) { return true; } if ($field_config['type'] === 'taxonomy' && !array_key_exists('taxonomy_type', $field_config)){ $term_ids = array_map('intval', explode(',', trim($sanitized))); $set = wp_set_post_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)) { 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 = wp_update_post(array_merge(['ID' => $this->object_id], $setFields), true); } 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; } } } 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; } } } }