# wpjam-basic/trunk/includes/class-wpjam-args.php

WPJAM Basic, version trunk. 1,480 lines.

- Page: https://pluginprobe.com/plugins/wpjam-basic/trunk/code/includes/class-wpjam-args.php
- Raw: https://pluginprobe.com/plugins/wpjam-basic/trunk/raw/includes/class-wpjam-args.php
- Modified: 2026-08-31T10:00:26+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/wpjam-basic/trunk/code/includes/class-wpjam-args.php#L10-L20`.

```php
<?php
trait WPJAM_Call_Trait{
	public function call($name, ...$args){
		if(is_string($name) && static::mod($name, '_by_model')){
			$cb	= [$this->model, $name];
		}else{
			$cb	= is_closure($name) ? $name : $this->$name;
			$cb	= is_closure($cb) ? $cb->bindTo($this, $this) : $cb;
		}

		return wpjam_call($cb, ...$args);
	}

	public function try($name, ...$args){
		return wpjam_try([$this, $name], ...$args);
	}

	public function catch($name, ...$args){
		return wpjam_catch([$this, $name], ...$args);
	}

	protected function call_dynamic_method($name, ...$args){
		return $this->call(wpjam_dynamic_method(static::class, $name), ...$args);
	}

	public static function add_dynamic_method($name, $closure){
		wpjam_dynamic_method(static::class, $name, $closure);
	}

	public static function mod(&$method, $mod){
		if(str_ends_with($mod, '_')){
			return try_prefix($method, '-', $mod);
		}elseif(str_starts_with($mod, '_')){
			return try_suffix($method, '-', $mod);
		}

		if(($action = str_replace('_'.$mod.'_', '_', $method)) != $method){
			$method	= $action;
			return true;
		}

		return static::mod($method, $mod.'_') || static::mod($method, '_'.$mod);
	}

	protected static function get_param($name='', $method='data'){
		return wpjam_param($name, $method);
	}

	public static function get_called(){
		return strtolower(static::class);
	}
}

trait WPJAM_Items_Trait{
	use WPJAM_Call_Trait;

	public function get_items($field=''){
		return $field == ':field'
		? (wpjam_get_annotation(static::class, 'items_field') ?: '_items')
		: ($this->{$field ?: $this->get_items(':field')} ?: []);
	}

	public function update_items($items, $field=''){
		$this->{$field ?: $this->get_items(':field')}	= $items;

		return $this;
	}

	public function process_items($cb, $field='', ...$args){
		$field	= $field ?: $this->get_items(':field');
		$items	= $this->get_items($field);

		try{
			if(count($args) == 3){
				[$item, $key, $action]	= $args;

				$args[]	= $field;
				$add	= $action === 'add';

				if(isset($key) ? wpjam_exists($items, $key) === $add : !$add){
					wpjam_throw('invalid_item_key', isset($key) ? '「'.$key.'」'.($add ? '已' : '不').'存在，无法'.(['add'=>'添加', 'update'=>'编辑', 'delete'=>'删除'][$action] ?? '操作') : 'key不能为空');
				}

				if(isset($item)){
					if($add && ($max = wpjam_get_annotation(static::class, 'max_items')) && count($items) >= $max){
						wpjam_throw('quota_exceeded', '最多'.$max.'个');
					}

					if(method_exists($this, 'validate_item')){
						wpjam_if_error($this->validate_item(...$args), 'throw');
					}

					if(method_exists($this, 'sanitize_item')){
						$args[0]	= $this->sanitize_item(...$args);
					}
				}
			}

			return $this->update_items(wpjam_call($cb, $items, ...$args), $field);
		}catch(Exception $e){
			return wpjam_catch($e);
		}
	}

	public function get_item($key, $field=''){
		return wpjam_get($this->get_items($field), $key);
	}

	public function get_item_arg($key, $arg, $field=''){	// deprecated
		return $this->get_item($key.'.'.$arg, $field);
	}

	public function add_item($key, ...$args){
		[$key, $item]	= ($args && !is_bool($key) && (is_scalar($key) || is_null($key))) ? [$key, array_shift($args)] : [null, $key];

		return $this->process_items(fn($items, $item)=> wpjam_add_at($items, count($items), $key, $item), $args[0] ?? '', $item, $key, 'add');
	}

	public function add_items($items, $field=''){
		return wpjam_map($items, fn(...$args)=> $this->add_item(...[...array_pad($args, -2, null), $field]), wp_is_numeric_array($items) ? 'v' : 'kv');
	}

	public function remove_item($item, $field=''){
		return $this->process_items(fn($items)=> array_diff($items, [$item]), $field);
	}

	public function edit_item($key, $item, $field=''){
		return $this->update_item($key, $item, $field);
	}

	public function update_item($key, $item, $field='', $action='update'){
		return $this->process_items(fn($items, $item)=> array_replace($items, [$key=>$item]), $field, $item, $key, $action);
	}

	public function set_item($key, $item, $field=''){
		return $this->update_item($key, $item, $field, 'set');
	}

	public function delete_item($key, $field=''){
		return wpjam_tap(
			$this->process_items(fn($items)=> wpjam_except($items, $key), $field, null, $key, 'delete'),
			fn()=> wpjam_call_method($this, 'after_delete_item', $key, $field)
		);
	}

	public function del_item($key, $field=''){
		return $this->delete_item($key, $field);
	}

	public function move_item($orders, $field=''){
		return $this->process_items(fn($items)=> array_merge(wpjam_pull($items, $orders), $items), $field);
	}

	public static function get_item_actions(){
		$args	= [
			'row_action'	=> false,
			'value_callback'=> fn()=> '',
			'data_callback'	=> fn($id)=> wpjam_try([static::class, 'get_item'], $id, static::get_param('i'), static::get_param('_field')),
			'callback'		=> fn($id, $data, $action)=> wpjam_try([static::class, $action], $id, static::get_param($action == 'move_item' ? 'item' : 'i'), ...[...($action == 'del_item' ? [] : [$data]), static::get_param('_field')])
		];

		return [
			'add_item'	=>['page_title'=>'新增项目',	'title'=>'新增',	'dismiss'=>true]+array_merge($args, ['data_callback'=> fn()=> []]),
			'edit_item'	=>['page_title'=>'修改项目',	'dashicon'=>'edit']+$args,
			'del_item'	=>['page_title'=>'删除项目',	'dashicon'=>'no-alt',	'class'=>'del-icon',	'direct'=>true,	'confirm'=>true]+$args,
			'move_item'	=>['page_title'=>'移动项目',	'dashicon'=>'move',		'class'=>'move-item',	'direct'=>true]+$args,
		];
	}
}

class WPJAM_Args implements ArrayAccess, IteratorAggregate, JsonSerializable{
	use WPJAM_Call_Trait;

	protected $args;

	public function __construct($args=[]){
		$this->args	= $args;
	}

	public function __get($key){
		$args	= $this->get_args();

		return wpjam_exists($args, $key) ? $args[$key] : ($key == 'args' ? $args : null);
	}

	public function __set($key, $value){
		$this->filter_args();

		$this->args[$key]	= $value;
	}

	public function __isset($key){
		return wpjam_exists($this->get_args(), $key) ?: ($this->$key !== null);
	}

	public function __unset($key){
		$this->filter_args();

		unset($this->args[$key]);
	}

	#[ReturnTypeWillChange]
	public function offsetGet($key){
		return $this->__get($key);
	}

	#[ReturnTypeWillChange]
	public function offsetSet($key, $value){
		$this->__set($key, $value);
	}

	#[ReturnTypeWillChange]
	public function offsetExists($key){
		return $this->__isset($key);
	}

	#[ReturnTypeWillChange]
	public function offsetUnset($key){
		$this->__unset($key);
	}

	#[ReturnTypeWillChange]
	public function getIterator(){
		return new ArrayIterator($this->get_args());
	}

	#[ReturnTypeWillChange]
	public function jsonSerialize(){
		return $this->get_args();
	}

	protected function filter_args(){
		return $this->args	= $this->args ?: [];
	}

	public function get_args(){
		return $this->filter_args();
	}

	public function update_args($args, $replace=null){
		$this->args	= $replace === true ? $args : ($replace === false ? 'wp_parse_args' : 'array_replace')($this->get_args(), $args);

		return $this;
	}

	public function process_arg($key, $cb){
		$value	= wpjam_call($cb, $this->get_arg($key));

		return is_null($value) ? $this->delete_arg($key) : $this->update_arg($key, $value);
	}

	public function get_arg($key, $default=null, $callback=false){
		$value	= wpjam_get($this->get_args(), $key);

		if($callback){
			$value	??= is_string($key) ? wpjam_callback([$this->model, 'get_'.$key]) : null;
			$value	= is_closure($value) ? $value->bindTo($this, $this) : $value;
			$value	= $callback === 'callback' ? maybe_callback($value, $this->name) : $value;
		}

		return $value ?? $default;
	}

	public function update_arg($key, $value){
		return $this->update_args(wpjam_set($this->get_args(), $key, $value), true);
	}

	public function delete_arg($key, ...$args){
		return $this->update_args(wpjam_except($this->get_args(), $key, ...$args), true);
	}

	public function pull($key, ...$args){
		$this->filter_args();

		return wpjam_pull($this->args, $key, ...$args);
	}

	public function pick($keys){
		return wpjam_pick($this, $keys);
	}

	public function to_array(){
		return $this->get_args();
	}

	public function sandbox($cb, ...$args){
		try{
			$archive	= $this->get_args();

			return wpjam_call($cb, ...$args);
		}finally{
			$this->args	= $archive;
		}
	}

	public function is_active(){
		return true;
	}

	protected function parse_method($name){
		$cb	= array_find([[$this->model, $name], $this->$name], fn($v)=> wpjam_callback($v));

		return is_closure($cb) ? $cb->bindTo($this, $this) : $cb;
	}

	public function call_method($name, ...$args){
		return ($cb = $this->parse_method($name)) ? wpjam_try($cb, ...$args) : (str_starts_with($name, 'filter_') ? array_shift($args) : null);
	}
}

class WPJAM_Register extends WPJAM_Args{
	use WPJAM_Items_Trait;

	public function __construct($name, $args=[]){
		$this->args	= $args	= ['name'=>$name]+$args;

		if($this->is_active() || !empty($args['active'])){
			$config	= static::registry('get_arg', 'config') ?: [];
			$model	= empty($config['model']) ? '' : ($args['model'] ?? '');
			$file	= ($model || !empty($args['hooks']) || !empty($args['init'])) ? wpjam_pull($args, 'file') : '';

			$file && is_file($file) && include_once $file;

			if($model){
				if(is_subclass_of($model, self::class)){
					trigger_error('「'.(is_object($model) ? get_class($model) : $model).'」是 '.self::class.' 子类');
				}

				if($config['model'] === 'object' && !is_object($model)){
					if(class_exists($model, true)){
						$model = $args['model']	= new $model(array_merge($args, ['object'=>$this]));
					}else{
						trigger_error('model 无效');
					}
				}

				foreach(['hooks'=>'add_hooks', 'init'=>'init'] as $k => $m){
					if(($args[$k] ?? ($k == 'hooks' || ($config[$k] ?? false))) === true){
						$args[$k] = wpjam_callback([$model, $m]);
					}
				}
			}
		}

		$this->args	= $args;
	}

	protected function filter_args(){
		$name	= $this->args['name'];

		return in_array($name, static::registry('get_arg', 'filtered[]'))
		? $this->args
		: ($this->args = apply_filters(static::registry('update_arg', 'filtered[]', $name)->name.'_args', $this->args, $name));
	}

	public function get_arg($key, $default=null, $callback=true){
		return parent::get_arg($key, $default, $callback ? 'callback' : 'parse');
	}

	public function get_parent(){
		return $this->sub_name ? self::get($this->name) : null;
	}

	public function get_sub($name){
		return self::get($this->name.':'.$name);
	}

	public function get_subs(){
		return wpjam_array(self::get_by(['name'=>$this->name]), fn($k, $v)=> $v->sub_name);
	}

	public function register_sub($name, $args){
		return self::register($this->name.':'.$name, new static($this->name, array_merge($args, ['sub_name'=>$name])));
	}

	public function unregister_sub($name){
		return self::unregister($this->name.':'.$name);
	}

	public static function registry($method, ...$args){
		$called		= static::class;
		$registry	= wpjam_registry($called);

		if(!$registry->config){
			$registry->config	= wpjam_get_annotation($called, 'config')+['model'=>true];
			$registry->defaults	= method_exists($called, 'get_defaults') ? static::get_defaults() : [];
		}

		return wpjam_catch([$registry, $method], ...$args);
	}

	public static function register($name, $args=[]){
		return static::registry('add_object', $name, $args);
	}

	public static function unregister($name, $args=[]){
		static::registry('remove_object', $name, $args);
	}

	public static function get_registereds($args=[], $output='objects', $operator='and'){
		wpjam_map(static::registry('pull', 'defaults'), [static::class, 'register'], 'kv');

		$objects	= static::registry('get_objects', $args, $operator);

		return $output == 'names' ? array_keys($objects) : $objects;
	}

	public static function get_by(...$args){
		return self::get_registereds(...((!$args || is_array($args[0])) ? $args : [[$args[0]=> $args[1]]]));
	}

	public static function get($name, $by='registered', $top=''){
		if($name && $by == 'registered'){
			return static::registry('get_object', $name) ?: static::register($name, static::registry('pull', 'defaults['.$name.']'));
		}

		if($name && $by == 'model' && strcasecmp($name, $top) !== 0){
			return array_find(static::get_registereds(), fn($v)=> is_string($v->model) && strcasecmp($name, $v->model) === 0) ?: static::get(get_parent_class($name), $by, $top);
		}
	}

	public static function get_setting_fields($args=[]){
		$objects	= array_filter(static::get_registereds(wpjam_pull($args, 'filter_args')), fn($v)=> !isset($v->active));
		$options	= wpjam_options($objects, $args);

		if(($args['type'] ?? '') == 'select'){
			$name	= wpjam_pull($args, 'name');
			$args	+= ['options'=>$options];

			return $name ? [$name => $args] : $args;
		}

		return $options;
	}

	public static function get_active($key=null){
		return wpjam_array(static::get_registereds(), fn($k, $v)=> ($v->active ?? $v->is_active()) ? [$k, $key ? $v->get_arg($key) : $v] : null, true);
	}

	public static function by_active(...$args){
		return self::call_active((did_action(current_filter()) ? 'on_' : 'filter_').wpjam_suffix(current_filter(), '-', 'wpjam_'), ...$args);
	}

	public static function call_active($method, ...$args){
		$type	= array_find(['filter', 'get'], fn($t)=> str_starts_with($method, $t.'_'));

		foreach(static::get_active() as $object){
			$res	= $object->call_method($method, ...$args);

			if($type == 'filter'){
				$args[0]	= $res;
			}elseif($type == 'get'){
				$return		= array_merge($return ?? [], is_array($res) ? $res : []);
			}
		}

		if($type == 'filter'){
			return $args[0];
		}elseif($type == 'get'){
			return $return ?? [];
		}
	}
}

class WPJAM_Registry extends WPJAM_Args{
	public function add_object(...$args){
		$count	= count($this->objects ?: []);
		$called	= $this->called ?: 'WPJAM_Args';

		if(is_object($args[0]) || is_array($args[0])){
			$v	= $args[0];
			$k	= is_object($v) ? $v->name : (wpjam_pull($v, 'name') ?: ($args[1] ?: '__'.$count));
		}else{
			[$k, $v]	= $args;
		}

		if(is_null($v)){
			return;
		}

		if($e = $k ? (is_numeric($k) ? '「'.$k.'」为纯数字' : (is_string($k) ? '' : '「'.var_export($k, true).'」不为字符串')) : '为空'){
			return trigger_error($this->name.'的注册 name'.$e) && false;
		}

		if(is_array($v)){
			if(!empty($v['admin']) && !is_admin()){
				return;
			}

			$v	= new $called(...(is_subclass_of($called, 'WPJAM_Register') ? [$k, $v] : [$v+['name'=>$k]]));
		}

		$this->get_object($k) && trigger_error($this->name.'「'.$k.'」已经注册。');

		$this->update_arg('objects['.$k.']', $v);

		if(wpjam_call_method($v, 'is_active') || $v->active){
			wpjam_hooks(maybe_callback($v->pull('hooks')));

			wpjam_init($v->pull('init'));

			wpjam_call_method($v, 'registered');
		}

		$count == 0 && wpjam_hooks($called.'::add_hooks');

		return $v;
	}

	public function remove_object($k){
		$this->delete_arg('objects['.$k.']');
	}

	public function get_object($k){
		return $this->get_arg('objects['.$k.']');
	}

	public function get_objects(...$args){
		$objects	= wpjam_filter($this->objects ?: [], ...$args);
		$by			= $this->get_arg('config[orderby]');

		return $by ? wpjam_sort($objects, ($by === true ? 'order' : $by), ($this->get_arg('config[order]') ?? 'DESC'), 10) : $objects;
	}

	public function on(...$args){
		foreach($this as $name => $registry){
			foreach(current_action() == 'wpjam_api' ? ['register_json'] : ['menu_page', 'admin_load'] as $key){
				if($registry->get_arg('config['.$key.']')){
					if($key == 'register_json'){
						[$registry->called, 'call_active']('register_json', $args[0]);
					}else{
						wpjam_add($key, array_values([$registry->called, 'get_active']($key)));
					}
				}
			}
		}
	}

	public static function get_instance($name, $args=[]){
		static $object;
		$object	??= new static();

		if(!$object->get_args()){
			wpjam_hooks('wpjam_api, wpjam_admin_init', [$object, 'on']);
		}

		return $object->$name ??= new static(['name'=>$name, 'called'=> is_subclass_of($name, 'WPJAM_Args') ? $name : null]+$args);
	}
}

class WPJAM_Data_Type extends WPJAM_Args{
	public function get_path($args, $item){
		return wpjam_if_error(wpjam_call_method($this->model.'::get_path', $args, $item), 'throw');
	}

	public function with_field($action, $value, $field){
		$res	= ($cb = $this->model.'::with_field') && wpjam_callback($cb) ? wpjam_try($cb, $action, $field, $value) : ($this->{$action.'_value'} ? $this->call($action.'_value', $value, $field) : $value);

		return $action == 'validate' && is_null($res) ? wpjam_throw('invalid_field_value', $field->_title.'「'.$value.'」的值无效') : $res;
	}

	public function query_label($value){
		if($value && $this->model && $this->label_field){
			if(is_array($value)){
				wpjam_call_method($this->model.'::update_caches', $value);

				return array_map(fn($v)=> ($l = $this->query_label($v)) ? ['label'=>$l, 'value'=>$v] : $v, $value);
			}

			return ($this->model::get($value) ?: [])[$this->label_field] ?? null;
		}
	}

	public function query_items($args){
		$args	= array_filter($args ?: [], fn($v)=> !is_null($v))+['number'=>10, 'data_type'=>true];

		if($this->query_items){
			return wpjam_try($this->query_items, $args);
		}

		if($this->model){
			$args	= isset($args['model']) ? wpjam_except($args, ['data_type', 'model', 'label_field', 'id_field']) : $args;
			$res	= wpjam_try($this->model.'::query_items', $args, 'items');
			$items	= wp_is_numeric_array($res) ? $res : ($res['items'] ?? []);

			return $this->label_field ? wpjam_column($items, ['label'=>$this->label_field, 'value'=>$this->id_field]) : $items;
		}

		return [];
	}

	public function nonce_action($args){
		return $this->name.':'.wpjam_json_encode(wpjam_sort(wpjam_except($args, ['search', 'exclude', 'parent']), 'k'));
	}

	public static function register($name, $args=[]){
		return wpjam_register(static::class, $name, $args);
	}

	public static function get_instance($name, $args=[]){
		if(!$name){
			return;
		}

		$model	= '';

		if($name == 'model'){
			$model	= $args[$name] ?? '';
			$name	= $model && class_exists($model) ? wpjam_join(':', $model, $args['label_field'], $args['id_field']) : '';
		}

		if($object = wpjam_get_registered(static::class, $name) ?? self::register($name, $model ? $args : ([
			'post_type'	=> ['model'=>'WPJAM_Post',	'schema'=>['type'=>'integer'],	'label_field'=>'post_title',	'id_field'=>'ID'],
			'taxonomy'	=> ['model'=>'WPJAM_Term',	'schema'=>['type'=>'integer'],	'label_field'=>'name',		'id_field'=>'term_id'],
			'author'	=> ['model'=>'WPJAM_User',	'schema'=>['type'=>'integer'],	'label_field'=>'display_name',	'id_field'=>'ID'],
			'video'		=> ['parse_value'=>'wpjam_get_video_mp4'],
		][$name] ?? null))){
			if($model){
				$object->validate_value	??= fn($v)=> wpjam_try($args['model'].'::get', $v) ? $v : null;
			}

			if($object->model){
				$object->meta_type	??= wpjam_call_method($object->model.'::get_meta_type') ?: '';
			}
		}

		return $object;
	}
}

class WPJAM_JSON extends WPJAM_Register{
	public function __invoke(){
		$method		= $this->method ?: $_SERVER['REQUEST_METHOD'];
		$attr		= $method != 'POST' && !str_ends_with($this->name, '.config') ? ['page_title', 'share_title', 'share_image'] : [];
		$response	= wpjam_try('apply_filters', 'wpjam_pre_json', [], $this, $this->name);
		$response	+= ['errcode'=>0, 'current_user'=>wpjam_try('wpjam_get_current_user', $this->pull('auth'))]+$this->pick($attr);

		if($this->modules){
			$modules	= maybe_callback($this->modules, $this->name, $this->args);
			$results	= array_map([static::class, 'parse_module'], wp_is_numeric_array($modules) ? $modules : [$modules]);
		}elseif($this->callback){
			$fields		= wpjam_try('maybe_callback', $this->fields ?: [], $this->name);
			$data		= $this->fields ? ($fields ? wpjam_params($method, $fields) : []) : $this->args;
			$results[]	= wpjam_try($this->pull('callback'), $data, $this->name);
		}elseif($this->template){
			$results[]	= is_file($this->template) ? include $this->template : '';
		}else{
			$results[]	= wpjam_except($this->args, 'name');
		}

		foreach($results as $v){
			if(is_array($v)){
				$response	+= wpjam_pull($v, $attr);
				$response	= array_merge($response, $v);
			}
		}

		$response	= apply_filters('wpjam_json', $response, $this->args, $this->name);

		foreach($attr as $k){
			if(($v = $response[$k] ?? '') || $k != 'share_image'){
				$response[$k]	= $k == 'share_image' ? wpjam_get_thumbnail($v, '500x400') : html_entity_decode($v ?: wp_get_document_title());
			}
		}

		return $response;
	}

	public static function redirect($name){
		header('X-Content-Type-Options: nosniff');

		rest_send_cors_headers(false);

		if('OPTIONS' === $_SERVER['REQUEST_METHOD']){
			status_header(403);
			exit;
		}

		ini_set('display_errors', 0);

		wpjam_hook('wp_die_'.(array_find(['jsonp_', 'json_'], fn($v)=> wpjam_call('wp_is_'.$v.'request')) ?: '').'handler', fn()=> 'wpjam_die_handler');

		if(!try_prefix($name, '-', 'mag.')){
			return;
		}

		$name	= wpjam_prefix($name, '-', 'mag.');	// 兼容
		$name	= str_replace('/', '.', $name);

		($user = wpjam_get_current_user()) && ($user_id = $user['user_id'] ?? '') && wp_set_current_user($user_id);

		do_action('wpjam_api', wpjam_var('json', $name));

		wpjam_send_json(wpjam_catch(self::get($name) ?: wp_die('接口未定义', 'invalid_api')));
	}

	public static function get_defaults(){
		return array_fill_keys(['post.list', 'post.calendar', 'post.get'], ['modules'=>[static::class, 'callback']])+[
			'media.upload'	=> ['modules'=>['callback'=>[static::class, 'media']]],
			'site.config'	=> ['modules'=>['type'=>'config']],
		];
	}

	public static function get_current(){
		return wpjam_var('json');
	}

	public static function get_rewrite_rule(){
		return [
			['api/([^/]+)/(.*?)\.json?$',	['module'=>'json', 'action'=>'mag.$matches[1].$matches[2]'], 'top'],
			['api/([^/]+)\.json?$', 		'index.php?module=json&action=$matches[1]', 'top'],
		];
	}

	public static function parse_module($module){
		$type	= wpjam_pull($module, 'type');
		$cb		= wpjam_pull($module, 'callback');
		$args	= $module['args'] ?? $module;
		$args	= is_array($args) ? $args : wpjam_parse_shortcode_attr(stripslashes_deep($args), 'module');

		if($cb){
			return wpjam_try($cb, $args);
		}

		if($type == 'data_type'){
			return wpjam_query('data_type', $args+['search'=>wpjam_param('s')]);
		}elseif($type == 'setting'){
			return wpjam_setting($args);
		}elseif($type == 'config'){
			return wpjam_config($args['group'] ?? '');
		}elseif(in_array($type, ['post_type', 'taxonomy', 'media'])){
			return wpjam_try(static::class.'::'.$type, $args);
		}
	}

	public static function post_type($args){
		$action	= wpjam_pull($args, 'action');
		$output	= wpjam_pull($args, 'output');

		if($action == 'upload'){
			return wpjam_media($args['media'] ?? '', ['media'=>true, 'output'=>$output, 'post_id'=>(int)wpjam_param('post_id', 'post')]);
		}

		$wp		= $GLOBALS['wp'];
		$query	= $GLOBALS['wp_query'];
		$object	= wpjam_get_json('object');
		$vars	= $object->query_vars ??= $wp->query_vars;

		if(in_array($action, ['list', 'calendar'])){
			$sub	= $action == 'calendar' ? false : wpjam_pull($args, 'sub');
			$args	= array_diff_key($args, WPJAM_Post::get_default_args());

			if($sub){
				$query	= wpjam_query($args);
			}else{
				$vars	= array_merge(wpjam_except($vars, ['module', 'action']), $args);
				$args	+= ['format'=>($action == 'calendar' ? 'date' : $action)];

				$wp->query_vars	= $vars = wpjam_parse_query_vars($vars, $action);
				$wp->query_posts();
			}

			$parsed	= wpjam_get_posts($query, $args);
			$result	= [];

			if(!$sub && $action != 'calendar'){
				$is		= wpjam_is($query);
				$paged	= $query->get('nopaging') ? 0 : ($query->get('paged') ?: 1);
				$result	= (is_string($is) ? ['is'=>$is] : [])+['total'=>$query->found_posts];
				$result	+= ['total_pages'=>$paged ? $query->max_num_pages : ($result['total'] ? 1 : 0), 'current_page'=>$paged ?: 1];

				if($paged == 1 && !$query->get('s') && in_array($query->get('orderby') ?: 'date', ['date', 'post_date'], true)){
					$result	+= ['next_cursor'=>($parsed && $result['total_pages'] > 1) ? array_last($parsed)['timestamp'] : 0];
				}

				if($queried	= $query->get_queried_object()){
					if(in_array($is, ['category', 'tag', 'tax'], true)){
						$result	+= ['current_taxonomy'=>$queried->taxonomy, 'current_term'=>wpjam_get_term($queried, $queried->taxonomy)];
					}elseif($is === 'author'){
						$result	+= ['current_author'=>wpjam_get_user($query->get('author'))];
					}elseif($is === 'post_type_archive'){
						$result	+= ['current_post_type'=>$queried->name];
					}
				}
			}

			if(!$output && !$sub && ($type = $query->get('post_type')) && is_string($type)){
				$output	= wpjam_get_post_type_setting($type, 'plural');
			}

			$output	= $output ?: 'posts';

			return apply_filters('wpjam_posts_json', $result+[$output=>$parsed], $query, $output);
		}elseif($action == 'get'){
			$type	= $args['post_type'] ??= '';
			$type	= $type === 'any' ? '' : $type;
			$status	= $args['post_status'] ?? '';
			$vars	= ['cache_results'=>true]+($status ? ['status'=>$status] : [])+$vars;

			if($type){
				!post_type_exists($type) && wp_die('invalid_post_type');

				$name	= $vars[(is_post_type_hierarchical($type) ? 'pagename' : 'name')] ?? '';
			}else{
				$map	= wp_list_pluck(get_post_types(['_builtin'=>false, 'query_var'=>true], 'objects'), 'query_var')+['post'=>'name', 'page'=>'pagename'];

				$type	= array_find_key($map, fn($v)=> !empty($vars[$v]));
				$name	= $type ? $vars[$map[$type]] : null;
			}

			if(!$name){
				$id		= ($args['id'] ?? 0) ?: (int)wpjam_param('id', ['required'=>true]);
				$type	??= get_post_type($id);
				$vars	= ['p'=>($type && (get_post_type($id) == $type)) ? $id : wp_die('invalid_post_id')]+$vars;
			}

			$vars['post_type']	= $type;
			$wp->query_vars		= $vars;
			$wp->query_posts();

			if(empty($id) && !$status && !$query->have_posts()){
				$id	= apply_filters('old_slug_redirect_post_id', null) ?: wp_die('invalid_post_id');

				$wp->query_vars	= ['post_type'=>'any', 'p'=>$id]+wpjam_except($vars, ['name', 'pagename']);
				$wp->query_posts();
			}

			$parsed	= $query->have_posts() ? array_first(wpjam_get_posts($query, $args)) : wp_die('invalid_parameter');
			$output	= $output ?: $parsed['post_type'];

			return wpjam_pull($parsed, ['share_title', 'share_image', 'share_data'])+[$output => $parsed];
		}
	}

	public static function taxonomy($args){
		$object		= wpjam_get_taxonomy(wpjam_get($args, 'taxonomy')) ?: wpjam_throw('invalid_taxonomy');
		$mapping	= wpjam_array(wp_parse_args(wpjam_pull($args, 'mapping') ?: []), fn($k, $v)=> [$k, wpjam_param($v)], true);

		return [(wpjam_pull($args, 'output') ?: $object->plural) => array_values(wpjam_query('term', array_merge($args, $mapping)))];
	}

	public static function media($args){
		return wpjam_media($args['media'] ?? '', $args);
	}

	public static function callback($name, $args=[]){
		$output	= $args['output'] ?? null;

		[$type, $action]	= explode('.', $name);

		if($type == 'post'){
			$type	= wpjam_param('post_type');
			$output	??= $action == 'get' ? 'post' : 'posts';
		}

		$modules[]	= array_filter(['type'=>'post_type', 'post_type'=>$type, 'action'=>$action, 'output'=>$output]+array_intersect_key($args, WPJAM_Post::get_default_args()), fn($v)=> !is_null($v));

		foreach($action == 'list' && $type && is_string($type) && !str_contains($type, ',') ? get_object_taxonomies($type) : [] as $tax){
			if(is_taxonomy_hierarchical($tax) && wpjam_get_taxonomy_setting($tax, 'show_in_posts_rest')){
				$modules[]	= ['type'=>'taxonomy', 'taxonomy'=>$tax, 'hide_empty'=>0];
			}
		}

		return $modules;
	}

	public static function add_hooks(){
		wpjam_is_json_request() && wpjam_hooks('-', [
			['the_title',		'convert_chars'],
			['init',			['wp_widgets_init', 'maybe_add_existing_user_to_blog']],
			['wp_loaded',		['_custom_header_background_just_in_time', '_add_template_loader_filters']],
			['plugins_loaded',	['wp_maybe_load_widgets', 'wp_maybe_load_embeds', '_wp_customize_include', '_wp_theme_json_webfonts_handler']]
		]);
	}

	public static function __callStatic($method, $args){
		if(in_array($method, ['parse_post_list_module', 'parse_post_get_module'])){
			return wpjam_catch('WPJAM_JSON::parse_module', [
				'type'		=> 'post_type',
				'action'	=> str_replace(['parse_post_', '_module'], '', $method)
			]+($args[0] ?? []));
		}
	}
}

/**
* @config menu_page, admin_load, register_json, init, orderby
**/
#[config('menu_page', 'admin_load', 'register_json', 'init', 'orderby')]
class WPJAM_Option_Setting extends WPJAM_Register{
	public function __invoke(){
		flush_rewrite_rules();

		$submit	= $_POST['submit'] ?? '';
		$values	= $this->validate_by_fields(wpjam_params('data')) ?: [];
		$fix	= is_network_admin() ? 'site_option' : 'option';

		if($this->option_type == 'array'){
			if($submit == 'reset'){
				$values	= wpjam_diff([$this, 'get_'.$fix](), $values, 'key');
			}else{
				$values	= wpjam_filter(array_merge([$this, 'get_'.$fix](), $values), fn($v)=> !is_null($v), true);
				$values	= $this->call_method('sanitize_callback', $values, $this->name) ?? $values;
			}

			$cb	= $this->update_callback;
			$cb ? $cb($this->name, $values) : [$this, 'update_'.$fix]($values);
		}else{
			wpjam_map($values, ...($submit == 'reset' ? ['delete_'.$fix, 'k'] : ['update_'.$fix, 'kv']));
		}

		$errors	= array_filter(get_settings_errors(), fn($e)=> !in_array($e['type'], ['updated', 'success', 'info']));
		$errors	&& wp_die(implode('&emsp;', array_column($errors, 'message')));

		return [
			'type'		=> (!$this->ajax || $submit == 'reset') ? 'redirect' : $submit,
			'notice'	=> $submit == 'reset' ? '设置已重置。' : '设置已保存。'
		];
	}

	public function __call($method, $args){
		$parts	= explode('_', $method);
		$action	= array_shift($parts);
		$type	= array_pop($parts);

		if($type == 'fields'){
			$fields	= $action == 'render' ? array_shift($args) : array_merge(...array_column($this->get_sections($action !== 'validate'), 'fields'));

			return $action == 'get' ? $fields : wpjam_fields($fields, ['value_callback'=>[$this, 'value_callback']])->$action(...$args);
		}elseif($type == 'option' || $type == 'setting'){
			$part	= $parts ? '_'.$parts[0] : '';

			if($part != '_site'){
				$blog_id	= $part == '_blog' ? array_shift($args) : $this->blog_id;
				$part		= $blog_id && is_multisite() ? '_blog' : '';
			}

			$params	= $part == '_blog' ? [$blog_id] : [];

			if($type == 'option'){
				$args	= $action == 'update' ? [wpjam_if_error($args[0], [])] : [];
				$res	= wpjam_call($action.$part.'_option', ...[...$params, $this->name, ...$args]);

				return $action == 'get' ? (wpjam_if_error($res, []) ?: []) : $res;
			}

			$name	= $args[0] ?? null;
			$data	= [$this, 'get'.$part.'_option'](...$params) ?: [];

			if($action == 'get'){
				if(is_array($name)){
					return wpjam_fill(array_filter($name), fn($n)=> [$this, 'get'.$part.'_setting'](...[...$params, $n]));
				}

				$site_default	= $part != '_site' && is_multisite() && $this->site_default;

				if(!$name){
					return $data+($site_default ? $this->get_site_option() : []);
				}

				$value	= wpjam_if_error(wpjam_get(is_array($data) ? $data : [], $name), null);

				if(is_null($value)){
					if($site_default){
						return $this->get_site_setting(...$args);
					}

					if(count($args) >= 2){
						return $args[1];
					}

					if($this->field_default){
						return wpjam_get(($this->_defaults ??= $this->get_defaults_by_fields()), $name);
					}
				}

				return is_string($value) ? str_replace("\r\n", "\n", trim($value)) : $value;
			}

			return [$this, 'update'.$part.'_option'](...[...$params, ($action == 'update'
				? wpjam_reduce(is_array($name) ? $name : [$name=>$args[1]], fn($c, $v, $n)=> wpjam_set($c, $n, $v), $data)
				: wpjam_except($data, $name)
			)]);
		}
	}

	public function get_arg($key, $default=null, $callback=true){
		$value	= parent::get_arg($key, $default, $callback);

		if($key == 'menu_page'){
			if(!$this->name || (is_network_admin() && !$this->site_default)){
				return;
			}

			if(!$value){
				if(!$this->post_type || !$this->title){
					return $value;
				}

				$value	= ['parent'=>wpjam_get_post_type_setting($this->post_type, 'plural'), 'order'=>1];
			}

			if(wp_is_numeric_array($value)){
				return wpjam_array($value, function($k, $v){
					if(!empty($v['tab_slug']) && !empty($v['plugin_page'])){
						return [$k, $v];
					}elseif(!empty($v['menu_slug'])){
						return [$k, $v+($v['menu_slug'] == $this->name ? ['menu_title'=>$this->title] : [])];
					}
				});
			}

			$value	+= ($value['function'] ??= 'option') == 'option' ? ['option_name'=>$this->name] : [];

			if(!empty($value['tab_slug'])){
				return ($value['plugin_page'] ??= $this->plugin_page) ? $value+['title'=>$this->title] : null;
			}

			$value	+= ['menu_slug'=>$this->plugin_page ?: $this->name, 'menu_title'=>$this->title];
		}elseif($key == 'admin_load'){
			$value	= wp_is_numeric_array($value) ? $value : ($value ? [$value] : []);
			$value	= array_map(fn($v)=> ($this->model && !isset($v['callback']) && !isset($v['model'])) ? $v+['model'=>$this->model] : $v, $value);
		}elseif($key == 'sections'){
			if(!$value || !is_array($value)){
				$id		= $this->type == 'section' ? (string)$this->section_id : ($this->current_tab ?: $this->sub_name ?: $this->name);
				$value	= [$id=>array_filter(['fields'=>$this->get_arg('fields', null, false)]) ?: $this->get_arg('section') ?: []];
			}

			$value	= wpjam_array($value, fn($k, $v)=> is_array($v) && isset($v['fields']) ? [$k, ['fields'=>maybe_callback($v['fields'] ?? [], $k, $this->name)]+$v] : null);
		}

		return $value;
	}

	public function get_current($output=''){
		$args	= wpjam_pick(wpjam_admin('vars'), ['plugin_page', 'current_tab']);

		return $output === 'args' ? $args : ($this->get_sub(wpjam_join(':', $args)) ?: $this);
	}

	protected function get_sections($all=false, $filter=true){
		$sections	= $this->get_arg('sections');
		$sections	= count($sections) == 1 ? array_map(fn($s)=> $s+['title'=>$this->title ?: ''], $sections) : $sections;
		$sections	= array_reduce($all ? $this->get_subs() : [], fn($c, $v)=> array_merge($c, $v->get_sections(false, false)), $sections);

		if(!$filter){
			return $sections;
		}

		$args		= $all ? [] : wpjam_map(self::get_current('args'), fn($v)=> ['value'=>$v, 'if_null'=>true]);
		$objects	= wpjam_sort(self::get_by(['type'=>'section', 'name'=>$this->name]+$args), 'order', 'desc', 10);

		foreach(array_reverse(array_filter($objects, fn($v)=> $v->order > 10))+$objects as $object){
			foreach(($object->get_arg('sections') ?: []) as $id => $section){
				$id			= $id ?: array_key_first($sections);
				$exist		= isset($sections[$id]) ? ($object->order > 10 ? wpjam_merge($section, $sections[$id]) : $sections[$id]) : [];	// 字段靠前
				$sections	= wpjam_set($sections, $id, wpjam_merge($exist, $section));
			}
		}

		return apply_filters('wpjam_option_setting_sections', array_filter($sections, fn($v)=> isset($v['title'], $v['fields'])), $this->name);
	}

	public function add_section(...$args){
		$keys	= ['model', 'fields', 'section'];
		$args	= is_array($args[0]) ? $args[0] : ['section_id'=>$args[0]]+(array_any($keys, fn($k)=> isset($args[1][$k])) ? $args[1] : ['fields'=>$args[1]]);
		$args	= array_any([...$keys, 'sections'], fn($k)=>isset($args[$k])) ? $args : ['sections'=>$args];

		return self::register(md5(wpjam_serialize($args)), new static($this->name, $args+['type'=>'section']));
	}

	public function value_callback($name=''){
		return $this->option_type == 'array' ? (is_network_admin() ? $this->get_site_setting($name) : $this->get_setting($name)) : get_option($name, null);
	}

	public function render($page){
		$nav	= $page->tab_slug ? '' : wpjam_tag('h2', ['nav-tab-wrapper', 'wp-clearfix']);
		$form	= wpjam_tag('form', ['method'=>'POST', 'id'=>'wpjam_option', 'novalidate']);
		$tabs	= [];

		foreach($this->get_sections() as $id => $sec){
			$nav && $nav->append('a', ['class'=>'nav-tab', 'href'=>'#tab_'.$id, 'data'=>wpjam_pick($sec, ['show_if'])], $sec['title']);

			$tabs[]	= wpjam_tag($nav ? 'div' : '', ['id'=>'tab_'.$id, 'class'=>'tab'])->append([
				[$nav ? 'h2' : 'h3', [], $sec['title']],
				wpjam_ob($sec['callback'] ?? '', $sec),
				wpautop($sec['summary'] ?? ''),
				$this->render_fields($sec['fields'])
			]);
		}

		$form->data('nonce', wp_create_nonce($this->option_group))->append([
			...(count($tabs) == 1 ? [$tabs[0]->tag('')] : $tabs),
			wpjam_tag('p', ['submit'])->append([
				get_submit_button('', 'primary', 'save', false),
				$this->reset ? get_submit_button('重置选项', 'secondary', 'reset', false) : ''
			])
		]);

		return count($tabs) > 1 && $nav ? $form->before($nav)->wrap('div', ['tabs']) : $form;
	}

	public function page_load(){
		wpjam_ajax('wpjam-option-action',	[
			'admin'			=> true,
			'callback'		=> $this,
			'nonce_action'	=> fn()=> $this->option_group,
			'allow'			=> fn()=> current_user_can($this->capability)
		]);
	}

	public static function create($name, $args){
		$args	= maybe_callback($args, $name)+[
			'option_group'	=> $name,
			'option_page'	=> $name,
			'option_type'	=> 'array',
			'capability'	=> 'manage_options',
			'ajax'			=> true
		];

		if($sub = wpjam_pick($args, ['plugin_page', 'current_tab'])){
			$rest	= wpjam_except($args, ['model', 'menu_page', 'admin_load', 'plugin_page', 'current_tab']);
		}else{
			$args	= ['primary'=>true]+$args;
		}

		if($object = self::get($name)){
			if(!$sub && $object->primary && trigger_error('option_setting'.'「'.$name.'」已经注册。'.var_export($args, true))){
				return $object;
			}

			$object->update_args($sub ? wpjam_except($rest, 'title') : $args);
		}else{
			$args['option_type'] == 'array' && !doing_filter('sanitize_option_'.$name) && is_null(get_option($name, null)) && add_option($name, []);

			$object	= self::register($name, $sub ? $rest : $args);
		}

		return $sub ? $object->register_sub(wpjam_join(':', $sub), $args) : $object;
	}

	public static function get_instance($name, ...$args){
		if($args && in_array($args[0], ['model', 'registered'], true)){
			return self::get($name, ...$args);
		}elseif($args && $args[0] === 'object'){
			array_shift($args);
		}

		$blog_id	= is_multisite() ? ($args[0] ?? 0) : 0;
		$object		= $blog_id ? (!is_numeric($blog_id) && trigger_error($name.':'.$blog_id) && false) : self::get($name);

		return $object ?: wpjam_var('option:'.wpjam_join('-', $name, $blog_id), fn()=> new static($name, ['blog_id'=>$blog_id]));
	}
}

class WPJAM_Meta_Type extends WPJAM_Register{
	public function __call($method, $args){
		if(in_array($method, ['get_data', 'add_data', 'update_data', 'delete_data', 'data_exists'])){
			$args	= [$this->name, ...$args];
			$cb		= str_replace('data', 'metadata', $method);
		}elseif($this->mod($method, '_by_mid')){
			$args	= [$this->name, ...$args];
			$cb		= $method.'_metadata_by_mid';
		}elseif($this->mod($method, '_meta')){
			$cb		= [$this, $method.'_data'];
		}elseif($this->mod($method, 'meta')){
			$cb		= [$this, $method];
		}

		return $cb(...$args);
	}

	public function get_options($args=[]){
		if($this->name == 'post'){
			$key	= 'post_type';
			$cb 	= 'wpjam_get_post_type_object';
		}elseif($this->name == 'term'){
			$key	= 'taxonomy';
			$cb 	= 'wpjam_get_taxonomy';
		}

		if(isset($key) && isset($args[$key])){
			$args[$key]	= ['value'=>($value = $args[$key]), 'if_null'=>true, 'callable'=>true];
			$object		= $cb($value);

			$object && ($this->call_option($value.'_base') || $this->call_option($value.'_base', [
				$key			=> $value,
				'title'			=> $this->name == 'post' ? '基础信息' : '快速编辑',
				'row_action'	=> $this->name == 'term',
				'action_name'	=> 'set',
				'page_title'	=> '设置'.$object->title,
				'fields'		=> [$object, 'get_fields'],
				'list_table'	=> $object->show_ui,
				'order'			=> 99,
			]));
		}

		if(isset($args['list_table'])){
			$args['title']		= true;
			$args['list_table']	= $args['list_table'] ? true : ['compare'=>'!==', 'value'=>'only'];
		}

		return wpjam_sort(wpjam_filter($this->get_arg('options[]'), $args), 'order', 'DESC', 10);
	}

	public function call_option($name, ...$args){
		$del	= try_prefix($name, '-', '-');
		$key	= 'options['.$name.']';

		if($del){
			return $this->delete_arg($key) && true;
		}

		if($args){
			$args	= $args[0];

			if($this->name == 'post'){
				$args	+= ['fields'=>[], 'priority'=>'default'];

				$args['post_type']	??= wpjam_pull($args, 'post_types') ?: null;
			}elseif($this->name == 'term'){
				$args['taxonomy']	??= wpjam_pull($args, 'taxonomies') ?: null;

				if(!isset($args['fields'])){
					$args['fields']		= [$name => wpjam_except($args, 'taxonomy')];
					$args['from_field']	= true;
				}
			}

			$this->update_arg($key, new WPJAM_Meta_Option(['name'=>$name, 'meta_type'=>$this->name]+$args));
		}

		return $this->get_arg($key);
	}

	public function get_table(){
		return _get_meta_table($this->name);
	}

	public function get_column($name='object_id'){
		return $name == 'id'
		? ('user' == $this->name ? 'umeta_id' : 'meta_id')
		: ($name == 'object_id' ? $this->name.'_id' : $name);
	}

	public function get_data_with_default($id, ...$args){
		if(!$args){
			return $this->get_data($id);
		}

		if($id && $args[0]){
			if(is_array($args[0])){
				return wpjam_array($args[0], fn($k, $v)=> [is_numeric($k) ? $v : $k, $this->get_data_with_default($id, ...(is_numeric($k) ? [$v, null] : [$k, $v]))]);
			}

			if($args[0] == 'meta_input'){
				trigger_error('meta_input');
				return wpjam_map($this->get_data($id), fn($v, $k)=> $this->get_data($id, $k, true));
			}

			if($this->data_exists($id, $args[0])){
				return $this->get_data($id, $args[0], true);
			}
		}

		return is_array($args[0]) ? [] : ($args[1] ?? null);
	}

	public function get_by_key($key, $value=null, $column=null){
		global $wpdb;

		$where	= array_filter([
			$key ? $wpdb->prepare('meta_key=%s', $key) : '',
			is_null($value) ? '' : $wpdb->prepare('meta_value=%s', maybe_serialize($value))
		]);

		if($where){
			$table	= $this->get_table();
			$where	= implode(' AND ', $where);
			$data	= $wpdb->get_results("SELECT * FROM {$table} WHERE {$where}", ARRAY_A) ?: [];

			return $data && $column ? array_first($data)[$this->get_column($column)] : array_map(fn($v)=> ['meta_value'=>maybe_unserialize($v['meta_value'])]+$v, $data);
		}

		return $column ? null : [];
	}

	public function update_data_with_default($id, $key, ...$args){
		if(is_array($key)){
			if(wpjam_is_assoc_array($key)){
				$defaults	= $args && is_array($args[0]) ? $args[0] : [];

				if(isset($key['meta_input']) && wpjam_is_assoc_array($key['meta_input'])){
					$this->update_data_with_default($id, wpjam_pull($key, 'meta_input'), wpjam_pull($defaults, 'meta_input'));
				}

				wpjam_map($key, fn($v, $k)=> $this->update_data_with_default($id, $k, $v, wpjam_pull($defaults, $k)));
			}

			return true;
		}

		[$value, $default]	= array_pad($args, 2, null);

		if(is_closure($value)){
			$value	= $value($this->get_data_with_default($id, $key, $default), $key, $id);
		}

		if(is_array($value)){
			if($value && (!is_array($default) || array_diff_assoc($default, $value))){
				return $this->update_data($id, $key, $value);
			}
		}else{
			if(isset($value) && ((is_null($default) && ($value || is_numeric($value))) || (!is_null($default) && $value != $default))){
				return $this->update_data($id, $key, $value);
			}
		}

		return $this->delete_data($id, $key);
	}

	public function delete_empty_data(){
		$wpdb	= $GLOBALS['wpdb'];
		$mids	= $wpdb->get_col("SELECT ".$this->get_column('id')." FROM ".$this->get_table()." WHERE meta_value = ''") ?: [];

		array_walk($mids, [$this, 'delete_by_mid']);
	}

	public function delete_by_key($key, $value=''){
		return delete_metadata($this->name, null, $key, $value, true);
	}

	public function delete_by_id($id){
		$wpdb	= $GLOBALS['wpdb'];
		$table	= $this->get_table();
		$column	= $this->get_column();
		$mids	= $wpdb->get_col($wpdb->prepare("SELECT meta_id FROM {$table} WHERE {$column} = %d ", $id)) ?: [];

		array_walk($mids, [$this, 'delete_by_mid']);
	}

	public function update_cache($ids){
		update_meta_cache($this->name, $ids);
	}

	public function cleanup(){
		$wpdb	= $GLOBALS['wpdb'];
		$key	= $this->object_key;
		$table	= $key ? $wpdb->{$this->name.'s'} : '';

		if(!$key){
			$model	= $this->object_model;

			if(!$model || !is_callable([$model, 'get_table'])){
				return;
			}

			$table	= $model::get_table();
			$key	= $model::get_primary_key();
		}

		if(is_multisite() && !str_starts_with($this->get_table(), $wpdb->prefix) && wpjam_lock($this->name.':meta_type:cleanup', DAY_IN_SECONDS, true)){
			return;
		}

		$mids	= $wpdb->get_col("SELECT m.".$this->get_column('id')." FROM ".$this->get_table()." m LEFT JOIN ".$table." t ON t.".$key." = m.".$this->get_column()." WHERE t.".$key." IS NULL") ?: [];

		array_walk($mids, [$this, 'delete_by_mid']);
	}

	public function create_table(){
		$table	= $this->get_table();
		$column	= $this->name.'_id';

		if($table != $GLOBALS['wpdb']->get_var("show tables like '{$table}'")){
			$GLOBALS['wpdb']->query("CREATE TABLE {$table} (
				meta_id bigint(20) unsigned NOT NULL auto_increment,
				{$column} bigint(20) unsigned NOT NULL default '0',
				meta_key varchar(255) default NULL,
				meta_value longtext,
				PRIMARY KEY (meta_id),
				KEY {$column} ({$column}),
				KEY meta_key (meta_key(191))
			)");
		}
	}

	public function register_keys(){
		if($this->name == 'post'){
			foreach(get_post_types(['show_in_rest'=>true]) as $post_type){
				foreach($this->get_options(['post_type'=>$post_type]) as $option){
					if(wpjam_is_assoc_array($option->fields)){
						wpjam_fields($option->fields)->register_meta($this->name, $post_type);
					}
				}
			}
		}
	}

	public function delete_if_default($check, $id, $key, $value){
		$type		= $this->name;
		$subtype	= $type == 'post' ? get_post_type($id) : ($type == 'term' ? get_term_taxonomy($id) : '');
		$registered	= $subtype ? (get_registered_meta_keys($type, $subtype)[$key] ?? null) : null;
		$registered	??= get_registered_meta_keys($type)[$key] ?? null;

		return $registered && array_key_exists('default', $registered) && $value === $registered['default']
		? (delete_metadata($type, $id, $key) || true)
		: $check;
	}

	public function registered(){
		add_action('init', [$this, 'register_keys'], 99);

		wpjam_hooks('add_'.$this->name.'_metadata, update_'.$this->name.'_metadata', [$this, 'delete_if_default'], 10, 4);
	}

	public static function get_defaults(){
		return array_merge([
			'post'	=> ['object_model'=>'WPJAM_Post',	'object_column'=>'title',	'object_key'=>'ID'],
			'term'	=> ['object_model'=>'WPJAM_Term',	'object_column'=>'name',	'object_key'=>'term_id'],
			'user'	=> ['object_model'=>'WPJAM_User',	'object_column'=>'display_name','object_key'=>'ID'],
		], (is_multisite() ? [
			'blog'	=> ['object_key'=>'blog_id'],
			'site'	=> [],
		] : []));
	}
}

class WPJAM_Meta_Option extends WPJAM_Args{
	public function __get($key){
		$value	= parent::__get($key);

		if(isset($value)){
			return $value;
		}

		if($key == 'list_table'){
			return did_action('current_screen') && !empty($GLOBALS['plugin_page']);
		}elseif($key == 'show_in_rest'){
			return true;
		}elseif($key == 'show_in_posts_rest'){
			return $this->show_in_rest;
		}
	}

	public function __call($method, $args){
		if($method == 'prepare' && ($this->callback || $this->update_callback)){
			return [];
		}

		$id		= array_shift($args);
		$fields	= maybe_callback($this->fields, $id, $this->name);

		if($method == 'get_fields'){
			return $fields;
		}

		$object	= wpjam_fields($fields, array_merge($this->get_args(), ['id'=>$id]));

		if($method == 'callback'){
			$data	= wpjam_catch([$object, 'validate'], ...$args);

			if(!wpjam_if_error($data, '')){
				return $data ?: true;
			}

			if($callback = $this->callback ?: $this->update_callback){
				$res	= is_callable($callback) ? call_user_func($callback, $id, $data, $fields) : false;

				return $res === false ? new WP_Error('invalid_callback') : $res;
			}

			return wpjam_update_metadata($this->meta_type, $id, $data, $object->get_defaults());
		}

		$res	= $object->$method(...$args);

		return $method == 'render' ? wpjam_echo(wpautop($this->summary ?: '').$res) : $res;
	}
}
```
