<?php
|
namespace JVBase\meta;
|
|
use JVBase\registrar\Registrar;
|
use WP_Post;
|
use WP_Term;
|
use WP_User;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Meta
|
{
|
/**
|
* @var string post, term, user, or options
|
*/
|
protected string $type;
|
/**
|
* @var string the full slug, with BASE
|
*/
|
protected string $slug;
|
|
protected string $contentType;
|
protected Item $item;
|
protected Storage $storage;
|
protected Validator $validator;
|
protected Sanitizer $sanitizer;
|
protected array $fields;
|
protected WP_Post|WP_Term|WP_User|null $wpObject;
|
protected int|string $ID;
|
protected MetaTypeManager $typeManager;
|
protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]];
|
|
protected array $defaults = ['post_thumbnail'];
|
/**
|
* Create Meta instance for a post
|
*/
|
public static function forPost(int $id): self
|
{
|
if (array_key_exists($id, self::$instances['post'])) {
|
return self::$instances['post'][$id];
|
}
|
$new = new self($id, 'post');
|
self::$instances['post'][$id] = $new;
|
return $new;
|
}
|
|
/**
|
* Create Meta instance for a term
|
*/
|
public static function forTerm(int $id): self
|
{
|
if (array_key_exists($id, self::$instances['term'])) {
|
return self::$instances['term'][$id];
|
}
|
$new = new self($id, 'term');
|
self::$instances['term'][$id] = $new;
|
return $new;
|
}
|
|
/**
|
* Create Meta instance for a user
|
*/
|
public static function forUser(int $id): self
|
{
|
if (array_key_exists($id, self::$instances['user'])) {
|
return self::$instances['user'][$id];
|
}
|
$new = new self($id, 'user');
|
self::$instances['user'][$id] = $new;
|
return $new;
|
}
|
|
/**
|
* Create Meta instance for options
|
*/
|
public static function forOptions(?string $baseKey = 'ajv'): self
|
{
|
if (array_key_exists($baseKey, self::$instances['options'])) {
|
return self::$instances['options'][$baseKey];
|
}
|
$new = new self($baseKey, 'options');
|
self::$instances['options'][$baseKey] = $new;
|
return $new;
|
}
|
/***************************************************************
|
* Constructor
|
***************************************************************/
|
|
public function __construct(int|string|null $id, string $type)
|
{
|
$this->validator = new Validator();
|
$this->sanitizer = new Sanitizer();
|
$this->typeManager = new MetaTypeManager();
|
$this->type = $type;
|
$this->ID = $id;
|
$this->buildData($id, $type);
|
}
|
|
protected function buildData(int|string|null $id, string $type):void
|
{
|
$this->wpObject = match($type) {
|
'post' => get_post($id),
|
'term' => get_term($id),
|
'user', 'integrations' => get_userdata($id),
|
default => false
|
};
|
|
$this->slug = match($type) {
|
'post' => $this->wpObject->post_type,
|
'term' => $this->wpObject->taxonomy,
|
'user' => jvbUserRole($id),
|
default => null
|
};
|
|
|
|
$registrar = Registrar::getInstance($this->slug);
|
$fields = $registrar ? $registrar->getFields() : [];
|
$meta = match($type) {
|
'post' => get_post_meta($id),
|
'term' => get_term_meta($id),
|
'user' => get_user_meta($id),
|
default => []
|
};
|
$meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta);
|
|
foreach ($fields as $fieldName => $config) {
|
$fieldName = jvbNoBase($fieldName);
|
if ($this->wpObject && property_exists($this->wpObject, $fieldName)) {
|
$config['wp'] = true;
|
$value = $this->wpObject->$fieldName;
|
} else if (in_array($fieldName, $this->defaults)) {
|
$config['wp'] = true;
|
switch ($fieldName) {
|
case 'post_thumbnail':
|
$value = get_post_thumbnail_id($this->ID);
|
break;
|
}
|
} else {
|
$value = array_key_exists(BASE.$fieldName, $meta) ? $meta[BASE.$fieldName] : $config['default']??'';
|
switch ($config['type']) {
|
case 'taxonomy':
|
if (!$config['isReference']??true) {
|
$config['wp'] = true;
|
$value = implode(',', wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
|
}
|
break;
|
case 'selector':
|
if (array_key_exists('subtype', $config) && $config['subtype'] === 'taxonomy' && !$config['isReference']??true) {
|
$config['wp'] = true;
|
$value = implode(',',wp_get_post_terms($this->ID, jvbCheckBase($config['taxonomy']), ['fields' => 'ids']));
|
}
|
break;
|
}
|
|
}
|
$this->fields[$fieldName] = new Field($fieldName, $value, $config);
|
}
|
}
|
|
// ─────────────────────────────────────────────────────────────
|
// 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
|
{
|
// Handle repeater subfield path
|
if (str_contains($name, ':')) {
|
return $this->getByPath($name);
|
}
|
|
return $this->fields[$name]->get();
|
}
|
|
/**
|
* Set a field value (validates & sanitizes by default)
|
*/
|
public function set(string $name, mixed $value, $autosave = true): self
|
{
|
// Handle repeater subfield path (e.g., "services:2:image")
|
if (str_contains($name, ':')) {
|
return $this->setByPath($name, $value);
|
}
|
|
$field = $this->fields[$name]??false;
|
if (!$field) {
|
error_log('No config found for field: '.$name);
|
return $this;
|
}
|
|
// Validate
|
if (!$this->validator->validate($value, $field->config)) {
|
$field->addError("Validation failed for {$name}");
|
return $this;
|
}
|
|
// Sanitize
|
$value = $this->sanitizer->sanitize($value, $field->config);
|
$field->set($value);
|
if ($autosave && $field->isDirty) {
|
$this->save();
|
}
|
|
return $this;
|
}
|
|
/**
|
* Get multiple fields
|
*/
|
public function getAll(array $fields = []): array
|
{
|
if (empty($fields) || $fields === ['all']) {
|
$fields = array_keys($this->fields);
|
}
|
$fields = array_filter($this->fields, function ($field) use ($fields) {
|
return in_array($field, $fields);
|
}, ARRAY_FILTER_USE_KEY);
|
|
return array_map(function ($field) {
|
return $field->value;
|
}, $fields);
|
}
|
|
/**
|
* Set multiple fields
|
*/
|
public function setAll(array $data):bool
|
{
|
foreach ($data as $name => $value) {
|
$this->set($name, $value, false);
|
}
|
return $this->save();
|
}
|
|
/**
|
* Save all dirty fields to database
|
*/
|
public function save(): bool
|
{
|
$dirtyFields = array_filter($this->fields, function ($field) {
|
return $field->isDirty;
|
});
|
$defaults = array_filter($dirtyFields, function ($field) {
|
return $field->isDefault;
|
});
|
$custom = array_filter($dirtyFields, function($field) {
|
return !$field->isDefault;
|
});
|
|
$success = true;
|
|
if (!empty($defaults)) {
|
$result = false;
|
switch ($this->type) {
|
case 'post':
|
//Deal with field exceptions, first, that cannot be set via wp_update_post
|
foreach ($defaults as $fieldName => $field) {
|
switch ($fieldName) {
|
case 'post_thumbnail':
|
$result = set_post_thumbnail($this->ID, $field->value);
|
unset($defaults[$fieldName]);
|
break;
|
}
|
//If it's a taxonomy, we use wp_set_object_terms
|
if (
|
($field->config['type'] === 'selector' && $field->config['subtype'] === 'taxonomy')
|
|| $field->config['type'] === 'taxonomy') {
|
|
$result = wp_set_object_terms($this->ID, array_map('absint', explode(',', $field->value)), jvbCheckBase($field->config['taxonomy']));
|
unset($defaults[$fieldName]);
|
}
|
}
|
|
|
|
if (!empty($defaults)) {
|
error_log('Remaining fields: '.print_r($defaults, true));
|
$defaults = array_map(function ($field) {
|
return $field->value;
|
}, $defaults);
|
error_log('Remaining values to save: '.print_r($defaults, true));
|
$data = array_merge([
|
'post_type' => $this->slug,
|
'ID' => $this->ID
|
], $defaults);
|
error_log('Updating post: '.print_r($data, true));
|
$result = wp_update_post($data);
|
}
|
break;
|
case 'term':
|
$result = wp_update_term($this->ID, $this->slug, $defaults);
|
break;
|
case 'user':
|
$data = array_merge([
|
'ID' => $this->ID
|
], $defaults);
|
$result = wp_update_user($data);
|
break;
|
}
|
if (!$result || is_wp_error($result)) {
|
$success = false;
|
}
|
}
|
if (!empty($custom)) {
|
$function = match ($this->type) {
|
'post' => 'update_post_meta',
|
'term' => 'update_term_meta',
|
'user' => 'update_user_meta',
|
'options', 'option' => 'update_option',
|
default => false,
|
};
|
if (!$function) {
|
error_log('[Meta]::save() Invalid type, cannot save: '.$this->type);
|
return false;
|
}
|
foreach ($custom as $field) {
|
$result = $function($this->ID, BASE.$field->name, $field->value);
|
if (!$result) {
|
error_log('Problem saving field: '.$field->name.' with value: '.print_r($field->value, true));
|
}
|
}
|
if ($this->type === 'term' && Registrar::getInstance($this->slug)->hasFeature('is_content')) {
|
update_term_meta($this->ID, BASE.'date_modified', date('Y-m-d H:i:s'));
|
}
|
}
|
|
|
return $success;
|
}
|
|
/**
|
* Delete a field value
|
*/
|
public function delete(string $name): bool
|
{
|
$field = $this->fields[$name]??false;
|
if (!$field) {
|
error_log('[Meta]::delete Could not delete field '.$name.': not registered');
|
return true;
|
}
|
|
if ($field->isTaxonomy()) {
|
wp_set_object_terms($this->ID, [], $this->slug);
|
return true;
|
}
|
|
return match ($this->type) {
|
'post' => delete_post_meta($this->ID, BASE.$name),
|
'term' => delete_term_meta($this->ID, BASE.$name),
|
'user', 'integrations' => delete_user_meta($this->ID, BASE.$name),
|
'options' => delete_option(BASE.$name),
|
default => false
|
};
|
}
|
|
|
|
/**
|
* Delete multiple field values
|
*/
|
public function deleteAll(array $names): array
|
{
|
$results = [];
|
foreach ($names as $name) {
|
$results[$name] = $this->delete($name);
|
}
|
return $results;
|
}
|
|
/*****************************************************************
|
* Repeater Access
|
*****************************************************************/
|
|
/**
|
* Get repeater accessor for fluent repeater operations
|
*/
|
public function repeater(string $name): Repeater
|
{
|
return new Repeater($this, $name);
|
}
|
|
protected function setByPath(string $path, mixed $value): self
|
{
|
error_log('Setting by path: '.$path.', with value: '.print_r($value, true));
|
$parts = explode(':', $path, 3);
|
if (count($parts) !== 3) {
|
return $this;
|
}
|
|
[$repeaterName, $rowIndex, $subField] = $parts;
|
$this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
|
|
return $this;
|
}
|
|
protected function getByPath(string $path): mixed
|
{
|
$parts = explode(':', $path, 3);
|
if (count($parts) !== 3) {
|
return null;
|
}
|
|
[$repeaterName, $rowIndex, $subField] = $parts;
|
return $this->repeater($repeaterName)->field((int) $rowIndex, $subField);
|
}
|
|
// ─────────────────────────────────────────────────────────────
|
// Utility Methods
|
// ─────────────────────────────────────────────────────────────
|
|
|
/**
|
* Discard all unsaved changes
|
*/
|
public function reset(): self
|
{
|
$this->item->resetAll();
|
return $this;
|
}
|
|
|
/**
|
* Get field configuration
|
*/
|
public function config(string $name): ?array
|
{
|
return $this->fields[$name]->config??null;
|
}
|
|
|
/**
|
* Get item ID
|
*/
|
public function id(): int|string|null
|
{
|
return $this->ID;
|
}
|
|
/**
|
* Get object type (post, term, user, options)
|
*/
|
public function objectType(): string
|
{
|
return $this->type;
|
}
|
|
/**
|
* Get content type (tattoo, artist, etc)
|
*/
|
public function contentType(): ?string
|
{
|
return $this->contentType;
|
}
|
|
|
// ─────────────────────────────────────────────────────────────
|
// Protected Helpers
|
// ─────────────────────────────────────────────────────────────
|
|
/**
|
* Check for field update overrides
|
*/
|
public 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;
|
}
|
|
/**
|
* Get default value for a field type
|
*/
|
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 => '',
|
};
|
}
|
|
}
|