<?php
|
namespace JVBase\meta;
|
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Main facade for meta operations
|
* Fluent API for getting/setting meta values with validation & sanitization
|
*/
|
class Meta
|
{
|
protected Item $item;
|
protected Storage $storage;
|
protected MetaValidator $validator;
|
protected MetaSanitizer $sanitizer;
|
protected MetaTypeManager $typeManager;
|
|
protected bool $autoValidate = true;
|
protected bool $autoSanitize = true;
|
|
// ─────────────────────────────────────────────────────────────
|
// Factory Methods
|
// ─────────────────────────────────────────────────────────────
|
|
public static function forPost(int $id): self
|
{
|
return new self($id, 'post');
|
}
|
|
public static function forTerm(int $id): self
|
{
|
return new self($id, 'term');
|
}
|
|
public static function forUser(int $id): self
|
{
|
return new self($id, 'user');
|
}
|
|
public static function forOptions(?string $baseKey = null): self
|
{
|
$instance = new self($baseKey, 'options');
|
$instance->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 => '',
|
};
|
}
|
}
|