PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / wishlist / Wishlist_Service.php

Wishlist_Service.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/wishlist/Wishlist_Service.php

783 lines 25.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace King_Addons\Wishlist;
4
5 use DateTime;
6 use DateTimeZone;
7 use WP_Error;
8 use wpdb;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Provides CRUD operations for wishlist items and lists.
16 */
17 class Wishlist_Service
18 {
19 private const DEFAULT_WISHLIST_ID = 'default';
20 private const CACHE_TTL = 600;
21
22 private Wishlist_Session $session;
23 private string $session_key;
24 private int $user_id;
25 private string $active_wishlist_id;
26
27 /**
28 * Set up wishlist service with current user and session.
29 *
30 * @param int|null $user_id Optional user identifier.
31 * @param string|null $session_key Optional session key for guests.
32 */
33 public function __construct(?int $user_id = null, ?string $session_key = null)
34 {
35 $this->session = new Wishlist_Session();
36 $this->user_id = $user_id ?? get_current_user_id();
37 $this->session_key = $session_key ?: $this->session->get_session_key();
38 $this->active_wishlist_id = $this->resolve_active_wishlist_id();
39 }
40
41 /**
42 * Get current active wishlist identifier.
43 *
44 * @return string Active wishlist id.
45 */
46 public function get_active_wishlist_id(): string
47 {
48 return $this->active_wishlist_id;
49 }
50
51 /**
52 * Set current active wishlist identifier.
53 *
54 * @param string $wishlist_id Wishlist identifier.
55 * @return void
56 */
57 public function set_active_wishlist_id(string $wishlist_id): void
58 {
59 $this->active_wishlist_id = $this->normalize_wishlist_id($wishlist_id);
60 if ($this->user_id > 0) {
61 update_user_meta($this->user_id, 'king_addons_active_wishlist_id', $this->active_wishlist_id);
62 }
63 }
64
65 /**
66 * Add product to wishlist.
67 *
68 * @param int $product_id Product identifier.
69 * @param int $variation_id Variation identifier.
70 * @param int $qty Quantity to store.
71 * @param string|null $wishlist_id Wishlist identifier.
72 * @return array|WP_Error Operation result.
73 */
74 public function add_item(int $product_id, int $variation_id = 0, int $qty = 1, ?string $wishlist_id = null)
75 {
76 $validation = $this->validate_product($product_id, $variation_id);
77 if (is_wp_error($validation)) {
78 return $validation;
79 }
80
81 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
82 $qty = max(1, $qty);
83 $now = $this->now();
84
85 global $wpdb;
86 $table = Wishlist_DB::get_items_table();
87
88 $existing = $this->get_item_row($wishlist_id, $product_id, $variation_id);
89
90 if ($existing) {
91 $wpdb->update(
92 $table,
93 [
94 'qty' => $qty,
95 'updated_at' => $now,
96 ],
97 [
98 'id' => intval($existing->id),
99 ],
100 ['%d', '%s'],
101 ['%d']
102 );
103 } else {
104 $wpdb->insert(
105 $table,
106 [
107 'user_id' => $this->user_id,
108 'session_key' => $this->session_key,
109 'wishlist_id' => $wishlist_id,
110 'product_id' => $product_id,
111 'variation_id' => $variation_id,
112 'qty' => $qty,
113 'created_at' => $now,
114 'updated_at' => $now,
115 'meta' => null,
116 ],
117 ['%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s']
118 );
119 }
120
121 $this->ensure_default_list($wishlist_id);
122 $this->invalidate_cache($wishlist_id);
123
124 return [
125 'success' => true,
126 'wishlist_id' => $wishlist_id,
127 'count' => $this->get_count($wishlist_id, true),
128 ];
129 }
130
131 /**
132 * Remove a product from wishlist.
133 *
134 * @param int $product_id Product identifier.
135 * @param int $variation_id Variation identifier.
136 * @param string|null $wishlist_id Wishlist identifier.
137 * @return array Operation result.
138 */
139 public function remove_item(int $product_id, int $variation_id = 0, ?string $wishlist_id = null): array
140 {
141 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
142 $row = $this->get_item_row($wishlist_id, $product_id, $variation_id);
143
144 if (!$row) {
145 return [
146 'success' => false,
147 'message' => esc_html__('Item not found in wishlist.', 'king-addons'),
148 'count' => $this->get_count($wishlist_id, true),
149 ];
150 }
151
152 global $wpdb;
153 $wpdb->delete(
154 Wishlist_DB::get_items_table(),
155 ['id' => intval($row->id)],
156 ['%d']
157 );
158
159 $this->invalidate_cache($wishlist_id);
160
161 return [
162 'success' => true,
163 'wishlist_id' => $wishlist_id,
164 'count' => $this->get_count($wishlist_id, true),
165 ];
166 }
167
168 /**
169 * Toggle wishlist state for a product.
170 *
171 * @param int $product_id Product identifier.
172 * @param int $variation_id Variation identifier.
173 * @param int $qty Quantity to store.
174 * @param string|null $wishlist_id Wishlist identifier.
175 * @return array|WP_Error Operation result.
176 */
177 public function toggle_item(int $product_id, int $variation_id = 0, int $qty = 1, ?string $wishlist_id = null)
178 {
179 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
180 $existing = $this->get_item_row($wishlist_id, $product_id, $variation_id);
181
182 if ($existing) {
183 return $this->remove_item($product_id, $variation_id, $wishlist_id);
184 }
185
186 return $this->add_item($product_id, $variation_id, $qty, $wishlist_id);
187 }
188
189 /**
190 * Retrieve wishlist items for current user or session.
191 *
192 * @param string|null $wishlist_id Wishlist identifier.
193 * @return array<int, object> List of wishlist rows.
194 */
195 public function get_items(?string $wishlist_id = null): array
196 {
197 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
198 global $wpdb;
199
200 $where = $this->get_scope_where($wishlist_id);
201 $table = Wishlist_DB::get_items_table();
202 $query = "SELECT * FROM {$table} WHERE {$where['sql']} ORDER BY created_at DESC";
203
204 /** @var array<int, object> $items */
205 $items = $wpdb->get_results($wpdb->prepare($query, $where['params']));
206
207 return $items ?: [];
208 }
209
210 /**
211 * Determine if wishlist already contains a product.
212 *
213 * @param int $product_id Product identifier.
214 * @param int $variation_id Variation identifier.
215 * @param string|null $wishlist_id Wishlist identifier.
216 * @return bool Whether the item is present.
217 */
218 public function has_item(int $product_id, int $variation_id = 0, ?string $wishlist_id = null): bool
219 {
220 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
221 return (bool) $this->get_item_row($wishlist_id, $product_id, $variation_id);
222 }
223
224 /**
225 * Get available wishlists for current scope.
226 *
227 * @return array<int, object> Lists rows.
228 */
229 public function get_lists(): array
230 {
231 global $wpdb;
232
233 $lists_table = Wishlist_DB::get_lists_table();
234 $where = $this->user_id > 0
235 ? $wpdb->prepare('user_id = %d', $this->user_id)
236 : $wpdb->prepare('session_key = %s', $this->session_key);
237
238 $lists = $wpdb->get_results("SELECT * FROM {$lists_table} WHERE {$where} ORDER BY created_at DESC");
239
240 if (empty($lists)) {
241 $this->ensure_default_list(self::DEFAULT_WISHLIST_ID);
242 $lists = $wpdb->get_results("SELECT * FROM {$lists_table} WHERE {$where} ORDER BY created_at DESC");
243 }
244
245 $unique = [];
246 foreach ($lists ?: [] as $list) {
247 $key = (string) ($list->slug ?: $list->id);
248 if (!isset($unique[$key])) {
249 $unique[$key] = $list;
250 }
251 }
252
253 return array_values($unique);
254 }
255
256 /**
257 * Create a new wishlist record.
258 *
259 * @param string $title Wishlist title.
260 * @param string $visibility Visibility mode.
261 * @return array<string, mixed>|WP_Error Created list data or error.
262 */
263 public function create_list(string $title, string $visibility = 'private')
264 {
265 global $wpdb;
266
267 if (empty($title)) {
268 return new WP_Error('wishlist_title_missing', esc_html__('List title is required.', 'king-addons'));
269 }
270
271 $slug_base = sanitize_title($title);
272 $slug = $slug_base ?: 'list-' . wp_generate_uuid4();
273 $lists_table = Wishlist_DB::get_lists_table();
274 $counter = 1;
275
276 while ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$lists_table} WHERE slug = %s", $slug))) {
277 $slug = $slug_base . '-' . $counter;
278 ++$counter;
279 }
280
281 $now = $this->now();
282 $wpdb->insert(
283 $lists_table,
284 [
285 'user_id' => $this->user_id,
286 'session_key' => $this->user_id > 0 ? '' : $this->session_key,
287 'title' => $title,
288 'slug' => $slug,
289 'visibility' => in_array($visibility, ['private', 'shared', 'public'], true) ? $visibility : 'private',
290 'created_at' => $now,
291 'updated_at' => $now,
292 ],
293 ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
294 );
295
296 return [
297 'id' => $wpdb->insert_id,
298 'slug' => $slug,
299 'title' => $title,
300 'visibility' => $visibility,
301 ];
302 }
303
304 /**
305 * Update item note stored in meta JSON.
306 *
307 * @param int $product_id Product identifier.
308 * @param int $variation_id Variation identifier.
309 * @param string $note Note content.
310 * @param string|null $wishlist_id Wishlist identifier.
311 * @return bool|WP_Error Whether update succeeded.
312 */
313 public function update_item_note(int $product_id, int $variation_id, string $note, ?string $wishlist_id = null)
314 {
315 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
316 $row = $this->get_item_row($wishlist_id, $product_id, $variation_id);
317
318 if (!$row) {
319 return new WP_Error('wishlist_note_missing_item', esc_html__('Item not found for note update.', 'king-addons'));
320 }
321
322 $meta = [];
323 if (!empty($row->meta)) {
324 $decoded = json_decode($row->meta, true);
325 if (is_array($decoded)) {
326 $meta = $decoded;
327 }
328 }
329
330 $meta['note'] = wp_strip_all_tags(wp_trim_words($note, 100));
331
332 global $wpdb;
333 $wpdb->update(
334 Wishlist_DB::get_items_table(),
335 [
336 'meta' => wp_json_encode($meta),
337 'updated_at' => $this->now(),
338 ],
339 ['id' => intval($row->id)],
340 ['%s', '%s'],
341 ['%d']
342 );
343
344 return true;
345 }
346
347 /**
348 * Get aggregated wishlist stats by product with optional date filtering.
349 *
350 * @param string|null $date_from Start date (Y-m-d format).
351 * @param string|null $date_to End date (Y-m-d format).
352 * @return array<int, array<string, mixed>> Stats per product.
353 */
354 public function get_product_stats(?string $date_from = null, ?string $date_to = null): array
355 {
356 global $wpdb;
357 $items_table = Wishlist_DB::get_items_table();
358 $conversions_table = Wishlist_DB::get_conversions_table();
359
360 $where_clauses = [];
361 $params = [];
362
363 if ($date_from) {
364 $where_clauses[] = 'i.created_at >= %s';
365 $params[] = $date_from . ' 00:00:00';
366 }
367
368 if ($date_to) {
369 $where_clauses[] = 'i.created_at <= %s';
370 $params[] = $date_to . ' 23:59:59';
371 }
372
373 $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
374
375 // Build the query with conversion stats
376 $query = "
377 SELECT
378 i.product_id,
379 COUNT(DISTINCT i.id) as adds,
380 COALESCE(c.conversions, 0) as conversions,
381 COALESCE(c.revenue, 0) as revenue
382 FROM {$items_table} i
383 LEFT JOIN (
384 SELECT
385 product_id,
386 COUNT(DISTINCT order_id) as conversions,
387 SUM(order_item_total) as revenue
388 FROM {$conversions_table}
389 GROUP BY product_id
390 ) c ON i.product_id = c.product_id
391 {$where_sql}
392 GROUP BY i.product_id
393 ORDER BY adds DESC
394 LIMIT 100
395 ";
396
397 if (!empty($params)) {
398 $results = $wpdb->get_results($wpdb->prepare($query, $params), ARRAY_A);
399 } else {
400 $results = $wpdb->get_results($query, ARRAY_A);
401 }
402
403 return $results ?: [];
404 }
405
406 /**
407 * Get total wishlist statistics summary.
408 *
409 * @param string|null $date_from Start date (Y-m-d format).
410 * @param string|null $date_to End date (Y-m-d format).
411 * @return array<string, mixed> Summary stats.
412 */
413 public function get_stats_summary(?string $date_from = null, ?string $date_to = null): array
414 {
415 global $wpdb;
416 $items_table = Wishlist_DB::get_items_table();
417 $conversions_table = Wishlist_DB::get_conversions_table();
418
419 $where_items = '';
420 $where_conv = '';
421 $params_items = [];
422 $params_conv = [];
423
424 if ($date_from) {
425 $where_items .= ($where_items ? ' AND ' : 'WHERE ') . 'created_at >= %s';
426 $params_items[] = $date_from . ' 00:00:00';
427 $where_conv .= ($where_conv ? ' AND ' : 'WHERE ') . 'converted_at >= %s';
428 $params_conv[] = $date_from . ' 00:00:00';
429 }
430
431 if ($date_to) {
432 $where_items .= ($where_items ? ' AND ' : 'WHERE ') . 'created_at <= %s';
433 $params_items[] = $date_to . ' 23:59:59';
434 $where_conv .= ($where_conv ? ' AND ' : 'WHERE ') . 'converted_at <= %s';
435 $params_conv[] = $date_to . ' 23:59:59';
436 }
437
438 // Total adds
439 $adds_query = "SELECT COUNT(*) FROM {$items_table} {$where_items}";
440 $total_adds = !empty($params_items)
441 ? (int) $wpdb->get_var($wpdb->prepare($adds_query, $params_items))
442 : (int) $wpdb->get_var($adds_query);
443
444 // Total conversions and revenue
445 $conv_query = "SELECT COUNT(DISTINCT order_id) as conversions, COALESCE(SUM(order_item_total), 0) as revenue FROM {$conversions_table} {$where_conv}";
446 $conv_row = !empty($params_conv)
447 ? $wpdb->get_row($wpdb->prepare($conv_query, $params_conv), ARRAY_A)
448 : $wpdb->get_row($conv_query, ARRAY_A);
449
450 $total_conversions = (int) ($conv_row['conversions'] ?? 0);
451 $total_revenue = (float) ($conv_row['revenue'] ?? 0);
452
453 // Unique users
454 $users_query = "SELECT COUNT(DISTINCT user_id) FROM {$items_table} WHERE user_id > 0 {$where_items}";
455 // Fix the WHERE clause for users query
456 if ($where_items) {
457 $users_query = str_replace('WHERE user_id > 0 WHERE', 'WHERE user_id > 0 AND', $users_query);
458 }
459 $unique_users = !empty($params_items)
460 ? (int) $wpdb->get_var($wpdb->prepare($users_query, $params_items))
461 : (int) $wpdb->get_var($users_query);
462
463 return [
464 'total_adds' => $total_adds,
465 'total_conversions' => $total_conversions,
466 'total_revenue' => $total_revenue,
467 'unique_users' => $unique_users,
468 'conversion_rate' => $total_adds > 0 ? round(($total_conversions / $total_adds) * 100, 2) : 0,
469 ];
470 }
471
472 /**
473 * Get wishlist items count.
474 *
475 * @param string|null $wishlist_id Wishlist identifier.
476 * @param bool $force_refresh Skip cache.
477 * @return int Count of items.
478 */
479 public function get_count(?string $wishlist_id = null, bool $force_refresh = false): int
480 {
481 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
482 $cache_key = $this->get_cache_key($wishlist_id);
483 $cache_enabled = Wishlist_Settings::get('cache_enabled', false);
484 $cache_ttl = max(0, intval(Wishlist_Settings::get('cache_ttl', self::CACHE_TTL)));
485
486 if (!$force_refresh && $cache_enabled) {
487 $cached = get_transient($cache_key);
488 if (false !== $cached) {
489 return intval($cached);
490 }
491 }
492
493 global $wpdb;
494 $where = $this->get_scope_where($wishlist_id);
495 $table = Wishlist_DB::get_items_table();
496 $query = "SELECT COUNT(id) FROM {$table} WHERE {$where['sql']}";
497 $count = intval($wpdb->get_var($wpdb->prepare($query, $where['params'])));
498
499 if ($cache_enabled) {
500 set_transient($cache_key, $count, $cache_ttl ?: self::CACHE_TTL);
501 }
502
503 return $count;
504 }
505
506 /**
507 * Merge guest wishlist items into a user account on login.
508 *
509 * @param int $user_id User identifier.
510 * @param string $session_key Guest session key.
511 * @return void
512 */
513 public function merge_guest_items(int $user_id, string $session_key): void
514 {
515 if ($user_id <= 0 || empty($session_key)) {
516 return;
517 }
518
519 global $wpdb;
520 $items_table = Wishlist_DB::get_items_table();
521 $lists_table = Wishlist_DB::get_lists_table();
522
523 $guest_items = $wpdb->get_results(
524 $wpdb->prepare(
525 "SELECT * FROM {$items_table} WHERE session_key = %s",
526 $session_key
527 )
528 );
529
530 foreach ($guest_items as $item) {
531 $wishlist_id = $this->normalize_wishlist_id($item->wishlist_id);
532 $existing = $wpdb->get_var(
533 $wpdb->prepare(
534 "SELECT id FROM {$items_table} WHERE user_id = %d AND wishlist_id = %s AND product_id = %d AND variation_id = %d",
535 $user_id,
536 $wishlist_id,
537 $item->product_id,
538 $item->variation_id
539 )
540 );
541
542 if ($existing) {
543 $wpdb->update(
544 $items_table,
545 [
546 'qty' => max(intval($item->qty), 1),
547 'updated_at' => $this->now(),
548 ],
549 ['id' => intval($existing)],
550 ['%d', '%s'],
551 ['%d']
552 );
553 } else {
554 $wpdb->insert(
555 $items_table,
556 [
557 'user_id' => $user_id,
558 'session_key' => '',
559 'wishlist_id' => $wishlist_id,
560 'product_id' => intval($item->product_id),
561 'variation_id' => intval($item->variation_id),
562 'qty' => max(intval($item->qty), 1),
563 'created_at' => $this->now(),
564 'updated_at' => $this->now(),
565 'meta' => $item->meta,
566 ],
567 ['%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s']
568 );
569 }
570 }
571
572 // Transfer guest lists ownership for Pro multi-list support.
573 $wpdb->update(
574 $lists_table,
575 [
576 'user_id' => $user_id,
577 'session_key' => '',
578 'updated_at' => $this->now(),
579 ],
580 [
581 'session_key' => $session_key,
582 ],
583 ['%d', '%s', '%s'],
584 ['%s']
585 );
586
587 // Remove guest rows to avoid duplication.
588 $wpdb->delete(
589 $items_table,
590 ['session_key' => $session_key],
591 ['%s']
592 );
593
594 $this->session->clear();
595 $this->invalidate_cache(self::DEFAULT_WISHLIST_ID);
596 }
597
598 /**
599 * Remove cached counts for the wishlist.
600 *
601 * @param string $wishlist_id Wishlist identifier.
602 * @return void
603 */
604 public function invalidate_cache(string $wishlist_id): void
605 {
606 delete_transient($this->get_cache_key($wishlist_id));
607 }
608
609 /**
610 * Ensure a default list row exists.
611 *
612 * @param string $wishlist_id Wishlist identifier.
613 * @return void
614 */
615 public function ensure_default_list(string $wishlist_id): void
616 {
617 global $wpdb;
618
619 $lists_table = Wishlist_DB::get_lists_table();
620
621 if ($this->user_id > 0) {
622 $existing_row = $wpdb->get_var(
623 $wpdb->prepare(
624 "SELECT id FROM {$lists_table} WHERE user_id = %d AND slug = %s LIMIT 1",
625 $this->user_id,
626 $wishlist_id
627 )
628 );
629 } else {
630 $existing_row = $wpdb->get_var(
631 $wpdb->prepare(
632 "SELECT id FROM {$lists_table} WHERE user_id = 0 AND session_key = %s AND slug = %s LIMIT 1",
633 $this->session_key,
634 $wishlist_id
635 )
636 );
637 }
638
639 if ($existing_row) {
640 return;
641 }
642
643 $wpdb->insert(
644 $lists_table,
645 [
646 'user_id' => $this->user_id,
647 'session_key' => $this->user_id > 0 ? '' : $this->session_key,
648 'title' => esc_html__('My Wishlist', 'king-addons'),
649 'slug' => $wishlist_id,
650 'visibility' => 'private',
651 'created_at' => $this->now(),
652 'updated_at' => $this->now(),
653 ],
654 ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
655 );
656 }
657
658 /**
659 * Get a single wishlist item row.
660 *
661 * @param string $wishlist_id Wishlist identifier.
662 * @param int $product_id Product identifier.
663 * @param int $variation_id Variation identifier.
664 * @return object|null Wishlist row.
665 */
666 private function get_item_row(string $wishlist_id, int $product_id, int $variation_id): ?object
667 {
668 global $wpdb;
669 $table = Wishlist_DB::get_items_table();
670 $where = $this->get_scope_where($wishlist_id);
671 $where['sql'] .= ' AND product_id = %d AND variation_id = %d';
672 $where['params'][] = $product_id;
673 $where['params'][] = $variation_id;
674
675 $query = "SELECT * FROM {$table} WHERE {$where['sql']} LIMIT 1";
676
677 return $wpdb->get_row($wpdb->prepare($query, $where['params']));
678 }
679
680 /**
681 * Validate product and variation existence.
682 *
683 * @param int $product_id Product identifier.
684 * @param int $variation_id Variation identifier.
685 * @return bool|WP_Error Validation result.
686 */
687 private function validate_product(int $product_id, int $variation_id)
688 {
689 if (!function_exists('wc_get_product')) {
690 return new WP_Error('wishlist_no_wc', esc_html__('WooCommerce is required for wishlist.', 'king-addons'));
691 }
692
693 $product = wc_get_product($variation_id > 0 ? $variation_id : $product_id);
694 if (!$product) {
695 return new WP_Error('wishlist_invalid_product', esc_html__('Product not found.', 'king-addons'));
696 }
697
698 return true;
699 }
700
701 /**
702 * Build scope-aware WHERE clause for queries.
703 *
704 * @param string $wishlist_id Wishlist identifier.
705 * @return array{sql:string,params:array<int, mixed>} Query fragment.
706 */
707 private function get_scope_where(string $wishlist_id): array
708 {
709 if ($this->user_id > 0) {
710 return [
711 'sql' => 'user_id = %d AND wishlist_id = %s',
712 'params' => [$this->user_id, $wishlist_id],
713 ];
714 }
715
716 return [
717 'sql' => 'session_key = %s AND wishlist_id = %s',
718 'params' => [$this->session_key, $wishlist_id],
719 ];
720 }
721
722 /**
723 * Normalize wishlist identifier.
724 *
725 * @param string|null $wishlist_id Wishlist identifier.
726 * @return string Normalized wishlist id.
727 */
728 private function normalize_wishlist_id(?string $wishlist_id): string
729 {
730 $resolved = $wishlist_id ?: $this->active_wishlist_id ?: self::DEFAULT_WISHLIST_ID;
731 $resolved = sanitize_title($resolved);
732
733 if (empty($resolved)) {
734 $resolved = self::DEFAULT_WISHLIST_ID;
735 }
736
737 return $resolved;
738 }
739
740 /**
741 * Resolve the active wishlist id from user meta or default.
742 *
743 * @return string Active wishlist id.
744 */
745 private function resolve_active_wishlist_id(): string
746 {
747 if ($this->user_id > 0) {
748 $saved = get_user_meta($this->user_id, 'king_addons_active_wishlist_id', true);
749 if (!empty($saved)) {
750 return $this->normalize_wishlist_id($saved);
751 }
752 }
753
754 return self::DEFAULT_WISHLIST_ID;
755 }
756
757 /**
758 * Build cache key per scope and wishlist.
759 *
760 * @param string $wishlist_id Wishlist identifier.
761 * @return string Cache key.
762 */
763 private function get_cache_key(string $wishlist_id): string
764 {
765 $scope = $this->user_id > 0 ? 'user-' . $this->user_id : 'sess-' . $this->session_key;
766 return 'king_addons_wishlist_count_' . $scope . '_' . $wishlist_id;
767 }
768
769 /**
770 * Current UTC datetime string for DB writes.
771 *
772 * @return string Datetime in mysql format.
773 */
774 private function now(): string
775 {
776 $dt = new DateTime('now', new DateTimeZone('UTC'));
777 return $dt->format('Y-m-d H:i:s');
778 }
779 }
780
781
782
783