<?php
|
namespace JVBase\meta;
|
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Core meta management class
|
*/
|
class MetaManager
|
{
|
public MetaTypeManager $type_manager;
|
public MetaValidator $validator;
|
public MetaSanitizer $sanitizer;
|
public MetaRenderer $renderer;
|
public MetaForm $form;
|
protected int|null $object_id;
|
public object|null $data;
|
protected string $field;
|
protected mixed $value;
|
protected string|null $object_type;
|
protected int $max_file_size = 5242880;
|
protected ?string $content = null;
|
protected \wpdb $wpdb;
|
protected array $postFields = [
|
'post_title',
|
'post_excerpt',
|
'post_content',
|
'post_date',
|
'post_status',
|
'post_modified',
|
'post_thumbnail'
|
];
|
|
protected array $userFields = [
|
'first_name',
|
'last_name',
|
'description',
|
'display_name',
|
'user_email',
|
];
|
|
protected array $termFields = [
|
'term_name',
|
'description'
|
];
|
|
public function __construct(?int $ID = null, ?string $type = null, ?string $content = null)
|
{
|
global $wpdb;
|
$this->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
|
{
|
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');
|
}
|
|
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
|
{
|
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);
|
}
|
}
|
|
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 '<form id ="'.$endpoint.'"'.$classes.' data-save="'.$endpoint.'" data-form-id="'.$ID.'">';
|
echo (array_key_exists('heading', $options)) ? '<h2>'.$options['heading'].'</h2>' : '';
|
if (array_key_exists('description', $options)) {
|
if (is_array($options['description'])) {
|
foreach ($options['description'] as $d) {
|
echo '<p>'.$d.'</p>';
|
}
|
} else {
|
echo '<p>'.$options['description'].'</p>';
|
}
|
|
}
|
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)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : '';
|
echo '</form>';
|
|
$out = ob_get_clean();
|
if (!$return) {
|
echo $out;
|
}
|
return $out;
|
|
}
|
|
|
function getEmptyTemplate($type, $name):string
|
{
|
$template = '<template class="'.$type.'Template">';
|
$out = '';
|
switch ($type) {
|
case 'text':
|
case 'textarea':
|
case 'number':
|
$out = '<p class="'.$name.'"></p>';
|
break;
|
case 'url':
|
case 'email':
|
$out = '<a class="'.$name.'">'.jvbIcon('link').'</a>';
|
break;
|
case 'set':
|
case 'checkbox':
|
case 'radio':
|
case 'taxonomy':
|
case 'user':
|
$out = '<h4 class="'.$name.' inline"><ul><li></li></ul>';
|
break;
|
case 'image':
|
case 'gallery':
|
$out = '<div class="'.$name.' images"><img/></div>';
|
break;
|
case 'repeater':
|
//Repeater should loop through each of its fields
|
break;
|
case 'date':
|
$out = '<p class="'.$name.'">'.jvbIcon('calendar').'<span></span></p>';
|
break;
|
case 'time':
|
$out = '<p class="'.$name.'">'.jvbIcon('time').'<time></time></p>';
|
break;
|
case 'true_false':
|
$out = '<p class="'.$name.'"></p>';
|
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)){
|
$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);
|
}
|
|
$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;
|
}
|
}
|
}
|
}
|