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

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

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