PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 trunk, at app/Shortcodes/DestinationShortcode.php

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