item->baseKey = $baseKey; return $instance; } // ───────────────────────────────────────────────────────────── // Constructor // ───────────────────────────────────────────────────────────── public function __construct(int|string|null $id, string $type) { $this->storage = new Storage(); $this->validator = new MetaValidator(); $this->sanitizer = new MetaSanitizer(); $this->typeManager = new MetaTypeManager(); $this->item = $this->buildItem($id, $type); } protected function buildItem(int|string|null $id, string $type): Item { $contentType = null; $wpObject = null; if ($id && $type !== 'options') { [$wpObject, $contentType] = match ($type) { 'post' => [get_post($id), jvbNoBase(get_post_type($id))], 'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)], 'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)], default => [null, null] }; } $item = new Item($id, $type, $contentType); $item->wpObject = $wpObject; $item->fieldConfigs = $this->loadFieldConfigs($contentType, $type); // Mark WP defaults in configs $defaults = Item::WP_DEFAULTS[$type] ?? []; foreach ($defaults as $name) { if (!isset($item->fieldConfigs[$name])) { $item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true]; } else { $item->fieldConfigs[$name]['_wp_default'] = true; } } return $item; } protected function loadFieldConfigs(?string $contentType, string $objectType): array { if (!$contentType && $objectType !== 'options') { return []; } return jvbGetFields($contentType ?? 'options', $objectType); } // ───────────────────────────────────────────────────────────── // Magic Methods for Fluent Access // ───────────────────────────────────────────────────────────── public function __get(string $name): mixed { return $this->get($name); } public function __set(string $name, mixed $value): void { $this->set($name, $value); } public function __isset(string $name): bool { return $this->item->hasField($name); } // ───────────────────────────────────────────────────────────── // Core API // ───────────────────────────────────────────────────────────── /** * Get a field value */ public function get(string $name): mixed { // Return from loaded field if exists if ($field = $this->item->getField($name)) { return $field->get(); } // Load from storage $value = $this->storage->get($this->item, $name); $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; $field = new Field($name, $value, $config); $this->item->setField($field); return $value; } /** * Set a field value (validates & sanitizes) */ public function set(string $name, mixed $value): self { $config = $this->item->getFieldConfig($name); if (!$config) { // Allow setting unknown fields with minimal config $config = ['type' => 'text', 'name' => $name]; } $config['name'] = $name; // Validate if ($this->autoValidate && !$this->validator->validate($value, $config)) { $field = $this->item->getField($name) ?? new Field($name, $value, $config); $field->addError("Validation failed for {$name}"); $this->item->setField($field); return $this; } // Sanitize if ($this->autoSanitize) { $value = $this->sanitizer->sanitize($value, $config); } // Get or create field $field = $this->item->getField($name); if ($field) { $field->set($value); } else { // Need to load original to track dirty state $original = $this->storage->get($this->item, $name); $field = new Field($name, $original, $config); $field->set($value); $this->item->setField($field); } return $this; } /** * Get multiple fields */ public function getAll(array $fields = []): array { if (empty($fields) || $fields === ['all']) { $fields = array_keys($this->item->fieldConfigs); } // Load all from storage $values = $this->storage->getAll($this->item, $fields); // Create Field instances foreach ($values as $name => $value) { if (!$this->item->getField($name)) { $config = $this->item->getFieldConfig($name) ?? ['type' => 'text']; $this->item->setField(new Field($name, $value, $config)); } } return $values; } /** * Set multiple fields */ public function setAll(array $data): self { foreach ($data as $name => $value) { $this->set($name, $value); } return $this; } /** * Save all dirty fields to database */ public function save(bool $updateTimestamp = true): bool { if (!$this->item->isValid()) { JVB()->error()->log('meta', 'Cannot save: validation errors exist', [ 'fields' => array_keys($this->item->getInvalidFields()) ], 'warning'); return false; } // Check for field overrides before saving foreach ($this->item->getDirtyFields() as $field) { if ($this->checkOverrides($field)) { $field->markClean(); } } return $this->storage->save($this->item, $updateTimestamp); } /** * Delete a field value */ public function delete(string $name): bool { $result = $this->storage->delete($this->item, $name); if ($result && $field = $this->item->getField($name)) { $field->set($this->getDefaultValue($name)); $field->markClean(); } return $result; } // ───────────────────────────────────────────────────────────── // Utility Methods // ───────────────────────────────────────────────────────────── /** * Get all dirty (changed) fields */ public function getDirty(): array { return array_map( fn(Field $f) => $f->value, $this->item->getDirtyFields() ); } /** * Check if any fields have changed */ public function isDirty(): bool { return $this->item->hasDirtyFields(); } /** * Discard all unsaved changes */ public function reset(): self { foreach ($this->item->fields as $field) { $field->reset(); } return $this; } /** * Get validation errors */ public function getErrors(): array { $errors = []; foreach ($this->item->getInvalidFields() as $name => $field) { $errors[$name] = $field->errors; } return $errors; } /** * Check if valid (no validation errors) */ public function isValid(): bool { return $this->item->isValid(); } /** * Disable auto-validation for bulk operations */ public function withoutValidation(): self { $this->autoValidate = false; return $this; } /** * Disable auto-sanitization */ public function withoutSanitization(): self { $this->autoSanitize = false; return $this; } /** * Get the underlying item (for rendering, etc) */ public function item(): Item { return $this->item; } /** * Get field configuration */ public function config(string $name): ?array { return $this->item->getFieldConfig($name); } /** * Get all field configurations */ public function configs(): array { return $this->item->fieldConfigs; } // ───────────────────────────────────────────────────────────── // Protected Helpers // ───────────────────────────────────────────────────────────── protected function checkOverrides(Field $field): bool { $name = $field->name; $type = $field->type(); $value = $field->value; do_action('jvb_meta_update', $name, $value, $this->item->objectType); $overrides = [ BASE . 'update_' . $name, BASE . 'update_' . $type, 'jvb_update_' . $name, 'jvb_update_' . $type, ]; foreach ($overrides as $override) { if (function_exists($override)) { $override($this->item->id, $value); return true; } } return false; } protected function getDefaultValue(string $name): mixed { $config = $this->item->getFieldConfig($name); $type = $config['type'] ?? 'text'; return match ($this->typeManager->getMetaType($type)) { 'object', 'array' => [], 'boolean' => false, 'integer' => 0, default => '', }; } }