<?php
|
namespace JVBase\meta;
|
|
use JVBase\base\options;
|
use JVBase\managers\Cache;
|
use JVBase\registrar\Registrar;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Meta {
|
/**
|
* @var string post, term, user, or options
|
*/
|
protected string $objectType;
|
/**
|
* @var string|null post type, user role, or taxonomy
|
*/
|
protected ?string $content;
|
/**
|
* @var int|string <int>post, term, or user ID OR {string} option key base
|
*/
|
protected int|string $ID;
|
/**
|
* @var array of Registrar's defined fields, if any
|
*/
|
protected static array $registrars = [];
|
protected static ?options $options = null;
|
/**
|
* @var array Field values, stored in memory
|
*/
|
protected array $fields;
|
|
protected ?\WC_Product $product = null;
|
|
protected static Validator $validator;
|
protected static Sanitizer $sanitizer;
|
|
protected static array $coreFields = [
|
'post' => ['post_title', 'post_content', 'post_excerpt', 'post_status', 'post_date', 'post_modified', 'post_thumbnail', 'post_name'],
|
'term' => ['name', 'description', 'parent'],
|
'user' => ['first_name', 'last_name', 'user_email', 'display_name', 'user_login'],
|
'options' => []
|
];
|
|
protected static array $instances = [
|
'post' => [],
|
'term' => [],
|
'user' => [],
|
'options'=> [],
|
];
|
|
private function __construct(string $objectType, int|string $ID) {
|
$this->objectType = $objectType;
|
$this->ID = $ID;
|
$this->content = $this->resolveBasedType();
|
|
if ($this->content && !array_key_exists($this->content, self::$registrars)) {
|
$registrar = Registrar::getInstance($this->content);
|
if ($registrar) {
|
self::$registrars[$this->content] = $registrar->getFields();
|
}
|
}
|
if ($this->objectType === 'options' && !isset(self::$options)) {
|
self::$options = options::getInstance();
|
}
|
|
if ($this->objectType === 'post' && function_exists('wc_get_product') && in_array($this->content, ['product', 'product_variation'], true)) {
|
$this->product = wc_get_product($this->ID) ?: null;
|
}
|
$this->buildData();
|
if (!isset(self::$sanitizer)) {
|
self::$sanitizer = new Sanitizer();
|
}
|
if (!isset(self::$validator)) {
|
self::$validator = new Validator();
|
}
|
}
|
protected function buildData():void
|
{
|
//Get all Item meta
|
$this->fields = match ($this->objectType) {
|
'post' => get_post_meta($this->ID),
|
'term' => get_term_meta($this->ID),
|
'user' => get_user_meta($this->ID),
|
default => []
|
};
|
$this->fields = array_combine(
|
array_map('jvbNoBase', array_keys($this->fields)),
|
$this->fields
|
);
|
|
//Add in core fields
|
$obj = match ($this->objectType) {
|
'post' => get_post($this->ID),
|
'term' => get_term($this->ID, $this->content),
|
'user' => get_userdata($this->ID),
|
default => false
|
};
|
if ($obj) {
|
foreach (self::$coreFields[$this->objectType] as $name) {
|
$value = match ($name) {
|
'post_thumbnail' => get_post_thumbnail_id($this->ID),
|
default => $obj->$name??false
|
};
|
// if (!$value) {
|
// $this->log('Could not find value for '.$name.', of '.$this->content);
|
// }
|
$this->fields[$name] = $value??'';
|
}
|
}
|
}
|
|
public static function forPost(int $ID):self
|
{
|
if (array_key_exists($ID, self::$instances['post'])) {
|
return self::$instances['post'][$ID];
|
}
|
self::$instances['post'][$ID] = new self('post', $ID);
|
return self::$instances['post'][$ID];
|
}
|
|
public static function forTerm(int $ID):self
|
{
|
if (array_key_exists($ID, self::$instances['term'])) {
|
return self::$instances['term'][$ID];
|
}
|
self::$instances['term'][$ID] = new self('term', $ID);
|
return self::$instances['term'][$ID];
|
}
|
|
public static function forUser(int $ID):self
|
{
|
if (array_key_exists($ID, self::$instances['user'])) {
|
return self::$instances['user'][$ID];
|
}
|
self::$instances['user'][$ID] = new self('user', $ID);
|
return self::$instances['user'][$ID];
|
}
|
|
public static function forOptions(string $base = 'default'):self
|
{
|
if (array_key_exists($base, self::$instances['options'])) {
|
return self::$instances['options'][$base];
|
}
|
self::$instances['options'][$base] = new self('options', $base);
|
return self::$instances['options'][$base];
|
}
|
|
protected function resolveBasedType(): ?string {
|
return match ($this->objectType) {
|
'post' => get_post_type($this->ID) ?: null,
|
'term' => ($term = get_term($this->ID)) && !is_wp_error($term) ? $term->taxonomy : null,
|
'user' => jvbUserRole($this->ID),
|
default => null
|
};
|
}
|
|
|
|
|
|
/***************************************************************************
|
* GETTING FIELDS
|
***************************************************************************/
|
public function get(string $name): mixed {
|
if (array_key_exists($name, $this->fields)) {
|
return $this->fields[$name];
|
}
|
|
if ($this->product && $this->isProductField($name)) {
|
$value = $this->getProductField($name);
|
} else if (in_array($name, static::$coreFields[$this->objectType], true)) {
|
$value = $this->getCoreField($name);
|
} else {
|
$value = $this->getMetaField($name);
|
}
|
$this->fields[$name] = $value;
|
return $value;
|
}
|
|
public function getAll(?array $names = null): array {
|
$names = $names ?? $this->getAllFieldNames();
|
$out = [];
|
foreach ($names as $name) {
|
$out[$name] = $this->get($name);
|
}
|
return $out;
|
}
|
|
protected function getCoreField(string $name): mixed {
|
return match ($this->objectType) {
|
'post' => $name === 'post_thumbnail'
|
? get_post_thumbnail_id($this->ID)
|
: (($p = get_post($this->ID)) ? ($p->$name ?? null) : null),
|
'term' => ($t = get_term($this->ID, $this->content)) && !is_wp_error($t) ? ($t->$name ?? null) : null,
|
'user' => ($u = get_userdata($this->ID)) ? ($u->$name ?? null) : null,
|
};
|
}
|
|
protected function getMetaField(string $name): mixed {
|
$key = $this->metaKey($name);
|
return match ($this->objectType) {
|
'post' => get_post_meta($this->ID, $key, true),
|
'term' => get_term_meta($this->ID, $key, true),
|
'user' => get_user_meta($this->ID, $key, true),
|
'options' => get_option($key)
|
};
|
}
|
|
/***************************************************************************
|
* SETTING FIELDS
|
***************************************************************************/
|
public function set(string $name, mixed $value): bool {
|
$field = $this->getFieldConfig($name);
|
if ($field) {
|
if (array_key_exists('type', $field)) {
|
if ($field['type'] === 'repeater' && str_contains($name, ':')) {
|
$this->log('Setting by path in set');
|
return $this->setByPath($name, $value);
|
} elseif ($field['type'] === 'group' && str_contains($name, '|')) {
|
return $this->setGroup($name, $value);
|
}
|
}
|
|
if (!self::$validator->validate($value, $field)) {
|
return false;
|
}
|
|
$value = self::$sanitizer->sanitize($value, $field);
|
}
|
|
if ($this->product && $this->isProductField($name)) {
|
return $this->setProductField($name, $value);
|
}
|
if (in_array($name, static::$coreFields[$this->objectType], true)) {
|
return $this->setCoreField($name, $value);
|
}
|
return $this->setMetaField($name, $value);
|
}
|
|
public function setAll(array $data): array {
|
$core = array_filter($data, function ($name) {
|
return in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
$results = [];
|
if (!empty($core)) {
|
$data = array_filter($data, function ($name) {
|
return !in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
$results = $this->setAllCoreFields($core);
|
}
|
|
foreach ($data as $name => $value) {
|
$results[$name] = $this->set($name, $value);
|
}
|
return $results;
|
}
|
|
protected function setAllCoreFields(array $data):array
|
{
|
$results = [];
|
|
$core = array_filter($data, function ($name) {
|
return in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
|
|
if (count($core) !== count($data)) {
|
$notCore = array_filter($data, function ($name) {
|
return !in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
$results = $this->setAll($notCore);
|
}
|
|
if (array_key_exists('post_thumbnail', $core)) {
|
$results['post_thumbnail'] = $this->setCoreField('post_thumbnail', $core['post_thumbnail']);
|
unset ($core['post_thumbnail']);
|
}
|
|
$return = match ($this->objectType) {
|
'post' => !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $core))),
|
'term' => !is_wp_error(wp_update_term($this->ID, $this->content, $core)),
|
'user' => !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $core))),
|
};
|
foreach ($core as $name => $value) {
|
$results[$name] = $return;
|
}
|
return $results;
|
}
|
|
protected function setCoreField(string $name, mixed $value): bool {
|
$success = match ($this->objectType) {
|
'post' => $name === 'post_thumbnail'
|
? (!empty($value) ? set_post_thumbnail($this->ID, (int)$value) : delete_post_thumbnail($this->ID))
|
: !is_wp_error(wp_update_post(['ID' => $this->ID, $name => $value])),
|
'term' => !is_wp_error(wp_update_term($this->ID, $this->content, [$name => $value])),
|
'user' => !is_wp_error(wp_update_user(['ID' => $this->ID, $name => $value])),
|
default => false
|
};
|
if ($success) {
|
$this->clearCache();
|
$this->fields[$name] = $value;
|
}
|
return $success;
|
}
|
|
protected function setMetaField(string $name, mixed $value): bool {
|
$key = $this->metaKey($name);
|
$result = match ($this->objectType) {
|
'post' => update_post_meta($this->ID, $key, $value),
|
'term' => update_term_meta($this->ID, $key, $value),
|
'user' => update_user_meta($this->ID, $key, $value),
|
'options' => update_option($key, $value)
|
};
|
if ($result) {
|
$this->fields[$name] = $value;
|
$this->clearCache();
|
}
|
return $result !== false;
|
}
|
/********************************************************************************
|
* DELETING FIELDS
|
********************************************************************************/
|
public function delete(string $name):bool
|
{
|
if (in_array($name, static::$coreFields[$this->objectType])) {
|
return $this->deleteCoreField($name);
|
}
|
|
$field = $this->getFieldConfig($name);
|
if ($field) {
|
if ($this->isTaxonomy($field)) {
|
return !is_wp_error(wp_set_object_terms($this->ID, [], $this->content));
|
}
|
}
|
$key = $this->metaKey($name);
|
return match ($this->objectType) {
|
'post' => delete_post_meta($this->ID, $key),
|
'term' => delete_term_meta($this->ID, $key),
|
'user' => delete_user_meta($this->ID, $key),
|
'option'=> delete_option($key)
|
};
|
}
|
public function deleteAll(array $fieldNames):array
|
{
|
$core = array_filter($fieldNames, function ($name) {
|
return in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
|
$results = [];
|
if (!empty($core)) {
|
$fieldNames = array_filter($fieldNames, function ($name) {
|
return !in_array($name, self::$coreFields[$this->objectType]);
|
});
|
$results = $this->deleteAllCoreFields($core);
|
}
|
|
foreach ($fieldNames as $name) {
|
$results[$name] = $this->delete($name);
|
}
|
|
return $results;
|
}
|
protected function deleteAllCoreFields(array $fieldNames):array
|
{
|
$results = [];
|
$core = array_filter($fieldNames, function ($name) {
|
return in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
|
|
if (count($core) !== count($fieldNames)) {
|
$notCore = array_filter($fieldNames, function ($name) {
|
return !in_array($name, self::$coreFields[$this->objectType]);
|
}, ARRAY_FILTER_USE_KEY);
|
$results = $this->deleteAll($notCore);
|
}
|
|
if (array_key_exists('post_thumbnail', $fieldNames)) {
|
$results['post_thumbnail'] = delete_post_thumbnail($this->ID);
|
unset($fieldNames['post_thumbnail']);
|
unset($core['post_thumbnail']);
|
}
|
|
$updated = [];
|
foreach ($core as $name) {
|
$updated[$name] = $this->getCoreDefault($name);
|
}
|
|
$success = match ($this->objectType) {
|
'post' => !is_wp_error(wp_update_post(array_merge(['ID' => $this->ID], $updated))),
|
'term' => !is_wp_error(wp_update_term($this->ID, $this->content, $updated)),
|
'user' => !is_wp_error(wp_update_user(array_merge(['ID' => $this->ID], $updated)))
|
};
|
|
foreach ($core as $name) {
|
$results[$name] = $success;
|
}
|
return $results;
|
}
|
|
protected function deleteCoreField(string $name):bool
|
{
|
if ($name === 'post_thumbnail') {
|
return delete_post_thumbnail($this->ID);
|
}
|
$default = $this->getCoreDefault($name);
|
return $this->setCoreField($name, $default);
|
}
|
|
|
/**************************************************************
|
* WOOCOMMERCE PRODUCT SPECIFIC
|
**************************************************************/
|
protected function isProductField(string $name): bool {
|
return $name === 'variations' || method_exists($this->product, 'get_'.$name) || method_exists($this->product, 'set_'.$name);
|
}
|
|
protected function getProductField(string $name): mixed {
|
$method = 'get_'.$name;
|
if ($name === 'variations') {
|
return $this->getVariations();
|
}
|
return method_exists($this->product, $method) ? $this->product->$method() : null;
|
}
|
protected function getVariations():array
|
{
|
$variations = $this->product->get_children();
|
$out = [];
|
foreach ($variations as $v) {
|
$out[] = array_merge(['id' => $v], self::forPost($v)->getAll());
|
}
|
return $out;
|
}
|
|
protected function setProductField(string $name, mixed $value): bool {
|
$method = 'set_'.$name;
|
if (!method_exists($this->product, $method)) return false;
|
$this->product->$method($value);
|
$this->product->save();
|
$this->fields[$name] = $value;
|
$this->clearCache();
|
return true;
|
}
|
|
protected function getAllProductFieldNames(): array
|
{
|
return [
|
'price',
|
'sku',
|
'sale_price',
|
'regular_price',
|
'date_on_sale_from',
|
'date_on_sale_to',
|
'max_purchase_quantity',
|
'min_purchase_quantity',
|
'short_description',
|
'description',
|
'image_id',
|
'gallery_image_ids',
|
'stock',
|
'stock_quantity',
|
'stock_status',
|
'variations'
|
];
|
}
|
|
/*** Sanitization & Validation ***/
|
// protected function sanitize(mixed $value, array $field): mixed {
|
// return match ($field['type']) {
|
// 'text', 'tel' => sanitize_text_field($value),
|
// 'email' => sanitize_email($value),
|
// 'url' => esc_url_raw($value),
|
// 'textarea' => !empty($field['quill']) ? wp_kses_post($value) : sanitize_textarea_field($value),
|
// 'number' => is_numeric($value) ? $value + 0 : 0,
|
// 'true_false' => (bool)$value,
|
// 'datetime' => sanitize_text_field($value),
|
// 'upload', 'image', 'gallery' => !empty($field['multiple']) ? array_map('intval', (array)$value) : (int)$value,
|
// 'checkbox', 'set' => array_map('sanitize_text_field', (array)$value),
|
// 'radio', 'select' => sanitize_text_field($value),
|
// 'selector', 'taxonomy', 'user', 'post' => array_map('intval', (array)$value),
|
// 'repeater', 'tagList' => $this->sanitizeRepeater($value, $field),
|
// 'group' => $this->sanitizeGroup($value, $field),
|
// default => is_array($value) ? array_map('sanitize_text_field', $value) : sanitize_text_field($value),
|
// };
|
// }
|
|
// protected function validate(string $name, mixed $value, array $field): bool|string {
|
// if (!empty($field['required']) && ($value === '' || $value === null)) {
|
// return $name.' is required';
|
// }
|
// if (isset($field['maxLength']) && is_string($value) && strlen($value) > $field['maxLength']) {
|
// return $name.' exceeds max length of '.$field['maxLength'];
|
// }
|
// if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
|
// return $name.' is below minimum of '.$field['min'];
|
// }
|
// if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
|
// return $name.' exceeds maximum of '.$field['max'];
|
// }
|
// if (in_array($field['type'], ['checkbox', 'set', 'radio', 'select'], true) && !empty($field['options'])) {
|
// foreach ((array)$value as $v) {
|
// if (!array_key_exists($v, $field['options'])) {
|
// return $name.': invalid option '.$v;
|
// }
|
// }
|
// }
|
// return true;
|
// }
|
|
/************************************************************************
|
* REPEATER Helpers
|
************************************************************************/
|
public function repeater(string $name):Repeater
|
{
|
return new Repeater($this, $name);
|
}
|
protected function setByPath(string $path, mixed $value):bool
|
{
|
if (!str_contains($path, ':')) {
|
$this->log('setByPath called without appropriate string (:)');
|
return $this->set($path, $value);
|
}
|
$parts = explode(':', $path, 3);
|
if (count($parts) !== 3) {
|
$this->log('Could not set value by path: '.$path.', with value: '.print_r($value, true));
|
return false;
|
}
|
[$repeaterName, $rowIndex, $subField] = $parts;
|
$this->clearCache();
|
return $this->repeater($repeaterName)->setField((int) $rowIndex, $subField, $value);
|
}
|
|
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);
|
}
|
/****************************************************************************************
|
* GROUP helpers
|
****************************************************************************************/
|
|
public function group(string $name): Group
|
{
|
return new Group($this, $name);
|
}
|
|
protected function setGroup(string $name, mixed $value):bool
|
{
|
if (!str_contains($name, '|')) {
|
$this->log('setGroup called without appropriate key (|)');
|
return $this->set($name, $value);
|
}
|
[$parent, $child] = explode('|', $name, 2);
|
$this->clearCache();
|
return $this->group($parent)->set($child, $value);
|
}
|
|
/***************************************************************************************
|
* UTILITY
|
***************************************************************************************/
|
protected function clearCache():void
|
{
|
match ($this->objectType) {
|
'post' => Cache::clearPost($this->ID),
|
'term' => Cache::clearTerm($this->ID),
|
'user' => Cache::clearUser($this->ID),
|
};
|
}
|
protected function isTaxonomy(array $field):bool
|
{
|
$type = $field['type']??'';
|
return $type === 'taxonomy' || ($type === 'selector' &&$field['subtype']??'' === 'taxonomy');
|
}
|
|
protected function getAllFieldNames(): array {
|
$names = static::$coreFields[$this->objectType];
|
if ($this->hasRegistrar()) {
|
$names = array_merge($names, array_keys(self::$registrars[$this->content]));
|
}
|
if ($this->product) {
|
$names = array_merge($names, $this->getAllProductFieldNames());
|
}
|
return array_unique($names);
|
}
|
|
protected function hasRegistrar():bool
|
{
|
return !is_null($this->content) && array_key_exists($this->content, self::$registrars);
|
}
|
|
public function getFieldConfig(string $name): ?array {
|
if ($this->objectType === 'options') return $this->getOptionsConfig($name);
|
if (!$this->hasRegistrar()) return null;
|
return self::$registrars[$this->content][$name] ?? null;
|
}
|
protected function getOptionsConfig(string $name):?array
|
{
|
return self::$options->getConfig($name);
|
}
|
|
protected function getCoreDefault(string $name):string
|
{
|
return match($name) {
|
'post_status' => 'draft',
|
'post_date','post_modified' => date('mysql'),
|
'post_name' => sanitize_title($this->get('post_title')),
|
default => ''
|
};
|
}
|
|
protected function metaKey(string $name): string {
|
if ($this->product) {
|
if (in_array($name, $this->getAllProductFieldNames())) {
|
return $name;
|
}
|
}
|
return jvbCheckBase($name);
|
}
|
|
/**
|
* Get item ID
|
*/
|
public function id(): int|string|null
|
{
|
return $this->ID;
|
}
|
|
public function config(string $name):?array
|
{
|
return $this->getFieldConfig($name);
|
}
|
|
protected function log(string $message, array $context = []):void
|
{
|
$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? '';
|
error_log('[Meta] '.$caller.' '.$message);
|
if (!empty($context)) {
|
error_log(' CONTEXT: '.print_r($context, true));
|
}
|
}
|
}
|