PluginProbe
WPJAM Basic / trunk
WPJAM Basic vtrunk
wpjam-basic / includes / class-wpjam-model.php

class-wpjam-model.php in WPJAM Basic trunk, at includes/class-wpjam-model.php

1,520 lines 47.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 abstract class WPJAM_Instance{
3 use WPJAM_Call_Trait;
4
5 protected $id;
6
7 protected function __construct($id){
8 $this->id = $id;
9 }
10
11 public function __isset($key){
12 return $this->$key !== null;
13 }
14
15 public function builtin($key){
16 $type = wpjam_get_builtin_type(get_class($this));
17 $object = $type ? wpjam_call('get_'.$type, $this->id) : null;
18
19 return (!$object || !$key || $key === $type) ? $object : ($key === 'data'
20 ? $object->to_array()
21 : $object->{(str_starts_with($key, $type.'_') ? '' : $type.'_').$key} ?? ($object->$key ?? $this->meta_get($key))
22 );
23 }
24
25 public function meta_get($key){
26 return wpjam_get_metadata(static::get_meta_type(), $this->id, $key);
27 }
28
29 public function meta_exists($key){
30 return metadata_exists(static::get_meta_type(), $this->id, $key);
31 }
32
33 public function meta_input(...$args){
34 return $args ? wpjam_update_metadata(static::get_meta_type(), $this->id, ...$args) : null;
35 }
36
37 abstract protected static function call_method($method, ...$args);
38
39 public static function get_meta_type(){
40 return static::call_method('get_meta_type') ?: wpjam_get_builtin_type(static::class);
41 }
42
43 public static function write($data, $id=0){
44 wpjam_try(fn()=> static::validate_data($data, $id));
45
46 $input = ($type = static::get_meta_type()) ? wpjam_pull($data, 'meta_input') : [];
47 $data = static::sanitize_data($data, $id);
48
49 return wpjam_tap(
50 static::call_method(...($id ? ['update', $id, $data] : ['insert', $data])),
51 fn($v)=> $input && wpjam_update_metadata($type, $id ?: $v, $input)
52 );
53 }
54
55 public static function insert($data){
56 return wpjam_catch([static::class, 'write'], $data);
57 }
58
59 public static function update($id, $data){
60 return wpjam_catch([static::class, 'write'], $data, $id);
61 }
62
63 public static function delete($id){
64 return wpjam_catch(fn()=> static::before_delete($id) || true ? static::call_method('delete', $id) : null);
65 }
66
67 public static function before_delete($id){
68 if(array_all(['is_deletable', 'get_instance'], fn($m)=> method_exists(static::class, $m))){
69 wpjam_try(static::class.'->is_deletable', $id) || wpjam_throw('indelible', '不可删除');
70 }
71 }
72
73 protected static function validate_data($data, $id=0){
74 return true;
75 }
76
77 protected static function sanitize_data($data, $id=0){
78 return $data;
79 }
80
81 public static function instance(...$args){
82 static $objects = [];
83
84 [$key, $cb] = count($args) == 2 && is_callable($args[1]) ? $args : [($args ? implode(':', $args) : 'singleton'), null];
85 $called = self::get_called();
86 $object = $objects[$called][$key] ?? null;
87
88 if(!$object){
89 $object = $cb ? $cb($key) : static::create_instance(...$args);
90
91 if(!is_wp_error($object) && !is_null($object)){
92 $objects[$called][$key] = $object;
93 }
94 }
95
96 return $object;
97 }
98
99 protected static function create_instance(...$args){
100 return new static(...$args);
101 }
102 }
103
104 abstract class WPJAM_Model extends WPJAM_Instance implements ArrayAccess, IteratorAggregate{
105 protected $_data = [];
106
107 public function __construct($data=[], $id=null){
108 if($id){
109 $this->id = $id;
110 $this->_data = $data ? array_diff_assoc($data, static::get($id)) : [];
111 }else{
112 $id = $data[static::get_primary_key()] ?? null;
113 $exist = isset($id) ? static::get($id) : null;
114
115 $exist && ($this->id = $id);
116
117 $this->_data = $exist ? array_diff_assoc($data, $exist) : $data;
118 }
119 }
120
121 public function __get($key){
122 return wpjam_exists($this->get_data(), $key) ? $this->get_data()[$key] : $this->meta_get($key);
123 }
124
125 public function __isset($key){
126 return wpjam_exists($this->get_data(), $key) || $this->meta_exists($key);
127 }
128
129 public function __set($key, $value){
130 $this->set_data($key, $value);
131 }
132
133 public function __unset($key){
134 $this->unset_data($key);
135 }
136
137 #[ReturnTypeWillChange]
138 public function offsetExists($key){
139 return wpjam_exists($this->get_data(), $key);
140 }
141
142 #[ReturnTypeWillChange]
143 public function offsetGet($key){
144 return $this->get_data($key);
145 }
146
147 #[ReturnTypeWillChange]
148 public function offsetSet($key, $value){
149 $this->set_data($key, $value);
150 }
151
152 #[ReturnTypeWillChange]
153 public function offsetUnset($key){
154 $this->unset_data($key);
155 }
156
157 #[ReturnTypeWillChange]
158 public function getIterator(){
159 return new ArrayIterator($this->get_data());
160 }
161
162 public function get_primary_id(){
163 return $this->get_data(static::get_primary_key());
164 }
165
166 public function get_data($key=''){
167 $data = is_null($this->id) ? [] : static::get($this->id);
168 $data = array_merge($data, $this->_data);
169
170 return $key ? ($data[$key] ?? null) : $data;
171 }
172
173 public function set_data($key, $value){
174 if(!is_null($this->id) && static::get_primary_key() == $key){
175 trigger_error('不能修改主键的值');
176 }else{
177 $this->_data[$key] = $value;
178 }
179
180 return $this;
181 }
182
183 public function unset_data($key){
184 $this->_data[$key] = null;
185 }
186
187 public function reset_data($key=''){
188 $this->_data = $key ? wpjam_except($this->_data, $key) : [];
189
190 return $this;
191 }
192
193 public function to_array(){
194 return $this->get_data();
195 }
196
197 public function save($data=[]){
198 $data = array_merge($this->_data, $data);
199 $data = $this->id ? wpjam_except($data, static::get_primary_key()) : $data;
200
201 return wpjam_tap(
202 $this->id ? static::update($this->id, $data) : static::insert($data),
203 fn($v)=> ($this->id = $this->reset_data()->id ?: $v)
204 );
205 }
206
207 public static function find($id){
208 return static::get_instance($id);
209 }
210
211 public static function get_actions(){
212 return [
213 'add' => ['title'=>'新建', 'dismiss'=>true],
214 'edit' => ['title'=>'编辑'],
215 'delete' => ['title'=>'删除', 'direct'=>true, 'confirm'=>true, 'bulk'=>true, 'order'=>1],
216 ];
217 }
218
219 public static function get_instance($id){
220 return $id ? static::instance($id, fn($id)=> static::get($id) ? new static([], $id) : null) : null;
221 }
222
223 public static function call_method($method, ...$args){
224 $name = static::get_handler();
225
226 if($method == 'insert_multi'){
227 array_walk($args[0], fn($v)=> wpjam_try(fn()=> static::validate_data($v)));
228 }elseif($method == 'delete_multi'){
229 array_walk($args[0], fn($v)=> static::before_delete($v));
230 }
231
232 return wpjam_call_handler($name, $method, ...$args);
233 }
234
235 public static function get_handler(){
236 return wpjam_get_handler(self::get_called()) ?: (property_exists(static::class, 'handler') ? static::$handler : null);
237 }
238
239 public static function set_handler($handler){
240 return wpjam_register_handler(self::get_called(), $handler);
241 }
242
243 public static function __callStatic($method, $args){
244 return in_array($method, ['item_callback', 'render_item'])
245 ? $args[0]
246 : static::call_method(wpjam_suffix($method, '-', '_by_handler'), ...$args);
247 }
248 }
249
250 class WPJAM_Handler extends WPJAM_Args{
251 public function __call($method, $args){
252 $method = strtolower($method);
253 $name = array_shift($args);
254 $object = is_object($name) ? $name : ($this)($name);
255
256 if(!$object){
257 return new WP_Error('undefined_handler');
258 }
259
260 if(in_array($method, ['get_primary_key', 'get_meta_type', 'get_searchable_fields', 'get_filterable_fields'])){
261 return $object->{wpjam_prefix($method, '-', 'get_')};
262 }
263
264 if(!method_exists($object, $method) && $this->mod($method, '_multi')){
265 return wpjam_catch(fn()=> array_walk($args[0], fn($item)=> wpjam_try([$object, $method], $item)) || true);
266 }
267
268 $cb = [$object, ['get_ids'=>'get_by_ids', 'get_all'=>'get_results'][$method] ?? $method];
269
270 return is_callable($cb) ? wpjam_catch($cb, ...$args) : new WP_Error('undefined_method', [$method]);
271 }
272
273 public function __invoke($name, $args=[], $force=false){
274 if(!$name){
275 return;
276 }
277
278 if(is_array($name)){
279 $args = $name;
280 $name = wpjam_pull($args, 'name') ?: md5(serialize($args));
281 }
282
283 $object = $force ? null : $this->get_arg('handlers['.$name.']');
284
285 if(!$object && $args){
286 $args = maybe_closure($args, $name);
287 $object = is_object($args) ? $args : (!empty($args['table_name']) ? WPJAM_DB::create($args) : WPJAM_Items::create($args, $name));
288
289 $object && $this->update_arg('handlers['.$name.']', $object);
290 }
291
292 return $object;
293 }
294 }
295
296 class WPJAM_DB extends WPJAM_Args{
297 private $query_vars;
298
299 public function __construct($table, $args=[]){
300 $pk = $args['primary_key'] ??= 'id';
301 $ck = $args['cache_key'] ?? '';
302 $ck = $args['cache_key'] = $ck == $pk ? '' : $ck;
303
304 $this->args = wp_parse_args($args, [
305 'table' => $table,
306 'orderby' => $pk,
307 'cache' => true,
308 'cache_group' => $table,
309 'cache_time' => DAY_IN_SECONDS
310 ]);
311
312 $this->init();
313 $this->process_arg('group_cache_key', fn($v)=> array_merge((array)$v, $ck ? [$ck] : []));
314 }
315
316 public function __call($method, $args){
317 if($this->mod($method, 'where_')){
318 if($method == 'fragment'){
319 return $this->where('', ...$args);
320 }
321
322 if(in_array($method, ['any', 'all'])){
323 $args[0] = array_filter(wpjam_map($args[0], fn($v, $k)=> is_numeric($k) ? '('.$v.')' : $this->where($k, $v, 'value')));
324 $args[0] = implode($method == 'any' ? ' OR ' : ' AND ', $args[0]);
325
326 return $this->where('', ...$args);
327 }
328
329 return $this->operator($method) ? $this->where(array_shift($args).'__'.$method, ...$args) : $this;
330 }elseif(array_key_exists($method, $this->query_vars)){
331 $value = $args ? $args[0] : ($method == 'found_rows' ? true : null);
332
333 if(!is_null($value)){
334 $this->query_vars[$method] = $value;
335 }
336
337 return $this;
338 }elseif($this->mod($method, '_by_db')){
339 global $wpdb;
340
341 if($method == 'last_error'){
342 return new WP_Error($args[0].'_error', $wpdb->$method);
343 }
344
345 if(in_array($method, ['insert', 'update', 'delete', 'replace'])){
346 if($this->field_types){
347 $wpdb->field_types = array_merge(($types = $wpdb->field_types), $this->field_types);
348 }
349 }elseif(in_array($method, ['get_results', 'get_ids', 'get_row', 'get_col', 'get_var'])){
350 if($method == 'get_ids'){
351 $args[0]['fields'] = $this->table.'.'.$this->primary_key;
352
353 $method = 'get_col';
354 }
355
356 if(is_array($args[0])){
357 $args[0] = $this->get_request($args[0]+(in_array($method, ['get_row', 'get_var']) ? ['limits'=> 'LIMIT 1'] : []));
358 }
359
360 if(in_array($method, ['get_results', 'get_row'])){
361 $args[1] = ARRAY_A;
362 }
363 }
364
365 try{
366 return $wpdb->$method(...$args);
367 }finally{
368 isset($types) && ($wpdb->field_types = $types);
369 }
370 }elseif(in_array($method, ['get_col', 'get_var', 'get_row', 'get_results'])){
371 $clauses = $this->get_clauses($args[0] ?? []);
372 $action = $method == 'get_results' && in_array($clauses['fields'], ['*', $this->table.'.*']) ? 'get_ids' : $method;
373 $items = [$this, $action.'_by_db']($clauses);
374 $total = $method == 'get_results' && ($args[1] ?? null) ? $this->find_total() : null;
375 $items = $action == 'get_ids' ? array_values($this->get_by_ids($items)) : $items;
376
377 return isset($total) ? ['items'=>$items, 'total'=>$total] : $items;
378 }elseif(str_contains($method, '_meta')){
379 return ($object = wpjam_get_meta_type($this->meta_type)) ? $object->$method(...$args) : null;
380 }elseif($this->mod($method, 'cache_')){
381 return [WPJAM_Cache::create($this), $method](...$args);
382 }elseif($this->mod($method, '_last_changed')){
383 $ck = $this->group_cache_key;
384 $ck = $ck && is_array($args[0] ?? '') ? array_find_key($args[0], fn($v, $k)=> !is_array($v) && in_array($k, $ck)) : [];
385 $k = 'last_changed'.($ck ? ':'.$ck.':'.$args[0][$ck] : '');
386
387 return $method == 'get'
388 ? ($this->cache_get($k) ?: wpjam_tap(microtime(), fn($v)=> $this->cache_set($k, $v)))
389 : ($method == 'delete' ? $this->cache_delete($k) : null);
390 }
391
392 return new WP_Error('undefined_method', [$method]);
393 }
394
395 protected function init(){
396 $this->meta_query = null;
397 $this->query_vars = [
398 'where' => [],
399 'limit' => 0,
400 'offset' => 0,
401 'orderby' => null,
402 'order' => null,
403 'groupby' => null,
404 'having' => null,
405 'found_rows' => false,
406 'search_term' => null,
407 'search_columns' => null
408 ];
409 }
410
411 public function find_by($field, $value, $order='ASC', $output='results'){
412 return [$this, 'get_'.$output.'_by_db']([
413 'where' => 'WHERE '.(is_array($field) ? $this->where_all($field, 'value') : $this->where($field, $value, 'value')),
414 'orderby' => $order ? 'ORDER BY `'.$this->get_arg('orderby').'` '.$order : ''
415 ]);
416 }
417
418 public function find_one($value, $field='', $order=''){
419 return $this->find_by($field ?: $this->primary_key, $value, $order, 'row');
420 }
421
422 public function find_total(){
423 return $this->get_var_by_db("SELECT FOUND_ROWS();");
424 }
425
426 public function get($id){
427 return $this->get_by($this->primary_key, $id);
428 }
429
430 public function operator($name){
431 return ($this->operator ??= array_reduce(['in', 'like', 'between', 'exists', 'regexp'], fn($c, $v)=>$c+[$v=>strtoupper($v), 'not_'.$v=>'NOT '.strtoupper($v)], ['not'=>'!=', 'lt'=>'<', 'lte'=>'<=', 'gt'=>'>', 'gte'=>'>=']))[$name] ?? null;
432 }
433
434 public function get_by($field, $value=null, $order='ASC'){
435 $pk = $this->primary_key;
436 $type = is_array($field) ? '' : ($field == $pk ? 'primary' : (in_array($field, $this->group_cache_key) ? 'cache' : ''));
437 $multi = is_array($value);
438
439 if($multi){
440 $value = wpjam_filter(array_filter($value), 'unique');
441
442 if(!$value){
443 return [];
444 }
445
446 if($type == 'primary'){
447 $ids = $value;
448 $data = array_filter($this->cache_get_multiple($ids) ?: [], 'is_array');
449 $rest = array_diff($ids, array_keys($data));
450
451 if($rest){
452 if($result = $this->find_by($pk, $rest)){
453 $result = array_column($result, null, $pk);
454 $data += $result;
455 $rest = array_diff($rest, array_keys($result));
456
457 $this->cache_set_multiple($result);
458 }
459
460 $rest && $this->cache_set_multiple(array_fill_keys($rest, []), 5);
461 }
462
463 if($data){
464 $data = wpjam_pick($data, $ids);
465
466 $this->meta_type && wpjam_lazyload($this->meta_type.'_meta', array_keys($data));
467
468 wpjam_lazyload($this->lazyload_key, $data);
469 }
470
471 return $data;
472 }elseif($type == 'cache'){
473 $result = wpjam_fill($value, fn($v)=> $this->query([$field=>$v, 'order'=>$order], 'cache'));
474 $data = wpjam_map(array_filter($result, fn($v)=> is_array($v[0]) && isset($v[0]['items'])), fn($v)=> $v[0]['items']);
475 $rest = array_diff_key($result, $data);
476 $ids = array_merge($rest ? $this->query([$field.'__in'=>array_keys($rest), 'order'=>$order], 'ids') : [], ...array_values($data));
477 $result = array_values($this->get_by_ids($ids));
478 $data = array_map(fn($ids)=> array_values($this->get_by_ids($ids)), $data);
479
480 foreach($rest as $v => $r){
481 $data[$v] = wp_list_filter($result, [$field => $v]) ?: [];
482
483 $this->cache_set_salted($r[1], ['items'=>array_column($data[$v], $pk)], $r[2]);
484 }
485
486 return $data;
487 }
488 }else{
489 if($type && ($queue = $this->pending_queue)){
490 $queue = is_array($queue) ? ($queue[$field] ?? '') : ($type == 'primary' ? $queue : '');
491 $queue && wpjam_load_pending($queue, fn($pending)=> $this->get_by($field, $pending, $order));
492 }
493
494 if($type == 'primary'){
495 return ($v = ($id = $value) ? $this->cache_get($id) : []) === false
496 ? wpjam_tap($this->find_one($id), fn($v)=> $this->cache_set($id, $v ?: [], $v ? $this->cache_time : 60))
497 : $v;
498 }elseif($type == 'cache'){
499 return $this->query([$field=>$value, 'order'=>$order], 'items');
500 }
501 }
502
503 return $this->find_by($field, $value, $order);
504 }
505
506 public function get_by_values($field, $values, $order='ASC'){
507 return $this->get_by($field, $values, $order);
508 }
509
510 public function get_by_ids($ids){
511 return $this->get_by($this->primary_key, $ids);
512 }
513
514 public function get_ids($ids){
515 return $this->get_by_ids($ids);
516 }
517
518 public function update_caches($values, $primary=false){
519 return $this->get_by(($primary ? '' : $this->cache_key) ?: $this->primary_key, $values);
520 }
521
522 protected function get_clauses($fields=[]){
523 $table = $this->table;
524 $pk = $this->primary_key;
525 $vars = $this->query_vars;
526 $meta_query = $this->meta_query;
527 $clauses = $meta_query ? $meta_query->get_sql($this->meta_type, $table, $pk, $this) : [];
528 $limit = (int)$vars['limit'];
529 $offset = (int)$vars['offset'];
530 $found_rows = $limit && $vars['found_rows'];
531 $groupby = $this->group_by() ?: ($meta_query ? $table.'.'.$pk : '');
532 $orderby = $this->order_by();
533 $where = (($where = $this->where()) || ($clauses['where'] ?? '')) ? ($where ?: '1=1').wpjam_pull($clauses, 'where') : '';
534
535 return $clauses + [
536 'found_rows'=> $found_rows ? 'SQL_CALC_FOUND_ROWS' : '',
537 'fields' => $fields ?: ($meta_query ? $table.'.*' : '*'),
538 'where' => $where ? ' WHERE '.$where : '',
539 'groupby' => $groupby ? ' GROUP BY '.$groupby.($vars['having'] ? ' HAVING '.$vars['having'] : '') : '',
540 'orderby' => $orderby ? ' ORDER BY '.$orderby : '',
541 'limits' => $limit ? ' LIMIT '.($offset ?: 0).', '.$limit : ($offset ? ' OFFSET '.$offset : '')
542 ];
543 }
544
545 public function get_request($clauses=null){
546 $clauses ??= $this->get_clauses();
547 $fields = wpjam_pull($clauses, 'fields');
548 $clauses += ['fields'=>(is_array($fields) ? $this->format($fields, 'fields') : $fields) ?: '*'];
549
550 return sprintf("SELECT %s %s %s FROM `{$this->table}` %s %s %s %s %s", ...array_map(fn($k)=> $clauses[$k] ?? '', ['found_rows', 'distinct', 'fields', 'join', 'where', 'groupby', 'orderby', 'limits']));
551 }
552
553 public function get_sql($fields=[]){
554 return $this->get_request($this->get_clauses($fields));
555 }
556
557 public function search(...$args){
558 if($args){
559 return $this->search_term(...$args);
560 }
561
562 $term = $this->query_vars['search_term'];
563 $fields = $this->query_vars['search_columns'] ?: $this->searchable_fields;
564 $fields = is_array($fields) ? (wp_is_numeric_array($fields) ? $fields : array_keys($fields)) : wp_parse_list($fields ?: '');
565
566 return $term && $fields ? $this->where_any(wpjam_array($fields, fn($i, $k)=> [$k.'__like', '%'.$term.'%'])) : $this;
567 }
568
569 public function group_by(...$args){
570 if($args){
571 return $this->groupby(...$args);
572 }
573
574 return ($by = $this->query_vars['groupby']) ? (array_any([',', '(',], fn($v)=> str_contains($by, $v)) ? $by : $this->prepare_by_db('%i', $by)) : '';
575 }
576
577 public function order_by(...$args){
578 if(count($args) >= 2){
579 [$by, $order] = $args;
580
581 if($by == 'rand'){
582 return 'RAND()';
583 }elseif(preg_match('/RAND\(([0-9]+)\)/i', $by, $matches)){
584 return $by;
585 }elseif(str_contains($by, ',')){
586 return $by;
587 }elseif(str_contains($by, '(') && str_contains($by, ')')){
588 return $by;
589 }elseif(str_ends_with($by, '__in')){
590 return null;
591 // $field = str_replace('__in', '', $by);
592 }
593
594 if($this->meta_query && ($clauses = $this->meta_query->get_clauses())){
595 if($mq = in_array($by, [array_first($clauses)['key'] ?? '', 'meta_value', 'meta_value_num'])
596 ? array_first($clauses)
597 : ($clauses[$by] ?? null)
598 ){
599 $num = $by == 'meta_value_num';
600 $by = $mq['alias'].".meta_value";
601
602 return ($num ? $by.'+0' : (empty($mq['type']) ? $by : "CAST(".$by." AS {$mq['cast']})")).' '.$order;
603 }
604 }
605
606 return $this->prepare_by_db('%i', $by).' '.$order;
607 }elseif($args){
608 return $this->orderby(...$args);
609 }
610
611 $vars = $this->query_vars;
612 $by = $vars['orderby'];
613 $order = $vars['order'] ?? ($this->get_arg('order') ?: 'DESC');
614 $order = is_string($order) && 'ASC' === strtoupper($order) ? 'ASC' : 'DESC';
615
616 return isset($by)
617 ? ($by ? implode(', ', wpjam_array(is_array($by) ? $by : [$by=>$order], fn($k, $v)=> [null, $this->order_by($k, $v)], true)) : '')
618 : (($by = $vars['groupby'] ? '' : $this->get_arg('orderby')) ? $by.' '.$order : '');
619 }
620
621 public function insert($data){
622 $wpdb = $GLOBALS['wpdb'];
623 $multi = wp_is_numeric_array($data);
624
625 [$data,$id] = $multi ? [array_filter($data), null] : [array_filter($data, fn($v)=> !is_null($v)), $data[$this->primary_key] ?? null];
626
627 if($multi && !$data){
628 return 0;
629 }
630
631 $this->clear([], $data);
632
633 if($id || $multi){
634 $wpdb->check_current_query = false;
635
636 $data = $multi ? $data : [$data];
637 $fields = $this->format(array_keys(array_first($data)), 'fields');
638 $update = implode(', ', array_map(fn($v)=> $v.' = VALUES('.$v.')', explode(', ', $fields)));
639 $values = implode(', ', array_map(fn($v)=> '('.$this->format($v, 'values').')', $data));
640 $result = $this->query_by_db("INSERT INTO `{$this->table}` ({$fields}) VALUES {$values} ON DUPLICATE KEY UPDATE {$update}");
641 }else{
642 $result = $this->insert_by_db($this->table, $data);
643 }
644
645 return $result === false
646 ? $this->last_error_by_db('insert')
647 : ($multi ? $result : wpjam_tap($id ?: $wpdb->insert_id, fn($id)=> $this->cache_delete($id)));
648 }
649
650 public function insert_multi($data){
651 return $this->insert(array_values(array_filter($data))); // 自增的�
652 况可能无法无法删除缓存,请注意
653 }
654
655 /*
656 update($id, $data);
657 update($data, $where);
658 update($data);
659 */
660 public function update(...$args){
661 if(!$args){
662 return 0;
663 }
664
665 [$id, $data, $where] = count($args) == 1 ? [null, ...$args, $this->where()] : (is_array($args[0]) ? [null, ...$args] : [$args[0], $args[1], null]);
666
667 if(!$data || (isset($where) && !$where)){
668 return 0;
669 }
670
671 $this->clear($id, $data, $where);
672
673 if(count($args) >= 2){
674 $result = $this->update_by_db($this->table, $data, $where ?? [$this->primary_key => $id]);
675 }else{
676 $data = $this->format($data, 'data');
677 $result = $this->query_by_db("UPDATE `{$this->table}` SET {$data} WHERE {$where}");
678 }
679
680 return $result === false ? $this->last_error_by_db('update') : $result;
681 }
682
683 /*
684 delete($where);
685 delete($id);
686 delete();
687 */
688 public function delete(...$args){
689 [$id, $where] = $args ? (wpjam_is_assoc_array($args[0]) ? [null, $args[0]] : [$args[0], null]) : [null, $this->where()];
690
691 if(isset($where) && !$where){
692 return 0;
693 }
694
695 $this->clear($id, [], $where);
696
697 $key = $this->primary_key;
698 $where ??= is_array($id) ? $this->where($key, $id, 'value') : [$key => $id];
699
700 if(is_array($where)){
701 $result = $this->delete_by_db($this->table, $where);
702 }else{
703 $result = $this->query_by_db("DELETE FROM `{$this->table}` WHERE {$where}");
704 }
705
706 if($result === false){
707 return $this->last_error_by_db('delete');
708 }
709
710 if($id){
711 wpjam_map((array)$id, [$this, 'delete_meta_by_id']);
712 }else{
713 $this->delete_orphan_meta($this->table, $key);
714 }
715
716 return $result;
717 }
718
719 public function delete_by($field, $value){
720 return $this->delete([$field => $value]);
721 }
722
723 public function delete_multi($ids){
724 return $ids ? $this->delete(array_values(array_filter($ids))) : 0;
725 }
726
727 protected function clear($ids=[], $data=[], $where=[]){
728 $this->delete_last_changed();
729
730 if(($ck = $this->group_cache_key) || $this->cache){
731 $ids = (array)$ids;
732 $pk = $this->primary_key;
733 $where = $where ? [is_array($where) ? $this->where_all($where, 'value') : $where] : [];
734 $data = wp_is_numeric_array($data) ? $data : [$data];
735 $ids = array_merge($ids, array_column($data, $pk));
736
737 $ids && $this->cache_delete_multiple($ids);
738 $ids && $ck && ($where[] = $this->where($pk, $ids, 'value'));
739
740 if($where){
741 if($result = $this->get_results_by_db([
742 'fields' => [$pk, ...$ck],
743 'where' => 'WHERE '.$this->where_any($where, 'value')
744 ])){
745 $this->cache_delete_multiple(array_column($result, $pk));
746
747 $ck && ($data = array_merge($data, $result));
748 }
749 }
750
751 $ck && wpjam_map($ck, fn($k)=> wpjam_map(array_unique(array_column($data, $k)), fn($v)=> $this->delete_last_changed([$k => $v])));
752 }
753 }
754
755 protected function format($field, ...$args){
756 $types = $this->field_types ?: [];
757
758 if(is_array($field)){
759 $type = $args[0];
760
761 if($type == 'fields'){
762 $query = array_fill(0, count($field), '%i');
763 $args = $field;
764 }else{
765 $query = $args = [];
766
767 foreach($field as $k => $v){
768 $query[] = ($type == 'data' ? "%i = " : '').(is_null($v) ? 'NULL' : ($types[$k] ?? '%s'));
769 $args = array_merge($args, ($type == 'data' ? [$k] : []), (is_null($v) ? [] : [$v]));
770 }
771 }
772
773 $query = implode(', ', $query);
774 }else{
775 $op = $args[0];
776 $v = $args[1];
777 $f = $types[$field] ?? '%s';
778
779 if(in_array($op, ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'])){
780 $v = is_array($v) ? $v : array_map('trim', explode(',', $v));
781 $c = count($v);
782
783 if(in_array($op, ['IN', 'NOT IN'])){
784 if($c <= 1){
785 $v = array_first($v ?: ['']);
786 $op = $op == 'IN' ? '=' : '!=';
787 }else{
788 $f = '('.implode(', ', array_fill(0, $c, $f)).')';
789 }
790 }else{
791 $f = $f.' AND '.$f;
792 }
793 }elseif(in_array($op, ['LIKE', 'NOT LIKE'])){
794 $f = '%s';
795 $v = (str_starts_with($v, '%') ? '%' : '').$this->esc_like_by_db(trim($v, '%')).(str_ends_with($v, '%') ? '%' : '');
796 }elseif(in_array($op, ['REGEXP', 'NOT REGEXP', 'RLIKE'])){
797 $f = '%s';
798 }elseif(in_array($op, ['EXISTS', 'NOT EXISTS'])){
799 $query = '%i IS '.($op == 'EXISTS' ? 'NOT NULL' : 'NULL');
800
801 return $this->prepare_by_db($query, [$field]);
802 }
803
804 $args = [$field, ...(array)$v];
805 $query = '%i '.$op.' '.$f;
806 }
807
808 return $args ? $this->prepare_by_db($query, $args) : $query;
809 }
810
811 public function where(...$args){
812 if(!$args){
813 return wpjam_tap(implode(' AND ', $this->search()->query_vars['where']), fn()=> $this->init());
814 }
815
816 [$field, $value, $output] = $args+['', null, 'object'];
817
818 if(!$field){
819 $value = $value ? '('.$value.')' : '';
820 }elseif(isset($value)){
821 $op = ($pos = strrpos($field, '__')) ? $this->operator(substr($field, $pos + 2)) : '';
822 $field = $op ? substr($field, 0, $pos) : $field;
823 $value = $this->format($field, $op ?: (is_array($value) ? 'IN' : '='), $value);
824 }
825
826 if($output == 'object'){
827 if($value){
828 $this->query_vars['where'][] = $value;
829 }
830
831 return $this;
832 }
833
834 return $value;
835 }
836
837 public function query(...$args){
838 if(!$args){
839 return $this;
840 }
841
842 $vars = $args[0];
843 $output = $args[1] ?? 'object';
844
845 if(in_array($output, ['cache', 'items', 'ids'])){
846 $vars += ['no_found_rows'=>true, 'suppress_filters'=>true];
847 }else{
848 $vars = apply_filters('wpjam_query_vars', $vars, $this);
849 $vars = isset($vars['groupby']) ? ['no_found_rows'=>true]+wpjam_except($vars, ['since', 'cursor']) : $vars;
850 $vars += empty($vars['no_found_rows']) ? ['number'=>50] : [];
851 }
852
853 $pk = $this->primary_key;
854 $qv = $vars;
855 $orderby = $qv['orderby'] ?? $this->get_arg('orderby');
856 $fields = wpjam_pull($qv, 'fields');
857 $cache = wpjam_pull($qv, 'cache_results', $output != 'ids') || $output == 'cache';
858 $suppress = wpjam_pull($qv, 'suppress_filters');
859 $found_rows = !wpjam_pull($qv, 'no_found_rows');
860
861 if($this->meta_type && ($meta_query = wpjam_pull($qv, ['meta_key', 'meta_value', 'meta_compare', 'meta_compare_key', 'meta_type', 'meta_type_key', 'meta_query']))){
862 $this->meta_query = new WP_Meta_Query();
863 $this->meta_query->parse_query_vars($meta_query);
864 }
865
866 foreach($qv as $k => $v){
867 if(is_null($v)){
868 continue;
869 }
870
871 if(array_key_exists($k, $this->query_vars)){
872 $this->query_vars[$k] = $v;
873 }elseif($k == 'number'){
874 if($v == -1){
875 $found_rows = false;
876 }else{
877 $this->limit($v);
878 }
879 }elseif(in_array($k, ['s', 'search'])){
880 $this->search($v);
881 }elseif(in_array($k, ['cursor', 'since'])){
882 $v > 0 && $this->where($orderby.'__'.($k == 'cursor' ? 'lt' : 'gt'), $v);
883 }elseif(in_array($k, ['exclude', 'include'])){
884 $v && is_array($v) && $this->where($pk.'__'.($k == 'include' ? 'in' : 'not_in'), $v);
885 }else{
886 $this->where($k, $v);
887 }
888 }
889
890 $found_rows && $this->found_rows();
891
892 $clauses = $this->get_clauses($fields);
893 $clauses = $suppress ? $clauses : apply_filters('wpjam_clauses', $clauses, $this);
894 $request = $this->get_request($clauses);
895 $cache = $cache && !str_contains(strtoupper($orderby), ' RAND(') && in_array($clauses['fields'], ['*', $this->table.'.*']);
896 $data = false;
897
898 if($cache){
899 $vars = map_deep($vars, 'strval');
900 $lc = $this->get_last_changed($vars);
901 $ck = md5(serialize($vars).$this->remove_placeholder_escape_by_db($request));
902 $data = $this->cache_get_salted($ck, $lc);
903
904 if($output == 'cache'){
905 return [$data, $ck, $lc];
906 }
907 }
908
909 if($data === false || !isset($data['items'])){
910 $items = ($cache || $output == 'ids') ? $this->get_ids_by_db($clauses) : $this->get_results_by_db($request);
911 $data = ['items'=>$items]+($found_rows ? ['total'=>$this->find_total()] : []);
912
913 $cache && $this->cache_set_salted($ck, $data, $lc);
914 }
915
916 if($output == 'ids'){
917 return $data['items'];
918 }
919
920 if($cache){
921 $data['items'] = array_values($this->get_by_ids($data['items']));
922 }
923
924 if($output == 'items'){
925 return $data['items'];
926 }
927
928 if($found_rows){
929 $data['next_cursor'] = 0;
930
931 if(!empty($qv['number'])){
932 $data['max_num_pages'] = ceil($data['total'] / $qv['number']);
933 $data['next_cursor'] = $data['items'] && $data['max_num_pages'] > 1 ? (int)(end($data['items'])[$orderby]) : 0;
934 }
935 }else{
936 $data['total'] = count($data['items']);
937 }
938
939 $data['datas'] = &$data['items']; // �
940 �容
941 $data['found_rows'] = &$data['total']; // �
942 �容
943 $data['request'] = $request;
944
945 return $output == 'object' ? (object)$data : $data;
946 }
947
948 public function query_items(...$args){
949 if(is_array($args[0])){
950 return $this->query(...($args+['', 'array']));
951 }
952
953 wpjam_map(['orderby'=>'orderby', 'order'=>'order', 's'=>'search_term'], fn($v, $k)=> is_null($this->$v) && $this->$v($this->get_param($k)));
954
955 wpjam_map($this->filterable_fields ?: [], fn($k)=> $this->where($k, $this->get_param($k)));
956
957 return $this->limit($args[0])->offset($args[1])->found_rows()->get_results([], true);
958 }
959
960 public function get_one_by($field, $value, $order='ASC'){
961 return ($items = $this->get_by($field, $value, $order)) ? array_first($items) : [];
962 }
963
964 public function find($fields=[]){
965 trigger_error('find');
966 return $this->get_results($fields);
967 }
968
969 public function cache_delete_by($field, $value, $order='ASC'){ // del 2026-06-30
970 trigger_error('cache_delete_by');
971
972 in_array($field, $this->group_cache_key) && wpjam_map((array)$value, fn($v)=> $this->cache_delete($this->query([$field=>$v, 'order'=>$order], 'cache')[1]));
973 }
974
975 public function get_wheres(){
976 return $this->where();
977 }
978
979 public static function create($args, $name=null){
980 return ($name ??= $args['table_name'] ?? '') ? new static($name, $args) : null;
981 }
982 }
983
984 class WPJAM_Items extends WPJAM_Args{
985 public function __construct($args=[]){
986 $args += ['items_type'=> wpjam_pull($args, 'type')];
987 $type = $args['items_type'];
988
989 $this->args = $args+($type ? ([
990 'option' => ['primary_key'=>'option_key'],
991 'transient' => ['item_type'=>''],
992 'meta' => ['parent_key'=>($args['meta_type'] ?? '').'_id'],
993 'post_content' => ['parent_key'=>'post_id'],
994 'cache' => ['item_type'=>'', 'retry_times'=> 10, 'cache_group'=>'list_cache'],
995 ][$type] ?? []) : [])+[
996 'item_type' => 'array',
997 'primary_key' => 'id'
998 ];
999 }
1000
1001 public function __call($method, $args){
1002 if(str_ends_with($method, '_items')){
1003 if($method == 'get_items'){
1004 $res = $this->cached_items;
1005
1006 if(is_array($res)){
1007 return $res;
1008 }
1009 }
1010
1011 if($method == 'update_items'){
1012 if($this->compressed && is_array($args[0])){
1013 $args[0] = wpjam_compress($args[0]);
1014 }
1015 }
1016
1017 if($this->$method){
1018 $res = $this->call($method, ...$args);
1019 }else{
1020 $type = $this->items_type;
1021
1022 if($type == 'option'){
1023 $keys = ['option_name'];
1024 $map = ['get_items'=>'get_option', 'update_items'=>'update_option'];
1025 }elseif($type == 'setting'){
1026 $keys = ['option_name', 'setting_name'];
1027 $map = ['get_items'=>'wpjam_get_setting', 'update_items'=>'wpjam_update_setting'];
1028 }elseif($type == 'transient'){
1029 $keys = ['transient'];
1030 $map = ['get_items'=>'get_transient', 'update_items'=>'set_transient'];
1031 $args = [...$args, ...($method == 'update_items' ? [DAY_IN_SECONDS] : [])];
1032 }elseif($type == 'meta'){
1033 $keys = ['meta_type', 'object_id', 'meta_key'];
1034 $map = ['get_items'=>'get_metadata', 'update_items'=>'update_metadata', 'delete_items'=>'delete_metadata'];
1035 $args = [...$args, ...($method == 'get_items' ? [true] : ($method == 'update_items' ? [$this->pre_value] : []))];
1036 }elseif($type == 'post_content'){
1037 if($post_id = $this->post_id){
1038 $object = wpjam_get_post($this->post_id, 'object');
1039
1040 if($method == 'get_items'){
1041 $res = $object->content;
1042 $res = (!$this->compressed || is_serialized($res)) ? $object->get_unserialized() : $res;
1043 }elseif($method == 'update_items'){
1044 $res = $object->save(['content'=>$args[0] ?: '']);
1045 }
1046 }
1047 }elseif($type == 'cache'){
1048 if($key = $this->cache_key){
1049 $object = WPJAM_Cache::create($this);
1050
1051 if($method == 'get_items'){
1052 $res = [$object, 'get_with_cas']($key);
1053
1054 [$res, $this->cas_token] = $res === false ? [false, null] : [$res['value'], $res['cas']];
1055 }elseif($method == 'update_items'){
1056 $cas = $this->cas_token;
1057 $res = [$object, $cas ? 'cas' : 'add'](...[...($cas ? [$cas] : []), $key, ...$args]);
1058 }
1059 }
1060 }
1061
1062 if(!isset($res)){
1063 if(isset($map[$method]) && array_all($keys, fn($k)=> $this->$k)){
1064 $res = ($map[$method])(...[...array_values(wpjam_pick($this, $keys)), ...$args]);
1065
1066 if($type == 'meta' && $method == 'get_items'){
1067 $this->pre_value = $res;
1068 }
1069 }else{
1070 $res = $method == 'delete_items' ? $this->update_items([]) : true;
1071 }
1072 }
1073 }
1074
1075 if($method == 'get_items'){
1076 $res = $this->compressed && $res && !is_array($res) ? wpjam_uncompress($res) : ($res ?: []);
1077 $res = $this->cached_items = $this->filter_items ? $this->filter_items($res) : $res;
1078 }else{
1079 unset($this->cached_items);
1080 }
1081
1082 return $res;
1083 }elseif(str_contains($method, '_setting')){
1084 if($this->option_name){
1085 $i = str_starts_with($method, 'update_') ? 2 : 1;
1086 $args = (isset($args[$i]) && !is_numeric($args[$i])) ? array_slice($args, 0, $i) : $args;
1087
1088 return ('wpjam_'.$method)($this->option_name, ...$args);
1089 }
1090 }elseif(in_array($method, ['increment', 'decrement'])){
1091 if($this->item_type == 'array'){
1092 [$k, $v] = array_pad($args, 2, 1);
1093
1094 return wpjam_tap(
1095 (int)$this->get($k)+($method == 'increment' ? $v : (0-$v)),
1096 fn($v)=> $this->process(fn($items)=> array_merge($items, [$k=>$v]))
1097 );
1098 }
1099 }elseif(in_array($method, ['insert', 'add', 'update', 'replace', 'set', 'delete', 'remove'])){
1100 return wpjam_retry($this->retry_times ?: 1, fn()=> $this->retry($method, $args));
1101 }
1102 }
1103
1104 protected function retry($method, $args){
1105 $items = $this->get_items();
1106 $type = $this->item_type;
1107 $key = $this->primary_key;
1108 $title = $this->primary_title ?: 'ID';
1109 $add = $method == 'insert' || ($method == 'add' && count($args) <= 1);
1110 $id = $add ? null : array_shift($args);
1111
1112 if(!$add){
1113 $id || wpjam_throw('empty_'.($key ?: 'id'), ($key ? $title : 'ID').'不能为空');
1114
1115 if($method == 'set'){
1116 $method = isset($items[$id]) ? 'set' : 'add';
1117 }elseif(($exist = isset($items[$id])) === ($method == 'add')){
1118 wpjam_throw(($exist ? 'duplicate_' : 'invalid_').($key ?: 'id'), ($key ? $title : 'ID').'-「'.$id.''.($exist ? '已存在' : '不存在'));
1119 }
1120 }
1121
1122 if(in_array($method, ['delete', 'remove'])){
1123 unset($items[$id]);
1124 }else{
1125 if(!$args || ($type == 'array' && !is_array($args[0]))){
1126 wpjam_throw(...($args ? ['invalid_item', '不是数组'] : ['empty_item', '不能为空']));
1127 }
1128
1129 $item = $args[0];
1130
1131 if($type == 'array'){
1132 if($add){
1133 if(in_array($key, ['option_key', 'id'])){
1134 $id = $items ? max(array_map(fn($id)=> (int)str_replace('option_key_', '', $id), array_keys($items)))+1 : 1;
1135 $id = $key == 'option_key' ? 'option_key_'.$id : $id;
1136 }else{
1137 $id = $item[$key] ?? '';
1138
1139 (!$id || isset($items[$id])) && wpjam_throw(($id ? 'duplicate_' : 'empty_').$key, $title.($id ? ''.$id.'」已被使用' : '不能为空'));
1140 }
1141 }
1142
1143 if(($uk = $this->unique_key) && ($add || isset($item[$uk]))){
1144 $uv = $item[$uk] ?? '';
1145 $blank = !$uv && !is_numeric($uv);
1146
1147 if($blank || array_find($items, fn($v, $k)=> ($add || $id != $k) && $v[$uk] == $uv)){
1148 wpjam_throw(($blank ? 'empty_' : 'duplicate_').$uk, ($this->unique_title ?: $uk).($blank ? '不能为空' : ''.$uv.'」已被使用'));
1149 }
1150 }
1151
1152 $item = [$key=>$id]+$item;
1153 }else{
1154 $add && in_array($item, $items) && wpjam_throw('duplicate_item', '不能重复');
1155 }
1156
1157 $item = $this->prepare_item ? $this->call('prepare_item', $item, $id, $method, $items) : $item;
1158 }
1159
1160 if(in_array($method, ['add', 'insert'])){
1161 ($max = $this->max_items) && count($items) >= $max && wpjam_throw('over_max_items', '最大�
1162 �许数量:'.$max);
1163 $last = $this->last ?? ($method == 'add');
1164
1165 if($type == 'array' || $id){
1166 $item = $type == 'array' ? array_filter($item, fn($v)=> !is_null($v)) : $item;
1167 $items = $last ? array_replace($items, [$id=>$item]) : [$id=>$item]+$items;
1168 }else{
1169 $last ? array_push($items, $item) : array_unshift($items, $item);
1170 }
1171 }elseif(in_array($method, ['set', 'update', 'replace'])){
1172 $items[$id] = $method == 'update' && $type == 'array' ? wp_parse_args($item, $items[$id]) : $item;
1173 }
1174
1175 if($type == 'array' && $items && in_array($key, ['option_key','id'])){
1176 $except = array_filter([$key, $this->parent_key]);
1177 $items = wpjam_map($items, fn($item)=> wpjam_except($item, $except));
1178 }
1179
1180 $result = $this->update_items($items);
1181
1182 return $result && $method == 'insert' && $type == 'array' ? ['id'=>$id, 'last'=>(bool)$last] : $result;
1183 }
1184
1185 public function process($cb){
1186 return is_closure($cb) ? wpjam_retry($this->retry_times ?: 1, fn()=> $this->update_items(wpjam_call($cb, $this->get_items()))) : null;
1187 }
1188
1189 public function query_items($args){
1190 $s = trim(wpjam_pull($args, 's') ?: '');
1191 $number = wpjam_pull($args, 'number') ?: 50;
1192 $offset = wpjam_pull($args, 'offset') ?: 0;
1193 $items = $this->parse_items();
1194 $items = array_values(($args || $s) ? array_filter($items, fn($item)=> (!$args || wpjam_matches($item, $args)) && (!$s || array_any($item, fn($v)=> str_contains($v, $s))) ) : $items);
1195
1196 return ['total'=>count($items), 'items'=>array_slice($items, $offset, $number)];
1197 }
1198
1199 public function parse_items($items=null){
1200 $items ??= $this->get_items();
1201
1202 if($this->item_type == 'array'){
1203 $items = $items && is_array($items) ? wpjam_map($items, fn($v, $k)=> [$this->primary_key => $k]+(is_array($v) ? $v : [])) : [];
1204 $items && wpjam_lazyload($this->lazyload_key, $items);
1205 }
1206
1207 return $items;
1208 }
1209
1210 public function get_results(){
1211 return $this->parse_items();
1212 }
1213
1214 public function reset(){
1215 return $this->delete_items();
1216 }
1217
1218 public function empty(){
1219 return wpjam_tap($this->get_items(), fn($v)=> $v && $this->delete_items());
1220 }
1221
1222 public function move($id, $data){
1223 return $this->process(fn($items)=> wpjam_pick($items, wpjam_move(array_keys($items), $id, $data)));
1224 }
1225
1226 public function get($id){
1227 return ($items = wpjam_pick($this->get_items(), [$id])) ? $this->parse_items($items)[$id] : null;
1228 }
1229
1230 public static function create($args, $name=null){
1231 if(!empty($args['option_name'])){
1232 $args += array_filter(['setting_name'=>wpjam_pull($args, 'items_field')]);
1233 $args += ['type'=>(!empty($args['setting_name']) ? 'setting' : 'option')];
1234 }elseif(!empty($args['items_model'])){ // 不建议
1235 $args += wpjam_fill(['get_items', 'update_items'], fn($k)=> [$args['items_model'], $k]);
1236 }elseif(wpjam_pull($args, 'type') == 'option_items'){ // 不建议
1237 $args += ['type'=>'option', 'option_name'=>$name];
1238 }else{
1239 $args = (!empty($args['items_type']) || (!empty($args['get_items']) && !empty($args['update_items']))) ? $args : [];
1240 }
1241
1242 return $args ? new static($args) : null;
1243 }
1244 }
1245
1246 class WPJAM_Notice extends WPJAM_Items{
1247 public static function render($type){
1248 foreach((self::get_instance($type))->get_items() as $key => $item){
1249 $data = ['notice_key'=>$key, 'notice_type'=>$type];
1250 $item += ['class'=>'is-dismissible', 'title'=>'', 'modal'=>0];
1251 $notice = trim($item['notice'] ?? '');
1252 $notice .= !empty($item['admin_url']) ? (($item['modal'] ? "\n\n" : ' ').'<a href="'.add_query_arg($data, home_url($item['admin_url'])).'">点击查看<span class="dashicons dashicons-arrow-right-alt"></span></a>') : '';
1253
1254 echo wpjam_tag('div', [
1255 'class' => $item['modal'] ? 'hidden notice-modal' : 'notice notice-'.$item['type'].' '.$item['class'],
1256 'data' => $item['modal'] ? ['title'=>$item['title'] ?: '消息'] : [],
1257 ], wpautop($notice).wpjam_get_page_button('delete_notice', ['data'=>$data]))."\n";
1258 }
1259 }
1260
1261 public static function add($item, $type='admin', $id=0){
1262 if(!$id || ($type == 'admin' ? (!is_multisite() || get_site($id)) : get_userdata($id))){
1263 $item = is_array($item) ? $item : ['notice'=>$item];
1264
1265 return (self::get_instance($type, $id))->insert($item+['type'=>'error', 'time'=>time(), 'key'=>md5(serialize($item))]);
1266 }
1267 }
1268
1269 public static function callback($data=[], $wp_die=true){
1270 $type = $data['notice_type'] ?? '';
1271 $key = $data['notice_key'] ?? '';
1272
1273 return ($type == 'user' || ($type == 'admin' && current_user_can('manage_options')))
1274 ? ($key ? (self::get_instance($type))->delete($key) : true)
1275 : ($wp_die ? wp_die('无效的类型') : true);
1276 }
1277
1278 public static function get_instance($type='admin', $id=0){
1279 $name = 'wpjam_notices';
1280 $id = (int)$id ?: ($type == 'user' ? get_current_user_id() : get_current_blog_id());
1281
1282 return wpjam_get_handler('notice:'.$type.':'.$id, static::create(($type == 'user' ? [
1283 'items_type' => 'meta',
1284 'meta_type' => 'user',
1285 'meta_key' => $name,
1286 'object_id' => $id
1287 ] : [
1288 'get_items' => fn()=> wpjam_get_option($name, $id),
1289 'update_items' => fn($items)=> wpjam_update_option($name, $items, $id),
1290 ])+[
1291 'primary_key' => 'key',
1292 'filter_items' => fn($items)=> array_filter(($items ?: []), fn($v)=> $v['time']>(time()-MONTH_IN_SECONDS*3) && trim($v['notice']))
1293 ]));
1294 }
1295
1296 public static function on_admin_init(){
1297 static::callback($_GET, false);
1298
1299 add_action('all_admin_notices', fn()=> [current_user_can('manage_options') && static::render('admin'), static::render('user')]);
1300
1301 wpjam_register_page_action('delete_notice', [
1302 'button_text' => '删除',
1303 'tag' => 'span',
1304 'class' => 'hidden delete-notice',
1305 'validate' => true,
1306 'direct' => true,
1307 'callback' => [static::class, 'callback']
1308 ]);
1309 }
1310 }
1311
1312 class WPJAM_Lazyloader extends WPJAM_Args{
1313 public function __call($method, $args){
1314 $name = $method.'['.array_shift($args).']';
1315
1316 return $args ? $this->update_arg($name, ...$args) : $this->get_arg($name);
1317 }
1318
1319 public function queue($name, $ids){
1320 if(!$name || !($ids = array_filter($ids))){
1321 return;
1322 }
1323
1324 if(is_array($name)){
1325 return array_walk($name, fn($n, $k)=> $this->queue($n, is_numeric($k) ? $ids : array_column($ids, $k)));
1326 }
1327
1328 $ids = array_unique($ids);
1329
1330 if($name == 'post'){
1331 _prime_post_caches($ids, false, false);
1332
1333 wpjam_lazyload('post_meta', $ids);
1334 }elseif(in_array($name, ['blog', 'site', 'term', 'comment'])){
1335 ('_prime_'.($name == 'blog' ? 'site' : $name).'_caches')($ids);
1336 }elseif(in_array($name, ['term_meta', 'comment_meta', 'blog_meta'])){
1337 wp_metadata_lazyloader()->queue_objects(wpjam_suffix($name, '-', '_meta'), $ids);
1338 }else{
1339 $pending = $this->pending($name);
1340
1341 if(!$pending){
1342 $loader = $this->loader($name) ?: (str_ends_with($name, '_meta') ? [
1343 'filter' => 'get_'.$name.'data',
1344 'callback' => fn($v)=> update_meta_cache(wpjam_suffix($name, '-', '_meta'), $v)
1345 ] : []);
1346
1347 $loader && wpjam_once($loader['filter'], 'tap', fn()=> $this->load($name, $loader['callback']));
1348 }
1349
1350 $this->pending($name, array_merge($pending ?: [], $ids));
1351 }
1352 }
1353
1354 public function load($name, $cb){
1355 if($name && ($pending = $this->pending($name))){
1356 wpjam_call($cb, array_unique($pending));
1357
1358 $this->pending($name, []);
1359 }
1360 }
1361
1362 public static function get_instance(){
1363 static $object;
1364 return $object ??= new self();
1365 }
1366 }
1367
1368 class WPJAM_Cache extends WPJAM_Args{
1369 public function __call($method, $args){
1370 $method = wpjam_prefix($method, '-', 'cache_');
1371 $multi = str_contains($method, '_multiple');
1372 $gnd = array_any(['get', 'delete'], fn($k)=> str_contains($method, $k));
1373
1374 $ik = $method == 'cas' ? 1 : 0;
1375 $ig = $ik+(($gnd || $multi) ? 1 : 2);
1376 $ie = $ig+(str_contains($method, '_salted') ? 1 : 0);
1377
1378 if(count($args) >= $ig){
1379 $cb = 'wp_cache_'.$method;
1380 $key = $args[$ik];
1381
1382 if($prefix = $this->prefix){
1383 $args[$ik] = $multi ? ($gnd ? 'wpjam_map' : 'wpjam_array')($key, fn($k)=> $prefix.':'.$k) : $prefix.':'.$key;
1384 }
1385
1386 if(!$gnd){
1387 $args[$ie] ??= $this->time ?: DAY_IN_SECONDS;
1388 }
1389
1390 if($method == 'get_with_cas'){
1391 $value = $cb($args[$ik], $this->group, $cas);
1392
1393 return $value === false ? false : ['value'=>$value, 'cas'=>$cas];
1394 }
1395
1396 $result = $cb(...wpjam_add_at($args, $ig, $this->group));
1397
1398 if($result && $method == 'get_multiple'){
1399 return wpjam_array($key, fn($i, $k) => (($v = $result[$args[0][$i]]) !== false) ? [$k, $v] : null);
1400 }
1401
1402 return $result;
1403 }
1404 }
1405
1406 public function is_over($key, $max, $time){
1407 $times = $this->get($key) ?: 0;
1408
1409 return $times > $max || ($this->set($key, $times+1, ($max == $times && $time > 60) ? $time : 60) && false);
1410 }
1411
1412 public static function create($group, $args=[]){
1413 if(is_object($group)){
1414 [$object, $group, $global] = [$group, ...((array)$group->cache_group+['', ''])];
1415
1416 $args = compact('group', 'global')+['prefix'=>$object->cache_prefix, 'time'=>$object->cache_time];
1417 }else{
1418 $args = is_array($group) ? $group : ['group'=>$group]+$args;
1419 }
1420
1421 if(!empty($args['group'])){
1422 if(wpjam_pull($args, 'global')){
1423 wp_cache_add_global_groups($args['group']);
1424 }
1425
1426 if(empty($args['time']) && !empty($args['cache_time'])){
1427 $args['time'] = $args['cache_time'];
1428 }
1429
1430 $name = wpjam_join(':', $args['group'], $args['prefix'] ?? '');
1431
1432 return wpjam_get_registered(static::class, $name) ?: wpjam_register(static::class, $name, $args);
1433 }
1434 }
1435
1436 public static function remember($key, $group, $cb=null, $args=[]){
1437 $fix = is_bool($group) ? ($group ? 'site_' : '').'transient' : '';
1438
1439 if($cb === false){
1440 return $fix ? ('delete_'.$fix)($key) : wp_cache_delete($key, $group);
1441 }
1442
1443 $args = is_numeric($args) ? ['expire'=>$args] : $args;
1444 $expire = ($args['expire'] ?? '') ?: 86400;
1445 $force = $args['force'] ?? false;
1446 $value = $fix ? ('get_'.$fix)($key) : wp_cache_get($key, $group, ($force === 'get' || $force === true));
1447
1448 return ($cb && ($value === false || $force === 'set' || $force === true)) ? wpjam_tap(
1449 $cb($value, $key, ...($fix ? [] : [$group ?: 'default'])),
1450 fn($value)=> $value !== false && ($fix ? ('set_'.$fix)($key, $value, $expire) : wp_cache_set($key, $value, $group, $expire))
1451 ) : $value;
1452 }
1453
1454 public static function table($name, $key, ...$args){
1455 $clean = $args && $args[0];
1456 $table = in_array($name, ['post', 'term', 'user']) ? $GLOBALS['wpdb']->{$name.'s'} : $name;
1457 $status = static::remember($table.':status', false, $clean ? false : fn()=> $GLOBALS['wpdb']->get_row("SHOW TABLE STATUS LIKE '{$table}'"));
1458
1459 return !$clean && $key == 'max_id' ? ($status ? $status->Auto_increment : null) : $status;
1460 }
1461
1462 public static function deleted_ids($name, ...$args){
1463 $key = 'deleted_ids:'.$name;
1464 $items = get_transient($key) ?: [];
1465
1466 if($args && $args[0]){
1467 if(is_string($args[0]) && try_suffix($args[0], '-', '-')){
1468 static::table($name, 'max_id', true);
1469
1470 $id = (int)$args[0];
1471 $update = in_array($id, $items) ? array_diff($items, [$id]) : null;
1472 }else{
1473 $ids = array_diff((array)$args[0], $items);
1474 $max = static::table($name, 'max_id');
1475 $ids = $ids && $max ? array_filter($ids, fn($id)=> $id < $max) : $ids;
1476 $update = $ids ? array_merge($items, $ids) : null;
1477 }
1478
1479 if(isset($update)){
1480 $items = array_values($update);
1481
1482 set_transient($key, $items, 30);
1483 }
1484 }
1485
1486 return $items;
1487 }
1488
1489 public static function verification($group, $key, ...$args){
1490 $group = (is_array($group) ? $group : ['group'=>$group])+['max_attempts'=>5, 'interval'=>1, 'expire'=>30, 'global'=>true];
1491 $cache = self::create(['group'=>'verification_code', 'prefix'=>$group['group'] ?: 'default']+$group);
1492
1493 if($max = $group['max_attempts']){
1494 $times = (int)$cache->get($key.':failed_times');
1495 ($times > $max) && wpjam_throw('too_many_attempts', sprintf(__('Failed attempts exceeded, Please try again in %d minutes.', 'wpjam'), $group['expire']/2));
1496 }
1497
1498 if($args){
1499 if(!$args[0] || (int)$args[0] !== (int)$cache->get($key.':code')){
1500 $max && $cache->set($key.':failed_times', $times+1, $group['expire']*30);
1501
1502 wpjam_throw('invalid_code');
1503 }
1504
1505 return true;
1506 }
1507
1508 if($group['interval']){
1509 $cache->get($key.':time') && wpjam_throw('error', sprintf(__('A verification code was sent %d minutes ago.', 'wpjam'), $group['interval']));
1510
1511 $cache->set($key.':time', time(), $group['interval']*60);
1512 }
1513
1514 $code = rand(100000, 999999);
1515
1516 $cache->set($key.':code', $code, $group['expire']*60);
1517
1518 return $code;
1519 }
1520 }