PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 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 All 41 releases
king-addons / includes / wishlist / Wishlist_Service.php

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

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