From 2127b1bdd73ecd2423e443992da4b442f5a3c1a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 04 Feb 2026 21:19:25 +0000
Subject: [PATCH] =Major overhaul of MetaManager.php -> Meta.php and RestRouteManager.php -> Rest.php. Seems to work for JakeVan
---
inc/meta/Storage.php | 401 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 390 insertions(+), 11 deletions(-)
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
index b2ea8ac..30160a9 100644
--- a/inc/meta/Storage.php
+++ b/inc/meta/Storage.php
@@ -2,7 +2,6 @@
namespace JVBase\meta;
use Exception;
-use wpdb;
if (!defined('ABSPATH')) {
exit;
@@ -14,7 +13,7 @@
*/
class Storage
{
- protected wpdb $wpdb;
+ protected \wpdb $wpdb;
public function __construct()
{
@@ -22,6 +21,10 @@
$this->wpdb = $wpdb;
}
+ // ─────────────────────────────────────────────────────────────
+ // Single Item Operations
+ // ─────────────────────────────────────────────────────────────
+
/**
* Load a single field value from database
*/
@@ -43,11 +46,11 @@
}
/**
- * Load multiple field values in a single query
+ * Load multiple field values for single item
*/
public function getAll(Item $item, array $fieldNames): array
{
- if (empty($fieldNames) || !$item->id) {
+ if (empty($fieldNames) || (!$item->id && $item->objectType !== 'options')) {
return [];
}
@@ -57,7 +60,7 @@
$values = [];
- // Get meta fields in bulk
+ // Get meta fields in bulk query
if (!empty($metaFields)) {
$values = $this->bulkGetMeta($item, $metaFields);
}
@@ -95,7 +98,7 @@
}
/**
- * Save all dirty fields on an item
+ * Save all dirty fields on a single item
*/
public function save(Item $item, bool $updateTimestamp = true): bool
{
@@ -142,6 +145,14 @@
*/
public function delete(Item $item, string $name): bool
{
+ // Handle taxonomy fields
+ $config = $item->getFieldConfig($name);
+ if ($config && ($config['type'] ?? '') === 'taxonomy' && !isset($config['taxonomy_type'])) {
+ $taxonomy = jvbCheckBase($config['taxonomy']);
+ wp_set_object_terms($item->id, [], $taxonomy, false);
+ return true;
+ }
+
$metaKey = BASE . $name;
return match ($item->objectType) {
@@ -154,7 +165,215 @@
}
// ─────────────────────────────────────────────────────────────
- // Protected helpers
+ // Bulk Operations
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Save multiple Meta instances in optimized transaction
+ * @param Meta[] $metas Array of Meta instances
+ * @return array<int, bool> Results keyed by item ID
+ */
+ public static function saveBulk(array $metas, bool $updateTimestamp = true): array
+ {
+ global $wpdb;
+
+ $results = [];
+ $postIdsToUpdate = [];
+
+ $wpdb->query('START TRANSACTION');
+
+ try {
+ // Group by object type for efficient processing
+ $grouped = [];
+ foreach ($metas as $meta) {
+ $item = $meta->item();
+ $type = $item->objectType;
+ $grouped[$type][] = ['meta' => $meta, 'item' => $item];
+ }
+
+ foreach ($grouped as $objectType => $group) {
+ $storage = new self();
+ [$table, $idColumn] = $storage->getTableInfo($objectType);
+
+ if (!$table && $objectType !== 'options') {
+ continue;
+ }
+
+ // Collect all operations
+ $metaInserts = [];
+ $wpDefaultUpdates = [];
+ $taxonomyUpdates = [];
+ $optionUpdates = [];
+
+ foreach ($group as $entry) {
+ $item = $entry['item'];
+ $dirty = $item->getDirtyFields();
+
+ if (empty($dirty)) {
+ $results[$item->id] = true;
+ continue;
+ }
+
+ foreach ($dirty as $field) {
+ if ($objectType === 'options') {
+ $optionUpdates[] = [
+ 'key' => $storage->optionKey($item, $field->name),
+ 'value' => $field->value
+ ];
+ } elseif ($field->isWpDefault()) {
+ $wpDefaultUpdates[$item->id][$field->name] = $field->value;
+ } elseif ($field->isTaxonomy()) {
+ $taxonomyUpdates[] = [
+ 'object_id' => $item->id,
+ 'taxonomy' => jvbCheckBase($field->config['taxonomy']),
+ 'value' => $field->value
+ ];
+ } else {
+ $metaInserts[] = [
+ 'id' => $item->id,
+ 'key' => BASE . $field->name,
+ 'value' => maybe_serialize($field->value)
+ ];
+ }
+ }
+
+ if ($updateTimestamp && $objectType === 'post') {
+ $postIdsToUpdate[] = $item->id;
+ }
+ }
+
+ // Execute bulk operations
+ if (!empty($metaInserts)) {
+ self::bulkUpsertMeta($table, $idColumn, $metaInserts);
+ }
+
+ if (!empty($wpDefaultUpdates)) {
+ self::batchUpdateWpDefaults($objectType, $wpDefaultUpdates);
+ }
+
+ if (!empty($taxonomyUpdates)) {
+ self::batchUpdateTaxonomies($taxonomyUpdates);
+ }
+
+ if (!empty($optionUpdates)) {
+ self::batchUpdateOptions($optionUpdates);
+ }
+
+ // Mark all fields clean
+ foreach ($group as $entry) {
+ $entry['item']->markAllClean();
+ $results[$entry['item']->id ?? 'options'] = true;
+ }
+ }
+
+ $wpdb->query('COMMIT');
+
+ // Update post timestamps in single query
+ if (!empty($postIdsToUpdate)) {
+ self::batchTouchPosts(array_unique($postIdsToUpdate));
+ }
+
+ // Clear caches
+ foreach ($metas as $meta) {
+ (new self())->clearCache($meta->item());
+ }
+
+ return $results;
+
+ } catch (Exception $e) {
+ $wpdb->query('ROLLBACK');
+
+ foreach ($metas as $meta) {
+ $results[$meta->item()->id ?? 'options'] = false;
+ }
+
+ JVB()->error()->log('meta_storage', 'Bulk save failed: ' . $e->getMessage(), [], 'error');
+
+ return $results;
+ }
+ }
+
+ /**
+ * Bulk load meta for multiple items
+ * @param array $ids Object IDs
+ * @param string $objectType post, term, user
+ * @param array $fields Field names to load
+ * @return array<int, array<string, mixed>> Values keyed by ID then field name
+ */
+ public static function getBulkValues(array $ids, string $objectType, array $fields): array
+ {
+ if (empty($ids) || empty($fields)) {
+ return [];
+ }
+
+ global $wpdb;
+ $storage = new self();
+
+ [$table, $idColumn] = $storage->getTableInfo($objectType);
+
+ if (!$table) {
+ return [];
+ }
+
+ // Separate WP defaults from custom meta
+ $defaults = Item::WP_DEFAULTS[$objectType] ?? [];
+ $wpFields = array_intersect($defaults, $fields);
+ $metaFields = array_diff($fields, $wpFields);
+
+ // Initialize results
+ $values = [];
+ foreach ($ids as $id) {
+ $values[$id] = array_fill_keys($fields, '');
+ }
+
+ // Bulk get custom meta
+ if (!empty($metaFields)) {
+ $metaKeys = array_map(fn($f) => BASE . $f, $metaFields);
+
+ $idPlaceholders = implode(',', array_fill(0, count($ids), '%d'));
+ $keyPlaceholders = implode(',', array_fill(0, count($metaKeys), '%s'));
+
+ $query = $wpdb->prepare(
+ "SELECT {$idColumn} as object_id, meta_key, meta_value
+ FROM {$table}
+ WHERE {$idColumn} IN ({$idPlaceholders})
+ AND meta_key IN ({$keyPlaceholders})",
+ array_merge($ids, $metaKeys)
+ );
+
+ $results = $wpdb->get_results($query, ARRAY_A);
+
+ foreach ($results as $row) {
+ $objectId = (int)$row['object_id'];
+ $fieldName = str_replace(BASE, '', $row['meta_key']);
+ $values[$objectId][$fieldName] = maybe_unserialize($row['meta_value']);
+ }
+ }
+
+ // Get WP default fields (requires individual lookups unfortunately)
+ if (!empty($wpFields)) {
+ foreach ($ids as $id) {
+ $tempItem = new Item($id, $objectType);
+
+ // Load WP object for defaults
+ $tempItem->wpObject = match ($objectType) {
+ 'post' => get_post($id),
+ 'term' => get_term($id),
+ 'user' => get_user_by('id', $id),
+ default => null
+ };
+
+ foreach ($wpFields as $field) {
+ $values[$id][$field] = $storage->getWpDefault($tempItem, $field);
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ // ─────────────────────────────────────────────────────────────
+ // Protected Helpers - Single Item
// ─────────────────────────────────────────────────────────────
protected function getWpDefault(Item $item, string $name): mixed
@@ -226,7 +445,7 @@
$taxonomy = jvbCheckBase($field->config['taxonomy']);
$value = $field->value;
- if (empty(trim($value))) {
+ if (empty(trim((string)$value))) {
wp_set_object_terms($item->id, [], $taxonomy, false);
return true;
}
@@ -266,7 +485,7 @@
return $values;
}
- protected function getTableInfo(string $objectType): array
+ public function getTableInfo(string $objectType): array
{
return match ($objectType) {
'post' => [$this->wpdb->postmeta, 'post_id'],
@@ -286,14 +505,14 @@
return update_option($this->optionKey($item, $field->name), $field->value);
}
- protected function optionKey(Item $item, string $name): string
+ public function optionKey(Item $item, string $name): string
{
return $item->baseKey
? BASE . $item->baseKey . '_' . $name
: BASE . $name;
}
- protected function clearCache(Item $item): void
+ public function clearCache(Item $item): void
{
match ($item->objectType) {
'post' => clean_post_cache($item->id),
@@ -302,4 +521,164 @@
default => null
};
}
+
+ // ─────────────────────────────────────────────────────────────
+ // Protected Helpers - Bulk Operations
+ // ─────────────────────────────────────────────────────────────
+
+ /**
+ * Bulk upsert meta using INSERT ... ON DUPLICATE KEY UPDATE
+ */
+ protected static function bulkUpsertMeta(string $table, string $idColumn, array $inserts): void
+ {
+ global $wpdb;
+
+ if (empty($inserts)) {
+ return;
+ }
+
+ // MySQL's ON DUPLICATE KEY requires a unique index
+ // For meta tables, we need to check existing and do update/insert
+ $existing = [];
+ $toInsert = [];
+ $toUpdate = [];
+
+ // Check which meta keys exist
+ $checks = [];
+ foreach ($inserts as $row) {
+ $checks[] = $wpdb->prepare("(%d, %s)", $row['id'], $row['key']);
+ }
+
+ $existingQuery = "SELECT {$idColumn}, meta_key FROM {$table}
+ WHERE ({$idColumn}, meta_key) IN (" . implode(',', $checks) . ")";
+ $existingRows = $wpdb->get_results($existingQuery, ARRAY_A);
+
+ foreach ($existingRows as $row) {
+ $existing[$row[$idColumn] . '_' . $row['meta_key']] = true;
+ }
+
+ // Separate inserts and updates
+ foreach ($inserts as $row) {
+ $key = $row['id'] . '_' . $row['key'];
+ if (isset($existing[$key])) {
+ $toUpdate[] = $row;
+ } else {
+ $toInsert[] = $row;
+ }
+ }
+
+ // Batch insert new records
+ if (!empty($toInsert)) {
+ $values = [];
+ $placeholders = [];
+
+ foreach ($toInsert as $row) {
+ $placeholders[] = "(%d, %s, %s)";
+ $values[] = $row['id'];
+ $values[] = $row['key'];
+ $values[] = $row['value'];
+ }
+
+ $sql = "INSERT INTO {$table} ({$idColumn}, meta_key, meta_value) VALUES " . implode(', ', $placeholders);
+ $wpdb->query($wpdb->prepare($sql, $values));
+ }
+
+ // Batch update existing records
+ if (!empty($toUpdate)) {
+ foreach ($toUpdate as $row) {
+ $wpdb->update(
+ $table,
+ ['meta_value' => $row['value']],
+ [$idColumn => $row['id'], 'meta_key' => $row['key']],
+ ['%s'],
+ ['%d', '%s']
+ );
+ }
+ }
+ }
+
+ /**
+ * Batch update post timestamps
+ */
+ protected static function batchTouchPosts(array $postIds): void
+ {
+ global $wpdb;
+
+ if (empty($postIds)) {
+ return;
+ }
+
+ $now = current_time('mysql');
+ $nowGmt = current_time('mysql', true);
+ $ids = implode(',', array_map('intval', $postIds));
+
+ $wpdb->query(
+ "UPDATE {$wpdb->posts}
+ SET post_modified = '{$now}', post_modified_gmt = '{$nowGmt}'
+ WHERE ID IN ({$ids})"
+ );
+ }
+
+ /**
+ * Batch update taxonomy relationships
+ */
+ protected static function batchUpdateTaxonomies(array $updates): void
+ {
+ foreach ($updates as $update) {
+ $termIds = empty(trim((string)$update['value']))
+ ? []
+ : array_map('intval', array_filter(explode(',', $update['value'])));
+
+ wp_set_object_terms($update['object_id'], $termIds, $update['taxonomy'], false);
+ }
+ }
+
+ /**
+ * Batch update WordPress default fields
+ */
+ protected static function batchUpdateWpDefaults(string $objectType, array $updates): void
+ {
+ foreach ($updates as $id => $fields) {
+ // Handle post_thumbnail separately
+ if (isset($fields['post_thumbnail'])) {
+ set_post_thumbnail($id, $fields['post_thumbnail']);
+ unset($fields['post_thumbnail']);
+ }
+ if (isset($fields['featured_image'])) {
+ set_post_thumbnail($id, $fields['featured_image']);
+ unset($fields['featured_image']);
+ }
+
+ if (empty($fields)) {
+ continue;
+ }
+
+ // Handle post_date conversion
+ if (isset($fields['post_date'])) {
+ $datetime = strtotime($fields['post_date']);
+ if ($datetime !== false) {
+ $fields['post_date'] = date('Y-m-d H:i:s', $datetime);
+ $fields['post_date_gmt'] = get_gmt_from_date($fields['post_date']);
+ $fields['edit_date'] = true;
+ }
+ }
+
+ match ($objectType) {
+ 'post' => wp_update_post(array_merge(['ID' => $id], $fields)),
+ 'user' => wp_update_user(array_merge(['ID' => $id], $fields)),
+ 'term' => null, // Terms need taxonomy, handled separately
+ default => null
+ };
+ }
+ }
+
+ /**
+ * Batch update options
+ */
+ protected static function batchUpdateOptions(array $updates): void
+ {
+ foreach ($updates as $update) {
+ update_option($update['key'], $update['value']);
+ }
+ }
}
--
Gitblit v1.10.0