PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Utils / Cache.php

Cache.php in Yatra – Travel Booking & Tour Operator Software 3.0.5, at app/Utils/Cache.php

675 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Utils;
6
7 /**
8 * Yatra Cache Manager
9 *
10 * Performance and entity/query caching. Use {@see self::isEnabled()} before relying on stored data.
11 * Controlled by option `yatra_cache_enabled`, {@see WP_DEBUG}, and option `yatra_debug_mode`
12 * (Settings → Advanced).
13 *
14 * **Not routed through this class (by design):** WordPress transients used for rate limiting, OAuth
15 * state, booking session tokens, telemetry throttles, and Pro Trip Consent draft storage — these are
16 * security/session flows, not optional performance caches.
17 */
18 class Cache
19 {
20 /**
21 * Cache prefixes for different data types
22 */
23 public const PREFIX_TABLE_EXISTS = 'yatra_table_exists_';
24 public const PREFIX_TRIP_DATA = 'yatra_trip_';
25 public const PREFIX_BOOKING_DATA = 'yatra_booking_';
26 public const PREFIX_CUSTOMER_DATA = 'yatra_customer_';
27 public const PREFIX_ACTIVITY_DATA = 'yatra_activity_';
28 public const PREFIX_DESTINATION_DATA = 'yatra_destination_';
29 public const PREFIX_QUERY_RESULT = 'yatra_query_';
30 public const PREFIX_STATS = 'yatra_stats_';
31 public const PREFIX_ITINERARY = 'yatra_itinerary_';
32 public const PREFIX_ATTRIBUTES = 'yatra_attributes_';
33 public const PREFIX_ITEM_TYPES = 'yatra_item_types_';
34 public const PREFIX_FRONTEND_ATTRIBUTES = 'yatra_frontend_attributes_';
35
36 /**
37 * Pro plugin cache prefixes
38 */
39 public const PREFIX_PRO_DYNAMIC_PRICING = 'yatra_pro_dynamic_pricing_';
40 public const PREFIX_PRO_ADDITIONAL_SERVICES = 'yatra_pro_additional_services_';
41 public const PREFIX_PRO_EMAIL_AUTOMATION = 'yatra_pro_email_automation_';
42 public const PREFIX_PRO_ABANDONED_BOOKING = 'yatra_pro_abandoned_booking_';
43 public const PREFIX_PRO_TRIP_CONSENT = 'yatra_pro_trip_consent_';
44 public const PREFIX_PRO_GOOGLE_CALENDAR = 'yatra_pro_google_calendar_';
45 public const PREFIX_PRO_MAILCHIMP = 'yatra_pro_mailchimp_';
46 public const PREFIX_PRO_FACEBOOK_PIXEL = 'yatra_pro_facebook_pixel_';
47 public const PREFIX_PRO_GOOGLE_ANALYTICS = 'yatra_pro_google_analytics_';
48
49 /**
50 * Specific cache keys
51 */
52 public const KEY_ITINERARY_ENTRY_WITH_RELATIONS = 'yatra_itinerary_entry_with_relations';
53 public const KEY_ITINERARY_BY_TRIP_ID = 'yatra_itinerary_by_trip_id';
54 public const KEY_DAY_EXISTS = 'yatra_day_exists_trip';
55 public const KEY_ACTIVITY_TYPE_LOOKUP = 'yatra_activity_type_lookup';
56 public const KEY_ACTIVITY_TYPE_FROM_ITEMS = 'yatra_activity_type_from_items';
57 public const KEY_ACTIVITY_ENTRY = 'yatra_activity_entry';
58 public const KEY_TRIPS_WITH_FILTERS = 'yatra_trips_with_filters';
59 public const KEY_AVAILABLE_ATTRIBUTES = 'yatra_available_attributes';
60 public const KEY_ITEMS_COUNT_BY_TYPE = 'yatra_items_count_by_type';
61 public const KEY_ACTIVITY_TRIP_COUNT = 'yatra_activity_trip_count';
62 public const KEY_ACTIVITY_TRIP_COUNT_DIRECT = 'yatra_activity_trip_count_direct';
63 public const KEY_TRIP_ATTRIBUTES = 'yatra_trip_attributes';
64
65 /**
66 * Pro plugin specific cache keys
67 */
68 public const KEY_PRO_PRICING_RULES = 'yatra_pro_pricing_rules';
69 public const KEY_PRO_PRICING_RULE = 'yatra_pro_pricing_rule';
70 public const KEY_PRO_ACTIVE_RULES = 'yatra_pro_active_pricing_rules';
71 public const KEY_PRO_ADDITIONAL_SERVICES = 'yatra_pro_additional_services';
72 public const KEY_PRO_EMAIL_TEMPLATES = 'yatra_pro_email_templates';
73 public const KEY_PRO_ABANDONED_BOOKINGS = 'yatra_pro_abandoned_bookings';
74 public const KEY_PRO_TRIP_CONSENTS = 'yatra_pro_trip_consents';
75 public const KEY_PRO_CALENDAR_EVENTS = 'yatra_pro_calendar_events';
76 public const KEY_PRO_MAILCHIMP_LISTS = 'yatra_pro_mailchimp_lists';
77
78 /**
79 * Cache durations (in seconds)
80 */
81 public const DURATION_TABLE_EXISTS = 3600; // 1 hour
82 public const DURATION_TRIP_DATA = 1800; // 30 minutes
83 public const DURATION_BOOKING_DATA = 900; // 15 minutes
84 public const DURATION_CUSTOMER_DATA = 1800; // 30 minutes
85 public const DURATION_ACTIVITY_DATA = 3600; // 1 hour
86 public const DURATION_DESTINATION_DATA = 3600; // 1 hour
87 public const DURATION_QUERY_RESULT = 600; // 10 minutes
88 public const DURATION_STATS = 300; // 5 minutes
89 public const DURATION_ITINERARY = 1800; // 30 minutes
90 public const DURATION_ATTRIBUTES = 3600; // 1 hour
91 public const DURATION_ITEM_TYPES = 1800; // 30 minutes
92 public const DURATION_LISTINGS = 1800; // 30 minutes
93 public const DURATION_COUNTS = 1800; // 30 minutes
94 public const DURATION_LOOKUPS = 1800; // 30 minutes
95 public const DURATION_SHORT = 600; // 10 minutes
96 public const DURATION_LONG = 3600; // 1 hour
97
98 /**
99 * Cache backend preference order
100 */
101 private static array $backends = ['transient', 'memory'];
102
103 /**
104 * In-memory cache for current request
105 */
106 private static array $memoryCache = [];
107
108 /**
109 * Available cache backends
110 */
111 private static ?array $availableBackends = null;
112
113 /**
114 * Check if caching is enabled (single source of truth for runtime).
115 *
116 * Requires: Advanced → Enable Cache on, WP_DEBUG off, and Yatra Advanced → Debug mode off.
117 */
118 public static function isEnabled(): bool
119 {
120 if (!(bool) get_option('yatra_cache_enabled', true)) {
121 return false;
122 }
123 if (defined('WP_DEBUG') && WP_DEBUG) {
124 return false;
125 }
126 // Align with Settings → Advanced → Debug mode (yatra_debug_mode).
127 if ((bool) get_option('yatra_debug_mode', false)) {
128 return false;
129 }
130
131 return true;
132 }
133
134 /**
135 * Get cache status information
136 */
137 public static function getStatus(): array
138 {
139 $optOn = (bool) get_option('yatra_cache_enabled', true);
140 $wpDebug = defined('WP_DEBUG') && WP_DEBUG;
141 $pluginDebug = (bool) get_option('yatra_debug_mode', false);
142 $effective = $optOn && !$wpDebug && !$pluginDebug;
143
144 $reason = null;
145 if (!$optOn) {
146 $reason = 'yatra_cache_enabled is false';
147 } elseif ($wpDebug) {
148 $reason = 'WP_DEBUG is enabled (wp-config.php)';
149 } elseif ($pluginDebug) {
150 $reason = 'Yatra Advanced Debug mode is enabled';
151 }
152
153 return [
154 'cache_enabled' => $optOn,
155 'debug_mode' => $wpDebug,
156 'yatra_debug_mode' => $pluginDebug,
157 'effective_status' => $effective,
158 'wp_debug' => defined('WP_DEBUG') ? WP_DEBUG : false,
159 'option_value' => $optOn,
160 'reason_disabled' => $reason,
161 ];
162 }
163
164 /**
165 * Enable cache for testing (temporary)
166 */
167 public static function enableForTesting(): bool
168 {
169 update_option('yatra_cache_enabled', true);
170 Logger::info('Cache enabled for testing', ['status' => true]);
171 return true;
172 }
173
174 /**
175 * Disable cache for testing (temporary)
176 */
177 public static function disableForTesting(): bool
178 {
179 update_option('yatra_cache_enabled', false);
180 Logger::info('Cache disabled for testing', ['status' => false]);
181 return true;
182 }
183
184 /**
185 * Get cached value
186 *
187 * @return mixed
188 */
189 public static function get(string $key)
190 {
191 // Check if caching is enabled
192 if (!self::isEnabled()) {
193 Logger::debug("Cache disabled, skipping cache lookup", ['key' => $key]);
194 return null;
195 }
196
197 // Try memory cache first (fastest)
198 if (isset(self::$memoryCache[$key])) {
199 Logger::debug("Cache hit (memory)", ['key' => $key]);
200 return self::$memoryCache[$key];
201 }
202
203 // Try external cache backends
204 foreach (self::getAvailableBackends() as $backend) {
205 $value = self::getFromBackend($backend, $key);
206 if ($value !== null) {
207 // Store in memory cache for subsequent requests
208 self::$memoryCache[$key] = $value;
209 Logger::debug("Cache hit ({$backend})", ['key' => $key]);
210 return $value;
211 }
212 }
213
214 Logger::debug("Cache miss", ['key' => $key]);
215 return null;
216 }
217
218 /**
219 * Set cached value
220 *
221 * @param mixed $value
222 */
223 public static function set(string $key, $value, int $duration = 3600): bool
224 {
225 // Check if caching is enabled
226 if (!self::isEnabled()) {
227 Logger::debug("Cache disabled, skipping cache set", ['key' => $key]);
228 return false;
229 }
230
231 // Always store in memory cache
232 self::$memoryCache[$key] = $value;
233
234 // Store in external cache backends
235 $success = false;
236 foreach (self::getAvailableBackends() as $backend) {
237 if (self::setToBackend($backend, $key, $value, $duration)) {
238 $success = true;
239 Logger::debug("Cache set ({$backend})", ['key' => $key, 'duration' => $duration]);
240 }
241 }
242
243 return $success;
244 }
245
246 /**
247 * Delete cached value
248 */
249 public static function delete(string $key): bool
250 {
251 // Check if caching is enabled
252 if (!self::isEnabled()) {
253 Logger::debug("Cache disabled, skipping cache delete", ['key' => $key]);
254 return false;
255 }
256
257 // Remove from memory cache
258 unset(self::$memoryCache[$key]);
259
260 // Remove from external cache backends
261 $success = false;
262 foreach (self::getAvailableBackends() as $backend) {
263 if (self::deleteFromBackend($backend, $key)) {
264 $success = true;
265 Logger::debug("Cache delete ({$backend})", ['key' => $key]);
266 }
267 }
268
269 return $success;
270 }
271
272 /**
273 * Clear cache by prefix
274 */
275 public static function clearByPrefix(string $prefix): bool
276 {
277 // Check if caching is enabled (but allow clearing even when disabled)
278 if (!self::isEnabled()) {
279 Logger::debug("Cache disabled, but clearing cache entries", ['prefix' => $prefix]);
280 }
281
282 // Clear memory cache
283 foreach (self::$memoryCache as $key => $value) {
284 if (strpos($key, $prefix) === 0) {
285 unset(self::$memoryCache[$key]);
286 }
287 }
288
289 // Clear external cache backends
290 $success = false;
291 foreach (self::getAvailableBackends() as $backend) {
292 if (self::clearPrefixFromBackend($backend, $prefix)) {
293 $success = true;
294 Logger::debug("Cache clear prefix ({$backend})", ['prefix' => $prefix]);
295 }
296 }
297
298 return $success;
299 }
300
301 /**
302 * Get or set cached value (cache-aside pattern)
303 *
304 * @return mixed
305 */
306 public static function remember(string $key, callable $callback, int $duration = 3600)
307 {
308 // Check if caching is enabled
309 if (!self::isEnabled()) {
310 Logger::debug("Cache disabled, executing callback directly", ['key' => $key]);
311 return $callback();
312 }
313
314 $value = self::get($key);
315
316 if ($value !== null) {
317 return $value;
318 }
319
320 // Generate value and cache it
321 $value = $callback();
322 self::set($key, $value, $duration);
323
324 Logger::debug("Cache remember", ['key' => $key, 'duration' => $duration]);
325 return $value;
326 }
327
328 /**
329 * Cache table existence check
330 */
331 public static function tableExists(string $tableName, callable $checkCallback): bool
332 {
333 $key = self::PREFIX_TABLE_EXISTS . $tableName;
334
335 return (bool) self::remember($key, $checkCallback, self::DURATION_TABLE_EXISTS);
336 }
337
338 /**
339 * Cache trip data
340 *
341 * @return mixed
342 */
343 public static function getTripData(int $tripId, callable $fetchCallback)
344 {
345 $key = self::PREFIX_TRIP_DATA . $tripId;
346
347 return self::remember($key, $fetchCallback, self::DURATION_TRIP_DATA);
348 }
349
350 /**
351 * Cache booking data
352 *
353 * @return mixed
354 */
355 public static function getBookingData(int $bookingId, callable $fetchCallback)
356 {
357 $key = self::PREFIX_BOOKING_DATA . $bookingId;
358
359 return self::remember($key, $fetchCallback, self::DURATION_BOOKING_DATA);
360 }
361
362 /**
363 * Cache query results
364 *
365 * @return mixed
366 */
367 public static function getQueryResult(string $queryHash, callable $queryCallback)
368 {
369 $key = self::PREFIX_QUERY_RESULT . $queryHash;
370
371 return self::remember($key, $queryCallback, self::DURATION_QUERY_RESULT);
372 }
373
374 /**
375 * Cache statistics
376 *
377 * @return mixed
378 */
379 public static function getStats(string $statsKey, callable $calculateCallback)
380 {
381 $key = self::PREFIX_STATS . $statsKey;
382
383 return self::remember($key, $calculateCallback, self::DURATION_STATS);
384 }
385
386 /**
387 * Invalidate related caches when data changes
388 */
389 public static function invalidateTrip(int $tripId): void
390 {
391 self::delete(self::PREFIX_TRIP_DATA . $tripId);
392 self::clearByPrefix(self::PREFIX_QUERY_RESULT);
393 self::clearByPrefix(self::PREFIX_STATS);
394
395 Logger::info("Cache invalidated for trip", ['trip_id' => $tripId]);
396 }
397
398 /**
399 * Invalidate booking-related caches
400 */
401 public static function invalidateBooking(int $bookingId): void
402 {
403 self::delete(self::PREFIX_BOOKING_DATA . $bookingId);
404 self::clearByPrefix(self::PREFIX_STATS);
405
406 Logger::info("Cache invalidated for booking", ['booking_id' => $bookingId]);
407 }
408
409 /**
410 * Trip / activity / destination listing caches (admin + frontend grids).
411 * Kept in sync with {@see \Yatra\Hooks\CacheHooks} expectations.
412 */
413 public static function invalidateListingCaches(): void
414 {
415 self::clearByPrefix(self::PREFIX_QUERY_RESULT);
416 self::clearByPrefix('trip_listing_');
417 self::clearByPrefix('activity_listing_');
418 self::clearByPrefix('destination_listing_');
419 }
420
421 /**
422 * Dashboard and report aggregate keys (beyond {@see PREFIX_STATS}).
423 */
424 public static function invalidateDashboardReportCaches(): void
425 {
426 self::clearByPrefix('dashboard_stats_');
427 self::clearByPrefix('report_stats_');
428 }
429
430 /**
431 * After a trip row write from {@see \Yatra\Repositories\TripRepository} (create / update / delete).
432 * Matches {@see CacheHooks} trip handlers so hooks and repository stay aligned.
433 */
434 public static function invalidateAfterTripWrite(string $operation, int $tripId): void
435 {
436 $operation = strtolower($operation);
437 if ($operation === 'create') {
438 // Listings + stats (matches legacy TripService::create follow-up clears).
439 self::invalidateListingCaches();
440 self::clearByPrefix(self::PREFIX_STATS);
441
442 return;
443 }
444 if ($tripId <= 0) {
445 return;
446 }
447 self::invalidateTrip($tripId);
448 self::invalidateListingCaches();
449 }
450
451 /**
452 * After bulk UPDATEs on the trips table (cron: scheduled publish/archive, seasonal) that bypass
453 * per-row {@see \Yatra\Repositories\TripRepository::afterWrite}.
454 */
455 public static function invalidateAfterBulkTripTableWrites(): void
456 {
457 self::invalidateListingCaches();
458 self::clearByPrefix(self::PREFIX_STATS);
459 self::invalidateDashboardReportCaches();
460 self::clearByPrefix(self::PREFIX_TRIP_DATA);
461 }
462
463 /**
464 * After booking mutations from {@see \Yatra\Repositories\BookingRepository}.
465 * Aligns with {@see CacheHooks::onBookingUpdated} and related handlers.
466 */
467 public static function invalidateAfterBookingWrite(int $bookingId): void
468 {
469 self::invalidateBooking($bookingId);
470 self::invalidateDashboardReportCaches();
471 }
472
473 /**
474 * Pro Dynamic Pricing rule list/detail caches (see {@see KEY_PRO_PRICING_RULES} / {@see KEY_PRO_PRICING_RULE}).
475 */
476 public static function invalidateProDynamicPricingCaches(?int $ruleId = null): void
477 {
478 self::clearByPrefix(self::KEY_PRO_PRICING_RULES);
479 self::clearByPrefix(self::KEY_PRO_ACTIVE_RULES);
480 if ($ruleId !== null && $ruleId > 0) {
481 self::delete(self::KEY_PRO_PRICING_RULE . '_' . $ruleId);
482 } else {
483 self::clearByPrefix(self::KEY_PRO_PRICING_RULE);
484 }
485 }
486
487 /**
488 * Get available cache backends
489 */
490 public static function getAvailableBackends(): array
491 {
492 if (self::$availableBackends === null) {
493 self::$availableBackends = [];
494
495 foreach (self::$backends as $backend) {
496 if (self::isBackendAvailable($backend)) {
497 self::$availableBackends[] = $backend;
498 }
499 }
500
501 // Always have memory as fallback
502 if (!in_array('memory', self::$availableBackends)) {
503 self::$availableBackends[] = 'memory';
504 }
505 }
506
507 return self::$availableBackends;
508 }
509
510 /**
511 * Check if cache backend is available
512 */
513 private static function isBackendAvailable(string $backend): bool
514 {
515 switch ($backend) {
516 case 'transient':
517 return function_exists('get_transient');
518 case 'memory':
519 return true;
520 default:
521 return false;
522 }
523 }
524
525 /**
526 * Get value from specific backend
527 *
528 * @return mixed
529 */
530 private static function getFromBackend(string $backend, string $key)
531 {
532 switch ($backend) {
533 case 'transient':
534 return self::getFromTransient($key);
535 case 'memory':
536 return self::$memoryCache[$key] ?? null;
537 default:
538 return null;
539 }
540 }
541
542 /**
543 * Set value to specific backend
544 *
545 * @param mixed $value
546 */
547 private static function setToBackend(string $backend, string $key, $value, int $duration): bool
548 {
549 switch ($backend) {
550 case 'transient':
551 return self::setToTransient($key, $value, $duration);
552 case 'memory':
553 return true; // Already handled above
554 default:
555 return false;
556 }
557 }
558
559 /**
560 * Delete from specific backend
561 */
562 private static function deleteFromBackend(string $backend, string $key): bool
563 {
564 switch ($backend) {
565 case 'transient':
566 return self::deleteFromTransient($key);
567 case 'memory':
568 return true; // Already handled above
569 default:
570 return false;
571 }
572 }
573
574 /**
575 * Clear prefix from specific backend
576 */
577 private static function clearPrefixFromBackend(string $backend, string $prefix): bool
578 {
579 switch ($backend) {
580 case 'transient':
581 return self::clearPrefixFromTransient($prefix);
582 case 'memory':
583 return true; // Already handled above
584 default:
585 return false;
586 }
587 }
588
589 /**
590 * WordPress Transient backend methods
591 *
592 * @return mixed
593 */
594 private static function getFromTransient(string $key)
595 {
596 if (!function_exists('get_transient')) return null;
597
598 $value = get_transient($key);
599 return $value !== false ? $value : null;
600 }
601
602 /**
603 * @param mixed $value
604 */
605 private static function setToTransient(string $key, $value, int $duration): bool
606 {
607 if (!function_exists('set_transient')) return false;
608
609 return set_transient($key, $value, $duration);
610 }
611
612 private static function deleteFromTransient(string $key): bool
613 {
614 if (!function_exists('delete_transient')) return false;
615
616 return delete_transient($key);
617 }
618
619 private static function clearPrefixFromTransient(string $prefix): bool
620 {
621 global $wpdb;
622
623 if (!$wpdb) return false;
624
625 // Delete transient entries
626 $wpdb->query($wpdb->prepare(
627 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
628 '_transient_' . $prefix . '%'
629 ));
630
631 // Delete timeout entries
632 $wpdb->query($wpdb->prepare(
633 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
634 '_transient_timeout_' . $prefix . '%'
635 ));
636
637 return true;
638 }
639
640
641
642 /**
643 * Get cache statistics
644 */
645 public static function getCacheStats(): array
646 {
647 $stats = [
648 'memory_cache_size' => count(self::$memoryCache),
649 'memory_usage' => 0,
650 'available_backends' => self::getAvailableBackends()
651 ];
652
653 // Calculate memory usage safely
654 if (function_exists('memory_get_usage')) {
655 $stats['memory_usage'] = memory_get_usage(true);
656 }
657
658 return $stats;
659 }
660
661 /**
662 * Clear all caches
663 */
664 public static function flush(): void
665 {
666 self::$memoryCache = [];
667
668 foreach (self::getAvailableBackends() as $backend) {
669 self::clearPrefixFromBackend($backend, 'yatra_');
670 }
671
672 Logger::info("All caches flushed");
673 }
674 }
675