PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / Shortcodes / DestinationShortcode.php

DestinationShortcode.php in Yatra – Travel Booking & Tour Operator Software 3.0.3, at app/Shortcodes/DestinationShortcode.php

481 lines 17.5 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\Shortcodes;
6
7 use Yatra\Services\SettingsService;
8
9 /**
10 * Destination Shortcode
11 *
12 * Displays destination showcase with associated trips using trip-listing-card.php template
13 */
14 class DestinationShortcode extends BaseShortcode
15 {
16 public function __construct()
17 {
18 parent::__construct('yatra_destination', [
19 'order' => 'desc',
20 'per_page' => '10',
21 'columns' => '3',
22 'show_trip_count' => 'yes',
23 'show_description' => 'yes',
24 'show_image' => 'yes',
25 'show_pagination' => 'yes', // Default to show pagination like trip shortcode
26 'destination' => '', // Specific destination slug(s), comma separated
27 'hide_empty' => 'yes',
28 'featured_only' => 'no',
29 'title' => 'Destination Showcase'
30 ]);
31 }
32
33 /**
34 * Render the destination shortcode content
35 */
36 protected function renderContent(array $atts): string
37 {
38 $atts = shortcode_atts($this->default_attributes, $atts, $this->tag);
39
40 // Extract per_page from attributes (only per_page parameter)
41 $per_page = 10; // default
42 if (!empty($atts['per_page']) && is_numeric($atts['per_page'])) {
43 $per_page = (int) $atts['per_page'];
44 }
45 $atts['per_page'] = $per_page;
46
47 // Get destinations using Yatra's service
48 $destinations_data = $this->getDestinations($atts);
49
50 // Prepare data for template
51 $data = [
52 'destinations' => $destinations_data['destinations'] ?? [],
53 'atts' => $atts,
54 'current_page' => $destinations_data['current_page'] ?? 1,
55 'max_pages' => $destinations_data['max_pages'] ?? 1,
56 'total_found' => $destinations_data['total_found'] ?? 0,
57 'per_page' => $per_page
58 ];
59
60 $destinationCssPath = YATRA_PLUGIN_PATH . 'assets/css/shortcodes/destination-shortcode.css';
61 $destinationCssVer = is_readable($destinationCssPath) ? YATRA_VERSION . '.' . filemtime($destinationCssPath) : YATRA_VERSION;
62 wp_enqueue_style(
63 'yatra-destination-shortcode',
64 YATRA_PLUGIN_URL . 'assets/css/shortcodes/destination-shortcode.css',
65 \Yatra\Providers\FrontendAssetsProvider::shortcodeStyleDependencies(),
66 $destinationCssVer
67 );
68
69 // Enqueue shortcode-specific JavaScript
70 wp_enqueue_script(
71 'yatra-destination-shortcode',
72 YATRA_PLUGIN_URL . 'assets/js/destination-shortcode.js',
73 ['jquery'],
74 YATRA_VERSION,
75 true
76 );
77
78 // Pass data to JavaScript
79 wp_localize_script('yatra-destination-shortcode', 'yatraDestinationShortcode', [
80 'ajaxurl' => admin_url('admin-ajax.php'),
81 'nonce' => wp_create_nonce('yatra_destination_shortcode_nonce')
82 ]);
83
84 return $this->loadTemplate('shortcodes/destination.php', $data);
85 }
86
87 /**
88 * Get destinations using Yatra's service
89 */
90 public function getDestinations(array $atts): array
91 {
92 try {
93 $destinationService = new \Yatra\Services\DestinationService();
94
95 // Get current page from query string or attributes (for AJAX)
96 $current_page = isset($atts['current_page']) ? (int) $atts['current_page'] : (isset($_GET['destination_page']) ? (int) $_GET['destination_page'] : 1);
97 // Use per_page parameter only
98 $per_page = 10; // Default fallback
99 if (!empty($atts['per_page'])) {
100 $per_page = (int) $atts['per_page'];
101
102 } else {
103
104 }
105
106 // Validate per_page to prevent issues
107 $per_page = max(1, min($per_page, 100)); // Between 1 and 100 items
108 $offset = ($current_page - 1) * $per_page;
109
110 // Start with very basic arguments to ensure we get destinations
111 $args = [
112 'limit' => $per_page,
113 'offset' => $offset,
114 'order_by' => 'name',
115 'order' => $atts['order'] === 'asc' ? 'ASC' : 'DESC'
116 ];
117
118 // Filter by specific destinations if provided
119 if (!empty($atts['destination'])) {
120 $args['where']['slug'] = explode(',', $atts['destination']);
121 }
122
123 // Get total count for pagination
124 $count_args = $args;
125 unset($count_args['limit']);
126 unset($count_args['offset']);
127 $total_destinations = $destinationService->count($count_args);
128
129 // Try using the base repository method to bypass status filtering
130 $result = $destinationService->getAll($args);
131
132
133 $destinations = [];
134
135 foreach ($result as $destinationData) {
136 // Get real trip data for this destination using classification tables
137 global $wpdb;
138
139 $tripClassificationsTable = \Yatra\Database\Tables\TripClassificationsTable::getTableName();
140 $tripsTable = \Yatra\Database\Tables\TripsTable::getTableName();
141
142 // Get trip IDs for this destination
143 $trip_ids = $wpdb->get_col($wpdb->prepare(
144 "SELECT tc.trip_id
145 FROM {$tripClassificationsTable} tc
146 INNER JOIN {$tripsTable} t ON tc.trip_id = t.id
147 WHERE tc.classification_id = %d
148 AND tc.classification_type = 'destination'
149 AND t.status = 'publish'",
150 $destinationData->id
151 ));
152
153 $trip_count = count($trip_ids);
154
155 // Get actual trips
156 $trips = [];
157 if (!empty($trip_ids)) {
158 $placeholders = implode(',', array_fill(0, count($trip_ids), '%d'));
159 $trips = $wpdb->get_results($wpdb->prepare(
160 "SELECT * FROM {$tripsTable}
161 WHERE id IN ({$placeholders})
162 AND status = 'publish'
163 ORDER BY created_at DESC
164 LIMIT 6",
165 ...$trip_ids
166 ));
167 }
168
169 // Calculate pricing from real trips
170 $min_price = null;
171 $max_price = null;
172 $durations = [];
173 $group_sizes = [];
174 $best_seasons = [];
175
176 // Calculate rating from reviews table directly
177 $total_rating_sum = 0;
178 $total_review_count = 0;
179
180 if (!empty($trip_ids)) {
181 $reviewsTable = \Yatra\Database\Tables\ReviewsTable::getTableName();
182 $placeholders = implode(',', array_fill(0, count($trip_ids), '%d'));
183
184 $reviews = $wpdb->get_results($wpdb->prepare(
185 "SELECT rating, COUNT(*) as review_count
186 FROM {$reviewsTable}
187 WHERE trip_id IN ({$placeholders})
188 AND status = 'approved'",
189 ...$trip_ids
190 ));
191
192
193
194 foreach ($reviews as $review) {
195 $total_rating_sum += $review->rating * $review->review_count;
196 $total_review_count += $review->review_count;
197
198
199 }
200 }
201
202 // Calculate average rating only for trips that actually have reviews
203 // Trips with no reviews are excluded from the average (not treated as 0 rating)
204 $avg_rating = $total_review_count > 0 ? $total_rating_sum / $total_review_count : 0;
205
206 foreach ($trips as $trip) {
207 // Debug: Log all trip data to see what fields exist
208
209 // Get pricing via centralized TripPricingService
210 $effective = \Yatra\Services\TripPricingService::getEffectivePrice($trip);
211 if ($effective > 0) {
212 if ($min_price === null || $effective < $min_price) {
213 $min_price = $effective;
214 }
215 if ($max_price === null || $effective > $max_price) {
216 $max_price = $effective;
217 }
218 }
219
220 // Get duration
221 if (!empty($trip->duration)) {
222 $durations[] = $trip->duration;
223 }
224
225 // Get group size
226 if (!empty($trip->max_group_size)) {
227 $group_sizes[] = $trip->max_group_size;
228 }
229
230 // Get best season
231 if (!empty($trip->best_season)) {
232 $best_seasons[] = $trip->best_season;
233 }
234 }
235
236 // Calculate averages
237 $final_avg_rating = $avg_rating; // Already calculated correctly above
238 $avg_duration = !empty($durations) ? array_sum($durations) / count($durations) : 0;
239 $avg_group_size = !empty($group_sizes) ? round(array_sum($group_sizes) / count($group_sizes)) : 0;
240 $best_season = !empty($best_seasons) ? $this->getMostCommonSeason($best_seasons) : 'Summer';
241
242 if (defined('WP_DEBUG') && WP_DEBUG) {
243
244
245
246
247
248
249
250 }
251
252 $destinations[] = [
253 'term' => $destinationData,
254 'trips' => $trips,
255 'trip_count' => $trip_count,
256 'description' => $destinationData->description ?? '',
257 'image' => $this->getDestinationImage($destinationData, $trips),
258 'link' => $this->getDestinationLink($destinationData),
259 'country' => $destinationData->country ?? '',
260 'region' => $destinationData->region ?? '',
261 'min_price' => $min_price,
262 'max_price' => $max_price,
263 'avg_rating' => $final_avg_rating,
264 'rating_count' => $total_review_count,
265 'avg_duration' => $avg_duration,
266 'avg_group_size' => $avg_group_size,
267 'best_season' => $best_season
268 ];
269 }
270
271 // Filter out empty destinations if requested
272 if ($atts['hide_empty'] === 'yes') {
273 $destinations = array_filter($destinations, function($destination) {
274 return !empty($destination['term']->name) && !empty($destination['term']->slug);
275 });
276 }
277
278 // Filter to show only featured destinations if requested
279 if ($atts['featured_only'] === 'yes') {
280 $destinations = array_filter($destinations, function($destination) {
281 // Check if destination is marked as featured
282 return isset($destination['term']->featured) && $destination['term']->featured == 1;
283 });
284 }
285
286 // Calculate pagination data
287 $max_pages = $per_page > 0 ? ceil($total_destinations / $per_page) : 1;
288
289 return [
290 'destinations' => $destinations,
291 'current_page' => $current_page,
292 'max_pages' => $max_pages,
293 'total_found' => $total_destinations,
294 'per_page' => $per_page
295 ];
296
297 } catch (\Exception $e) {
298 if (defined('WP_DEBUG') && WP_DEBUG) {
299
300 }
301 return [];
302 }
303 }
304
305 /**
306 * Get the most common season from an array of seasons
307 */
308 private function getMostCommonSeason(array $seasons): string
309 {
310 if (empty($seasons)) {
311 return 'Summer';
312 }
313
314 $counts = array_count_values($seasons);
315 arsort($counts);
316 return array_key_first($counts);
317 }
318
319
320 /**
321 * Resolve image URL from destination `icon` (same shape as admin: type image|icon, value = attachment ID or URL).
322 */
323 private function getImageUrlFromDestinationIcon($icon): string
324 {
325 if ($icon === null || $icon === '') {
326 return '';
327 }
328
329 if (is_string($icon)) {
330 $decoded = json_decode($icon, true);
331 if (is_array($decoded)) {
332 $icon = $decoded;
333 } else {
334 $icon = maybe_unserialize($icon);
335 }
336 }
337
338 if (!is_array($icon)) {
339 return '';
340 }
341
342 $type = $icon['type'] ?? $icon[0] ?? '';
343 $value = $icon['value'] ?? $icon[1] ?? '';
344
345 if ($type !== 'image' || $value === '' || $value === null) {
346 return '';
347 }
348
349 if (is_numeric($value)) {
350 $url = wp_get_attachment_image_url((int) $value, 'large');
351
352 return $url ?: '';
353 }
354
355 if (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
356 return $value;
357 }
358
359 return '';
360 }
361
362 /**
363 * Decode metadata column to array (JSON or PHP serialized).
364 *
365 * @return array<string, mixed>
366 */
367 private function decodeDestinationMetadata($raw): array
368 {
369 if ($raw === null || $raw === '') {
370 return [];
371 }
372
373 if (is_array($raw)) {
374 return $raw;
375 }
376
377 if (!is_string($raw)) {
378 return [];
379 }
380
381 $decoded = json_decode($raw, true);
382 if (is_array($decoded)) {
383 return $decoded;
384 }
385
386 $unserialized = maybe_unserialize($raw);
387
388 return is_array($unserialized) ? $unserialized : [];
389 }
390
391 /**
392 * Get destination image URL for shortcode cards.
393 *
394 * @param object $destination Classification row from DB
395 * @param array<int, object> $trips Associated trips (newest first), used for featured_image fallback
396 */
397 private function getDestinationImage($destination, array $trips = []): string
398 {
399 // Check for destination image in metadata
400 if (isset($destination->image) && !empty($destination->image)) {
401 return $destination->image;
402 }
403
404 // Check for destination banner in metadata
405 if (isset($destination->banner) && !empty($destination->banner)) {
406 return $destination->banner;
407 }
408
409 // Check for destination thumbnail/featured image
410 if (isset($destination->thumbnail) && !empty($destination->thumbnail)) {
411 return is_numeric($destination->thumbnail) ? (string) wp_get_attachment_url((int) $destination->thumbnail) : $destination->thumbnail;
412 }
413
414 // Check for destination cover image
415 if (isset($destination->cover_image) && !empty($destination->cover_image)) {
416 return $destination->cover_image;
417 }
418
419 // Check for destination hero image
420 if (isset($destination->hero_image) && !empty($destination->hero_image)) {
421 return $destination->hero_image;
422 }
423
424 $fromIcon = $this->getImageUrlFromDestinationIcon($destination->icon ?? null);
425 if ($fromIcon !== '') {
426 return $fromIcon;
427 }
428
429 // Check for metadata with image (JSON or serialized)
430 if (isset($destination->metadata) && $destination->metadata !== '') {
431 $metadata = $this->decodeDestinationMetadata($destination->metadata);
432 if ($metadata !== []) {
433 $image_fields = ['image', 'thumbnail', 'banner', 'featured_image', 'cover_image', 'hero_image', 'image_id'];
434 foreach ($image_fields as $field) {
435 if (!empty($metadata[$field])) {
436 $val = $metadata[$field];
437
438 return is_numeric($val) ? (string) wp_get_attachment_url((int) $val) : (string) $val;
439 }
440 }
441 }
442 }
443
444 foreach ($trips as $trip) {
445 if (!empty($trip->featured_image) && is_numeric($trip->featured_image)) {
446 $url = wp_get_attachment_image_url((int) $trip->featured_image, 'large');
447 if ($url !== false && $url !== '') {
448 return $url;
449 }
450 }
451 }
452
453 // Fallback to placeholder
454 $fallback_url = YATRA_PLUGIN_URL . 'assets/images/placeholder.png';
455
456 if (defined('WP_DEBUG') && WP_DEBUG) {
457
458 }
459
460 return $fallback_url;
461 }
462
463 /**
464 * Get destination link
465 */
466 private function getDestinationLink($destination): string
467 {
468 if (isset($destination->slug)) {
469 // Use permalink helper so global base + plain permalinks are respected
470 if (function_exists('yatra_get_destination_permalink')) {
471 return yatra_get_destination_permalink($destination);
472 }
473
474 $base = SettingsService::getString('destination_base', 'destination');
475 return home_url('/' . $base . '/' . $destination->slug . '/');
476 }
477
478 return '#'; // Fallback
479 }
480 }
481