PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.15 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 All 83 releases
yatra / app / Services / BaseService.php

BaseService.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Services/BaseService.php

704 lines 21.8 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\Services;
6
7 use Yatra\Utils\Logger;
8 use Yatra\Services\CacheService;
9 use Yatra\Contracts\ServiceInterface;
10 use Yatra\Utils\Cache;
11
12 /**
13 * Enhanced Base Service Class
14 *
15 * Provides enterprise-grade service layer with:
16 * - Integrated caching
17 * - Comprehensive logging
18 * - Transaction support
19 * - Event hooks
20 * - Performance monitoring
21 */
22 abstract class BaseService implements ServiceInterface
23 {
24 /**
25 * Get repository instance
26 */
27 abstract protected function getRepository();
28
29 /**
30 * Get all items with caching
31 */
32 public function getAll(array $filters = []): array
33 {
34 $startTime = microtime(true);
35
36 // Check if caching is enabled
37 if ($this->isCacheEnabled()) {
38 $cacheKey = $this->getCacheKey('all', $filters);
39
40 $result = $this->getCachedResult($cacheKey, function() use ($filters) {
41 return $this->getRepository()->all($filters);
42 });
43 } else {
44 // Bypass cache and get fresh data
45 Logger::debug("Cache disabled, getting fresh data", ['service' => static::class, 'filters' => $filters]);
46 $result = $this->getRepository()->all($filters);
47 }
48
49 $executionTime = microtime(true) - $startTime;
50 Logger::debug("Service getAll executed", [
51 'service' => static::class,
52 'filters' => $filters,
53 'execution_time' => $executionTime,
54 'result_count' => count($result)
55 ]);
56
57 return $result;
58 }
59
60 /**
61 * Get item by ID with caching
62 */
63 public function getById(int $id): ?\stdClass
64 {
65 if ($id <= 0) {
66 Logger::warning("Invalid ID provided to getById", ['service' => static::class, 'id' => $id]);
67 return null;
68 }
69
70 $startTime = microtime(true);
71
72 // Check if caching is enabled
73 if ($this->isCacheEnabled()) {
74 $cacheKey = $this->getCacheKey('entity', ['id' => $id]);
75
76 $result = $this->getCachedResult($cacheKey, function() use ($id) {
77 return $this->getRepository()->find($id);
78 });
79 } else {
80 // Bypass cache and get fresh data
81 Logger::debug("Cache disabled, getting fresh data", ['service' => static::class, 'id' => $id]);
82 $result = $this->getRepository()->find($id);
83 }
84
85 $executionTime = microtime(true) - $startTime;
86 Logger::debug("Service getById executed", [
87 'service' => static::class,
88 'id' => $id,
89 'execution_time' => $executionTime,
90 'found' => $result !== null,
91 'cache_enabled' => $this->isCacheEnabled()
92 ]);
93
94 return $result;
95 }
96
97 /**
98 * Create new item with enhanced error handling and logging
99 */
100 public function create(array $data): int
101 {
102 $startTime = microtime(true);
103
104 try {
105 Logger::info("Service create started", [
106 'service' => static::class,
107 'data_keys' => array_keys($data)
108 ]);
109
110 // Validate data
111 $this->validateCreate($data);
112
113 // Process data before creation
114 $data = $this->processBeforeCreate($data);
115
116 // Fire before create hook
117 $this->fireHook('before_create', $data);
118
119 // Create record
120 $id = $this->getRepository()->create($data);
121
122 // Process after creation
123 $this->processAfterCreate($id, $data);
124
125 // Fire after create hook
126 $this->fireHook('after_create', $id, $data);
127
128 // Clear related caches
129 $this->invalidateRelatedCaches($id, 'create');
130
131 $executionTime = microtime(true) - $startTime;
132 Logger::info("Service create completed", [
133 'service' => static::class,
134 'id' => $id,
135 'execution_time' => $executionTime
136 ]);
137
138 return $id;
139
140 } catch (\Exception $e) {
141 $executionTime = microtime(true) - $startTime;
142 Logger::error("Service create failed", [
143 'service' => static::class,
144 'data_keys' => array_keys($data),
145 'execution_time' => $executionTime,
146 'error' => $e->getMessage()
147 ]);
148 throw $e;
149 }
150 }
151
152 /**
153 * Update item with enhanced error handling and logging
154 */
155 public function update(int $id, array $data): bool
156 {
157 $startTime = microtime(true);
158
159 try {
160 if ($id <= 0) {
161 throw new \InvalidArgumentException('Invalid ID provided for update');
162 }
163
164 Logger::info("Service update started", [
165 'service' => static::class,
166 'id' => $id,
167 'data_keys' => array_keys($data)
168 ]);
169
170 // Validate data
171 $this->validateUpdate($id, $data);
172
173 // Process data before update
174 $data = $this->processBeforeUpdate($id, $data);
175
176 // Fire before update hook
177 $this->fireHook('before_update', $id, $data);
178
179 // Update record
180 $result = $this->getRepository()->update($id, $data);
181
182 if ($result) {
183 // Process after update
184 $this->processAfterUpdate($id, $data);
185
186 // Fire after update hook
187 $this->fireHook('after_update', $id, $data);
188
189 // Clear related caches
190 $this->invalidateRelatedCaches($id, 'update');
191 }
192
193 $executionTime = microtime(true) - $startTime;
194 Logger::info("Service update completed", [
195 'service' => static::class,
196 'id' => $id,
197 'success' => $result,
198 'execution_time' => $executionTime
199 ]);
200
201 return $result;
202
203 } catch (\Exception $e) {
204 $executionTime = microtime(true) - $startTime;
205 Logger::error("Service update failed", [
206 'service' => static::class,
207 'id' => $id,
208 'data_keys' => array_keys($data),
209 'execution_time' => $executionTime,
210 'error' => $e->getMessage()
211 ]);
212 throw $e;
213 }
214 }
215
216 /**
217 * Delete item with enhanced error handling and logging
218 */
219 public function delete(int $id): bool
220 {
221 $startTime = microtime(true);
222
223 try {
224 if ($id <= 0) {
225 throw new \InvalidArgumentException('Invalid ID provided for delete');
226 }
227
228 Logger::info("Service delete started", [
229 'service' => static::class,
230 'id' => $id
231 ]);
232
233 // Process before delete
234 $this->processBeforeDelete($id);
235
236 // Fire before delete hook
237 $this->fireHook('before_delete', $id);
238
239 // Delete record
240 $result = $this->getRepository()->delete($id);
241
242 if ($result) {
243 // Process after delete
244 $this->processAfterDelete($id);
245
246 // Fire after delete hook
247 $this->fireHook('after_delete', $id);
248
249 // Clear related caches
250 $this->invalidateRelatedCaches($id, 'delete');
251 }
252
253 $executionTime = microtime(true) - $startTime;
254 Logger::info("Service delete completed", [
255 'service' => static::class,
256 'id' => $id,
257 'success' => $result,
258 'execution_time' => $executionTime
259 ]);
260
261 return $result;
262
263 } catch (\Exception $e) {
264 $executionTime = microtime(true) - $startTime;
265 Logger::error("Service delete failed", [
266 'service' => static::class,
267 'id' => $id,
268 'execution_time' => $executionTime,
269 'error' => $e->getMessage()
270 ]);
271 throw $e;
272 }
273 }
274
275 /**
276 * Validate data for creation (implement ServiceInterface)
277 */
278 public function validateCreate(array $data): void
279 {
280 // Default implementation - override in child classes
281 $this->validate($data);
282 }
283
284 /**
285 * Validate data for update (implement ServiceInterface)
286 */
287 public function validateUpdate(int $id, array $data): void
288 {
289 // Default implementation - override in child classes
290 $this->validate($data, $id);
291 }
292
293 /**
294 * Legacy validate method (override in child classes)
295 */
296 protected function validate(array $data, ?int $id = null): void
297 {
298 // Override in child classes
299 }
300
301 /**
302 * Count items with caching
303 */
304 public function count(array $filters = []): int
305 {
306 if ($this->isCacheEnabled()) {
307 $cacheKey = $this->getCacheKey('count', $filters);
308
309 return (int) $this->getCachedResult($cacheKey, function() use ($filters) {
310 return $this->getRepository()->count($filters);
311 }, 300); // 5 minute cache for counts
312 } else {
313 // Bypass cache and get fresh data
314 Logger::debug("Cache disabled, getting fresh count", ['service' => static::class, 'filters' => $filters]);
315 return $this->getRepository()->count($filters);
316 }
317 }
318
319 /**
320 * Check if item exists
321 */
322 public function exists(int $id): bool
323 {
324 if ($id <= 0) {
325 return false;
326 }
327
328 if ($this->isCacheEnabled()) {
329 $cacheKey = $this->getCacheKey('exists', ['id' => $id]);
330
331 return (bool) $this->getCachedResult($cacheKey, function() use ($id) {
332 return $this->getRepository()->exists($id);
333 }, 600); // 10 minute cache for existence checks
334 } else {
335 // Bypass cache and get fresh data
336 Logger::debug("Cache disabled, checking existence directly", ['service' => static::class, 'id' => $id]);
337 return $this->getRepository()->exists($id);
338 }
339 }
340
341 /**
342 * Get paginated results with caching
343 */
344 public function paginate(int $page = 1, int $perPage = 10, array $filters = []): array
345 {
346 if ($this->isCacheEnabled()) {
347 $cacheKey = $this->getCacheKey('paginate', [
348 'page' => $page,
349 'per_page' => $perPage,
350 'filters' => $filters
351 ]);
352
353 return $this->getCachedResult($cacheKey, function() use ($page, $perPage, $filters) {
354 return $this->executePaginateQuery($page, $perPage, $filters);
355 });
356 } else {
357 // Bypass cache and get fresh data
358 Logger::debug("Cache disabled, getting fresh pagination", ['service' => static::class, 'page' => $page, 'per_page' => $perPage]);
359 return $this->executePaginateQuery($page, $perPage, $filters);
360 }
361 }
362
363 /**
364 * Execute pagination query
365 */
366 private function executePaginateQuery(int $page, int $perPage, array $filters): array
367 {
368 $repository = $this->getRepository();
369
370 // Handle different paginate method signatures
371 if (method_exists($repository, 'paginate')) {
372 // Try the new signature first: paginate(int $page, int $perPage, array $filters)
373 try {
374 return $repository->paginate($page, $perPage, $filters);
375 } catch (\ArgumentCountError $e) {
376 // Fall back to old signature: paginate(array $filters)
377 $filters['page'] = $page;
378 $filters['per_page'] = $perPage;
379 return $repository->paginate($filters);
380 }
381 } else {
382 // Fallback to all method with pagination parameters
383 $filters['limit'] = $perPage;
384 $filters['offset'] = ($page - 1) * $perPage;
385 return $repository->all($filters);
386 }
387 }
388
389 /**
390 * Check if caching is enabled (delegates to {@see Cache::isEnabled()} then backend probe).
391 */
392 protected function isCacheEnabled(): bool
393 {
394 try {
395 if (!\Yatra\Utils\Cache::isEnabled()) {
396 return false;
397 }
398
399 if (!$this->isCacheBackendAvailable()) {
400 Logger::warning("Cache enabled but backend not available, disabling cache");
401 return false;
402 }
403
404 return true;
405 } catch (\Exception $e) {
406 Logger::error("Failed to check cache enabled status", [
407 'error' => $e->getMessage(),
408 ]);
409 return false;
410 }
411 }
412
413 /**
414 * Check if cache backend is available
415 */
416 private function isCacheBackendAvailable(): bool
417 {
418 try {
419 // Test basic cache operation
420 $testKey = 'yatra_cache_test_' . time();
421 $testValue = 'test_value';
422
423 Cache::set($testKey, $testValue, 1);
424 $retrieved = Cache::get($testKey);
425
426 // Clean up test key
427 Cache::delete($testKey);
428
429 return $retrieved === $testValue;
430
431 } catch (\Exception $e) {
432 Logger::warning("Cache backend availability check failed", [
433 'error' => $e->getMessage()
434 ]);
435 return false;
436 }
437 }
438
439 /**
440 * Clear cache for entity
441 */
442 protected function clearEntityCache(int $id): void
443 {
444 if (!$this->isCacheEnabled()) {
445 return; // No cache to clear
446 }
447
448 $entityType = strtolower(str_replace(['\\', 'Service'], ['_', ''], static::class));
449
450 // The cache keys generated by getCacheKey() are prefixed with "service_{$entityType}_..."
451 // and use a params hash, so prefix deletion by "{$entityType}_..." would miss them.
452 // To avoid stale admin reads, clear the whole service keyspace and also delete the most
453 // common single-entity keys directly.
454 Cache::delete($this->getCacheKey('entity', ['id' => $id]));
455 Cache::delete($this->getCacheKey('exists', ['id' => $id]));
456
457 // Clear all caches for this service (covers all/all/count/paginate variants).
458 $this->clearCacheByPattern("service_{$entityType}_");
459
460 // Clear query result caches
461 $this->clearCacheByPattern(Cache::PREFIX_QUERY_RESULT);
462
463 Logger::info("Entity cache cleared", [
464 'service' => static::class,
465 'entity_id' => $id,
466 'entity_type' => $entityType
467 ]);
468 }
469
470 /**
471 * Clear all cache for this service
472 */
473 protected function clearAllServiceCache(): void
474 {
475 if (!$this->isCacheEnabled()) {
476 return; // No cache to clear
477 }
478
479 $entityType = strtolower(str_replace(['\\', 'Service'], ['_', ''], static::class));
480
481 // Clear all caches related to this service (see getCacheKey()).
482 $this->clearCacheByPattern("service_{$entityType}_");
483
484 Logger::info("All service cache cleared", [
485 'service' => static::class,
486 'entity_type' => $entityType
487 ]);
488 }
489
490 /**
491 * Clear cache by pattern
492 */
493 private function clearCacheByPattern(string $pattern): void
494 {
495 try {
496 Cache::clearByPrefix($pattern);
497 } catch (\Exception $e) {
498 Logger::warning("Failed to clear cache pattern", [
499 'pattern' => $pattern,
500 'error' => $e->getMessage()
501 ]);
502 }
503 }
504
505 /**
506 * Get cached result or execute callback with performance monitoring
507 *
508 * @return mixed
509 */
510 protected function getCachedResult(string $cacheKey, callable $callback, int $duration = 1800)
511 {
512 $startTime = microtime(true);
513
514 try {
515 if ($this->isCacheEnabled()) {
516 $result = Cache::remember($cacheKey, $callback, $duration);
517
518 // Record cache performance metrics
519 $this->recordCacheMetrics($cacheKey, $startTime, true);
520
521 return $result;
522 } else {
523 // Bypass cache and execute callback directly
524 $result = $callback();
525
526 // Record non-cache performance metrics
527 $this->recordCacheMetrics($cacheKey, $startTime, false);
528
529 return $result;
530 }
531
532 } catch (\Exception $e) {
533 // Fallback to direct execution on cache failure
534 Logger::warning("Cache operation failed, falling back to direct execution", [
535 'cache_key' => $cacheKey,
536 'error' => $e->getMessage()
537 ]);
538
539 $result = $callback();
540 $this->recordCacheMetrics($cacheKey, $startTime, false, $e);
541
542 return $result;
543 }
544 }
545
546 /**
547 * Record cache performance metrics
548 */
549 private function recordCacheMetrics(string $cacheKey, float $startTime, bool $cacheUsed, ?\Exception $error = null): void
550 {
551 $executionTime = microtime(true) - $startTime;
552
553 $metrics = [
554 'cache_key' => substr($cacheKey, 0, 50) . '...',
555 'cache_used' => $cacheUsed,
556 'execution_time' => round($executionTime * 1000, 2), // in milliseconds
557 'service' => static::class,
558 'timestamp' => time()
559 ];
560
561 if ($error) {
562 $metrics['error'] = $error->getMessage();
563 Logger::warning("Cache performance recorded with error", $metrics);
564 } else {
565 Logger::debug("Cache performance recorded", $metrics);
566 }
567
568 // Store metrics for analytics (optional - can be extended)
569 $this->storeCacheMetrics($metrics);
570 }
571
572 /**
573 * Store cache metrics for analytics
574 */
575 private function storeCacheMetrics(array $metrics): void
576 {
577 try {
578 // Store metrics in WordPress options for analytics
579 $existingMetrics = get_option('yatra_cache_metrics', []);
580 $existingMetrics[] = $metrics;
581
582 // Keep only last 1000 entries to prevent bloat
583 if (count($existingMetrics) > 1000) {
584 $existingMetrics = array_slice($existingMetrics, -1000);
585 }
586
587 update_option('yatra_cache_metrics', $existingMetrics, false); // autoload=false: metrics, read on demand
588
589 } catch (\Exception $e) {
590 // Don't let metrics storage failures break the main functionality
591 Logger::debug("Failed to store cache metrics", [
592 'error' => $e->getMessage()
593 ]);
594 }
595 }
596
597 /**
598 * Generate cache key for service operations
599 */
600 protected function getCacheKey(string $operation, array $params = []): string
601 {
602 $serviceClass = strtolower(str_replace(['\\', 'Service'], ['_', ''], static::class));
603 $paramsHash = md5(serialize($params));
604
605 return "service_{$serviceClass}_{$operation}_{$paramsHash}";
606 }
607
608 /**
609 * Fire WordPress hooks for service events
610 */
611 protected function fireHook(string $action, ...$args): void
612 {
613 $serviceClass = strtolower(str_replace(['\\', 'Service'], ['_', ''], static::class));
614 $hookName = "yatra_{$serviceClass}_{$action}";
615
616 do_action($hookName, ...$args);
617
618 Logger::debug("Service hook fired", [
619 'hook' => $hookName,
620 'args_count' => count($args)
621 ]);
622 }
623
624 /**
625 * Invalidate related caches
626 */
627 protected function invalidateRelatedCaches(int $id, string $operation): void
628 {
629 // Clear entity cache
630 $this->clearEntityCache($id);
631
632 // Clear all service cache for list operations
633 $this->clearAllServiceCache();
634
635 // Clear related entity caches based on operation type
636 $this->clearRelatedEntityCaches($id, $operation);
637
638 Logger::debug("Related caches invalidated", [
639 'service' => static::class,
640 'entity_id' => $id,
641 'operation' => $operation
642 ]);
643 }
644
645 /**
646 * Clear related entity caches
647 */
648 protected function clearRelatedEntityCaches(int $id, string $operation): void
649 {
650 // Override in child classes to clear specific related caches
651 // For example: TripService would clear destination, activity caches
652 // AttributeService would clear trip-related caches, etc.
653 }
654
655 /**
656 * Process data before create (override in child classes)
657 */
658 protected function processBeforeCreate(array $data): array
659 {
660 return $data;
661 }
662
663 /**
664 * Process after create (override in child classes)
665 */
666 protected function processAfterCreate(int $id, array $data): void
667 {
668 // Override in child classes
669 }
670
671 /**
672 * Process data before update (override in child classes)
673 */
674 protected function processBeforeUpdate(int $id, array $data): array
675 {
676 return $data;
677 }
678
679 /**
680 * Process after update (override in child classes)
681 */
682 protected function processAfterUpdate(int $id, array $data): void
683 {
684 // Override in child classes
685 }
686
687 /**
688 * Process before delete (override in child classes)
689 */
690 protected function processBeforeDelete(int $id): void
691 {
692 // Override in child classes
693 }
694
695 /**
696 * Process after delete (override in child classes)
697 */
698 protected function processAfterDelete(int $id): void
699 {
700 // Override in child classes
701 }
702 }
703
704