<?php
|
namespace JVBase\meta;
|
|
use Exception;
|
use wpdb;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Handles persistence of meta values to WordPress
|
* Pure storage layer - no validation or sanitization
|
*/
|
class Storage
|
{
|
protected wpdb $wpdb;
|
|
public function __construct()
|
{
|
global $wpdb;
|
$this->wpdb = $wpdb;
|
}
|
|
/**
|
* Load a single field value from database
|
*/
|
public function get(Item $item, string $name): mixed
|
{
|
if ($item->isWpDefault($name)) {
|
return $this->getWpDefault($item, $name);
|
}
|
|
$metaKey = BASE . $name;
|
|
return match ($item->objectType) {
|
'post' => get_post_meta($item->id, $metaKey, true),
|
'term' => get_term_meta($item->id, $metaKey, true),
|
'user', 'integrations' => get_user_meta($item->id, $metaKey, true),
|
'options' => $this->getOption($item, $name),
|
default => ''
|
};
|
}
|
|
/**
|
* Load multiple field values in a single query
|
*/
|
public function getAll(Item $item, array $fieldNames): array
|
{
|
if (empty($fieldNames) || !$item->id) {
|
return [];
|
}
|
|
$defaults = Item::WP_DEFAULTS[$item->objectType] ?? [];
|
$wpFields = array_intersect($defaults, $fieldNames);
|
$metaFields = array_diff($fieldNames, $wpFields);
|
|
$values = [];
|
|
// Get meta fields in bulk
|
if (!empty($metaFields)) {
|
$values = $this->bulkGetMeta($item, $metaFields);
|
}
|
|
// Get WP default fields
|
foreach ($wpFields as $name) {
|
$values[$name] = $this->getWpDefault($item, $name);
|
}
|
|
return $values;
|
}
|
|
/**
|
* Save a single field
|
*/
|
public function saveField(Item $item, Field $field): bool
|
{
|
if ($field->isWpDefault()) {
|
return $this->saveWpDefault($item, $field);
|
}
|
|
if ($field->isTaxonomy()) {
|
return $this->saveTaxonomyField($item, $field);
|
}
|
|
$metaKey = BASE . $field->name;
|
|
return match ($item->objectType) {
|
'post' => update_post_meta($item->id, $metaKey, $field->value) !== false,
|
'term' => update_term_meta($item->id, $metaKey, $field->value) !== false,
|
'user', 'integrations' => update_user_meta($item->id, $metaKey, $field->value) !== false,
|
'options' => $this->saveOption($item, $field),
|
default => false
|
};
|
}
|
|
/**
|
* Save all dirty fields on an item
|
*/
|
public function save(Item $item, bool $updateTimestamp = true): bool
|
{
|
$dirty = $item->getDirtyFields();
|
|
if (empty($dirty)) {
|
return true;
|
}
|
|
$this->wpdb->query('START TRANSACTION');
|
|
try {
|
foreach ($dirty as $field) {
|
if (!$this->saveField($item, $field)) {
|
throw new Exception("Failed to save field: {$field->name}");
|
}
|
$field->markClean();
|
}
|
|
$this->wpdb->query('COMMIT');
|
|
// Update post modified timestamp
|
if ($updateTimestamp && $item->objectType === 'post' && $item->id) {
|
wp_update_post(['ID' => $item->id]);
|
}
|
|
$this->clearCache($item);
|
|
return true;
|
|
} catch (Exception $e) {
|
$this->wpdb->query('ROLLBACK');
|
JVB()->error()->log('meta_storage', $e->getMessage(), [
|
'item_id' => $item->id,
|
'object_type' => $item->objectType,
|
'fields' => array_keys($dirty)
|
], 'error');
|
return false;
|
}
|
}
|
|
/**
|
* Delete a field value
|
*/
|
public function delete(Item $item, string $name): bool
|
{
|
$metaKey = BASE . $name;
|
|
return match ($item->objectType) {
|
'post' => delete_post_meta($item->id, $metaKey),
|
'term' => delete_term_meta($item->id, $metaKey),
|
'user', 'integrations' => delete_user_meta($item->id, $metaKey),
|
'options' => delete_option($this->optionKey($item, $name)),
|
default => false
|
};
|
}
|
|
// ─────────────────────────────────────────────────────────────
|
// Protected helpers
|
// ─────────────────────────────────────────────────────────────
|
|
protected function getWpDefault(Item $item, string $name): mixed
|
{
|
if (in_array($name, ['featured_image', 'post_thumbnail'])) {
|
return get_post_thumbnail_id($item->id);
|
}
|
|
return match ($item->objectType) {
|
'post' => $this->getPostField($item, $name),
|
'term' => $this->getTermField($item, $name),
|
'user' => $this->getUserField($item, $name),
|
default => ''
|
};
|
}
|
|
protected function getPostField(Item $item, string $name): mixed
|
{
|
return match ($name) {
|
'post_title' => get_the_title($item->id),
|
'post_excerpt' => get_the_excerpt($item->id),
|
'post_content' => get_post_field('post_content', $item->id),
|
default => $item->wpObject->$name ?? ''
|
};
|
}
|
|
protected function getTermField(Item $item, string $name): mixed
|
{
|
return match ($name) {
|
'term_name' => get_term_field('name', $item->id),
|
'description' => get_term_field('description', $item->id),
|
default => ''
|
};
|
}
|
|
protected function getUserField(Item $item, string $name): mixed
|
{
|
return match ($name) {
|
'display_name' => get_the_author_meta('display_name', $item->id),
|
'user_email' => get_the_author_meta('user_email', $item->id),
|
'first_name' => get_the_author_meta('first_name', $item->id),
|
'last_name' => get_the_author_meta('last_name', $item->id),
|
default => $item->wpObject->$name ?? ''
|
};
|
}
|
|
protected function saveWpDefault(Item $item, Field $field): bool
|
{
|
$name = $field->name;
|
$value = $field->value;
|
|
if (in_array($name, ['featured_image', 'post_thumbnail'])) {
|
return set_post_thumbnail($item->id, $value) !== false;
|
}
|
|
return match ($item->objectType) {
|
'post' => wp_update_post(['ID' => $item->id, $name => $value]) !== 0,
|
'term' => !is_wp_error(wp_update_term($item->id, $item->wpObject->taxonomy, [
|
$name => $value,
|
'slug' => $name === 'term_name' ? sanitize_title($value) : null
|
])),
|
'user' => wp_update_user(['ID' => $item->id, $name => $value]) !== 0,
|
default => false
|
};
|
}
|
|
protected function saveTaxonomyField(Item $item, Field $field): bool
|
{
|
$taxonomy = jvbCheckBase($field->config['taxonomy']);
|
$value = $field->value;
|
|
if (empty(trim($value))) {
|
wp_set_object_terms($item->id, [], $taxonomy, false);
|
return true;
|
}
|
|
$termIds = array_map('intval', array_filter(explode(',', $value)));
|
$result = wp_set_object_terms($item->id, $termIds, $taxonomy, false);
|
|
return !is_wp_error($result);
|
}
|
|
protected function bulkGetMeta(Item $item, array $fields): array
|
{
|
[$table, $idColumn] = $this->getTableInfo($item->objectType);
|
|
if (!$table) {
|
return [];
|
}
|
|
$metaKeys = array_map(fn($f) => BASE . $f, $fields);
|
$placeholders = implode(',', array_fill(0, count($metaKeys), '%s'));
|
|
$query = $this->wpdb->prepare(
|
"SELECT meta_key, meta_value FROM {$table}
|
WHERE {$idColumn} = %d AND meta_key IN ({$placeholders})",
|
array_merge([$item->id], $metaKeys)
|
);
|
|
$results = $this->wpdb->get_results($query, ARRAY_A);
|
|
$values = array_fill_keys($fields, '');
|
|
foreach ($results as $row) {
|
$key = str_replace(BASE, '', $row['meta_key']);
|
$values[$key] = maybe_unserialize($row['meta_value']);
|
}
|
|
return $values;
|
}
|
|
protected function getTableInfo(string $objectType): array
|
{
|
return match ($objectType) {
|
'post' => [$this->wpdb->postmeta, 'post_id'],
|
'term' => [$this->wpdb->termmeta, 'term_id'],
|
'user', 'integrations' => [$this->wpdb->usermeta, 'user_id'],
|
default => [null, null]
|
};
|
}
|
|
protected function getOption(Item $item, string $name): mixed
|
{
|
return get_option($this->optionKey($item, $name));
|
}
|
|
protected function saveOption(Item $item, Field $field): bool
|
{
|
return update_option($this->optionKey($item, $field->name), $field->value);
|
}
|
|
protected function optionKey(Item $item, string $name): string
|
{
|
return $item->baseKey
|
? BASE . $item->baseKey . '_' . $name
|
: BASE . $name;
|
}
|
|
protected function clearCache(Item $item): void
|
{
|
match ($item->objectType) {
|
'post' => clean_post_cache($item->id),
|
'term' => clean_term_cache($item->id),
|
'user', 'integrations' => clean_user_cache($item->id),
|
default => null
|
};
|
}
|
}
|