PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 / BlockDataService.php

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

594 lines 21.7 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\Helpers\TripListingFilterBuilder;
8 use Yatra\Repositories\TripRepository;
9 use Yatra\Shortcodes\ActivityShortcode;
10 use Yatra\Shortcodes\DestinationShortcode;
11 use Yatra\Shortcodes\TripCategoryShortcode;
12
13 /**
14 * Block Data Service
15 *
16 * Shared service for rendering blocks and shortcodes
17 * Provides reusable data fetching and template rendering logic
18 *
19 * @package Yatra\Services
20 * @since 3.0.0
21 */
22 class BlockDataService
23 {
24 /** Max grid columns for tour / activity / destination listing blocks (matches block editor RangeControl). */
25 private const LISTING_COLUMNS_MAX = 6;
26
27 /**
28 * Defaults aligned with each block's block.json (REST and ServerSideRender rely on full merge).
29 *
30 * @return array<string, mixed>
31 */
32 private static function defaultTourBlockAttributes(): array
33 {
34 return [
35 'order' => 'desc',
36 'featured' => false,
37 'featured_priority' => '',
38 'featuredPriority' => '',
39 'per_page' => 10,
40 'columns' => 3,
41 'title' => 'Our Trips',
42 'show_pagination' => true,
43 // Per-instance card layout. 'inherit' = use the site-wide setting.
44 'card_layout' => 'inherit',
45 'cardLayout' => '',
46 'destinationIds' => [],
47 'activityIds' => [],
48 'categoryIds' => [],
49 'difficultyIds' => [],
50 'destination' => '',
51 'activity' => '',
52 'category' => '',
53 'destination_ids' => '',
54 'activity_ids' => '',
55 'category_ids' => '',
56 'difficulty' => '',
57 'price_min' => '',
58 'price_max' => '',
59 'duration_min' => '',
60 'duration_max' => '',
61 'search' => '',
62 ];
63 }
64
65 /** Allowed values for the trip "Featured Priority" filter (matches admin TripForm). */
66 private const FEATURED_PRIORITY_VALUES = ['featured', 'new', 'limited'];
67
68 /**
69 * Trip listing for shortcodes, AJAX pagination, and programmatic use.
70 *
71 * @param array<string, mixed> $rawAtts
72 *
73 * @return array{trips: \Yatra\Models\Trip[], max_pages: int, current_page: int, total_found: int}
74 */
75 public static function getTripListingForShortcode(array $rawAtts): array
76 {
77 $atts = wp_parse_args(is_array($rawAtts) ? $rawAtts : [], self::defaultTourBlockAttributes());
78 self::normalizeTripShortcodeAttributes($atts);
79
80 return self::queryTripsForAtts($atts);
81 }
82
83 /**
84 * @return array<string, mixed>
85 */
86 private static function defaultActivityBlockAttributes(): array
87 {
88 return [
89 'order' => 'asc',
90 'columns' => 3,
91 'per_page' => 10,
92 'title' => 'Activity Listings',
93 'show_pagination' => true,
94 'activityIds' => [],
95 'activity' => '',
96 'activity_ids' => '',
97 'show_trip_count' => true,
98 'show_description' => true,
99 'show_image' => true,
100 'hide_empty' => false,
101 ];
102 }
103
104 /**
105 * @return array<string, mixed>
106 */
107 private static function defaultDestinationBlockAttributes(): array
108 {
109 return [
110 'order' => 'asc',
111 'columns' => 3,
112 'per_page' => 10,
113 'title' => 'Destination Showcase',
114 'show_pagination' => true,
115 'destinationIds' => [],
116 'destination' => '',
117 'destination_ids' => '',
118 'show_trip_count' => true,
119 'show_description' => true,
120 'show_image' => true,
121 'hide_empty' => false,
122 'featured_only' => false,
123 ];
124 }
125
126 /**
127 * @return array<string, mixed>
128 */
129 private static function defaultTripCategoryBlockAttributes(): array
130 {
131 return [
132 'order' => 'desc',
133 'columns' => 3,
134 'per_page' => 10,
135 'title' => 'Trip Categories',
136 'show_pagination' => true,
137 'category' => '',
138 'show_trip_count' => true,
139 'show_description' => true,
140 'show_image' => true,
141 'hide_empty' => false,
142 'featured_only' => false,
143 'categoryIds' => [],
144 'category_ids' => '',
145 ];
146 }
147
148 /**
149 * Coerce REST/block booleans and legacy string values.
150 *
151 * @param mixed $value
152 */
153 private static function coerceToBool($value, bool $default): bool
154 {
155 if ($value === null || $value === '') {
156 return $default;
157 }
158 if (is_bool($value)) {
159 return $value;
160 }
161 if (is_int($value) || is_float($value)) {
162 return (bool) $value;
163 }
164 $s = strtolower((string) $value);
165
166 return in_array($s, ['1', 'true', 'yes', 'on'], true);
167 }
168
169 /**
170 * Normalize trip/tour attributes in place (shortcode extras like category are preserved).
171 *
172 * @param array<string, mixed> $atts
173 */
174 private static function normalizeTripShortcodeAttributes(array &$atts): void
175 {
176 $order = strtolower((string) ($atts['order'] ?? 'desc'));
177 $atts['order'] = in_array($order, ['asc', 'desc'], true) ? $order : 'desc';
178
179 $atts['featured'] = self::coerceToBool($atts['featured'] ?? false, false) ? '1' : '0';
180
181 $perPage = (int) ($atts['per_page'] ?? 10);
182 if ($perPage === -1) {
183 $perPage = 10;
184 }
185 $atts['per_page'] = (string) max(1, min(100, $perPage));
186
187 $cols = (int) ($atts['columns'] ?? 3);
188 $atts['columns'] = (string) max(1, min(self::LISTING_COLUMNS_MAX, $cols));
189
190 $atts['title'] = sanitize_text_field((string) ($atts['title'] ?? 'Our Trips'));
191
192 $atts['show_pagination'] = self::coerceToBool($atts['show_pagination'] ?? true, true) ? 'yes' : 'no';
193
194 // Card layout override. Accept camelCase (Gutenberg block: cardLayout)
195 // and snake_case (shortcode: card_layout). Unknown/empty/"inherit" keeps
196 // 'inherit' so the template falls back to the site-wide Design setting.
197 $cardLayoutRaw = '';
198 if (isset($atts['cardLayout']) && is_string($atts['cardLayout']) && $atts['cardLayout'] !== '') {
199 $cardLayoutRaw = $atts['cardLayout'];
200 } elseif (isset($atts['card_layout']) && is_string($atts['card_layout']) && $atts['card_layout'] !== '') {
201 $cardLayoutRaw = $atts['card_layout'];
202 }
203 $cardLayoutRaw = strtolower(trim($cardLayoutRaw));
204 $atts['card_layout'] = in_array($cardLayoutRaw, ['standard', 'compact_mobile', 'compact_all'], true)
205 ? $cardLayoutRaw
206 : 'inherit';
207
208 // Featured Priority (matches admin form: featured | new | limited; "none"/empty = no filter).
209 // Accept both snake_case (shortcode) and camelCase (Gutenberg block attribute).
210 $featuredPriorityRaw = '';
211 if (isset($atts['featured_priority']) && is_string($atts['featured_priority']) && $atts['featured_priority'] !== '') {
212 $featuredPriorityRaw = $atts['featured_priority'];
213 } elseif (isset($atts['featuredPriority']) && is_string($atts['featuredPriority']) && $atts['featuredPriority'] !== '') {
214 $featuredPriorityRaw = $atts['featuredPriority'];
215 }
216 $featuredPriority = strtolower(trim((string) $featuredPriorityRaw));
217 if ($featuredPriority === 'none') {
218 $featuredPriority = '';
219 }
220 if ($featuredPriority !== '' && !in_array($featuredPriority, self::FEATURED_PRIORITY_VALUES, true)) {
221 $featuredPriority = '';
222 }
223 $atts['featured_priority'] = $featuredPriority;
224 }
225
226 /**
227 * Render trip/tour - Shared method for both blocks and shortcodes
228 *
229 * @param array $attributes Block or shortcode attributes
230 * @return string Rendered HTML
231 */
232 public static function renderTrip(array $attributes): string
233 {
234 try {
235 $atts = wp_parse_args(is_array($attributes) ? $attributes : [], self::defaultTourBlockAttributes());
236 self::normalizeTripShortcodeAttributes($atts);
237
238 // Get trips using Yatra's service
239 $trips_data = self::queryTripsForAtts($atts);
240
241 $per_page = max(1, (int) ($atts['per_page'] ?? 10));
242 $atts['per_page'] = $per_page;
243
244 // Prepare data for template
245 $data = [
246 'trips' => [
247 'trips' => $trips_data['trips'] ?? [],
248 'max_pages' => $trips_data['max_pages'] ?? 1,
249 'current_page' => $trips_data['current_page'] ?? 1,
250 'total_found' => $trips_data['total_found'] ?? 0,
251 'per_page' => $per_page
252 ],
253 'atts' => $atts,
254 'current_page' => $trips_data['current_page'] ?? 1,
255 'max_pages' => $trips_data['max_pages'] ?? 1,
256 'total_found' => $trips_data['total_found'] ?? 0,
257 'per_page' => $per_page
258 ];
259
260 // Enqueue assets
261 self::enqueueTripAssets();
262
263 // Load template
264 $result = self::loadTemplate('shortcodes/trip.php', $data);
265
266
267 return $result;
268 } catch (\Exception $e) {
269
270 return '<div class="yatra-error">Trip rendering failed</div>';
271 } catch (\Error $e) {
272
273 return '<div class="yatra-error">Trip rendering failed</div>';
274 }
275 }
276
277 /**
278 * Render trip/tour block - Backward compatibility method
279 *
280 * @param array $attributes Block attributes
281 * @return string Rendered HTML
282 */
283 public static function renderTripBlock(array $attributes): string
284 {
285 return self::renderTrip($attributes);
286 }
287
288 /**
289 * Enqueue trip assets
290 */
291 private static function enqueueTripAssets(): void
292 {
293 \Yatra\Providers\FrontendAssetsProvider::registerCoreFrontendStylesheets();
294 $cssPath = \YATRA_PLUGIN_PATH . 'assets/css/shortcodes/trip-shortcode.css';
295 $cssVer = is_readable($cssPath) ? \YATRA_VERSION . '.' . filemtime($cssPath) : \YATRA_VERSION;
296 wp_enqueue_style(
297 'yatra-trip-shortcode',
298 \YATRA_PLUGIN_URL . 'assets/css/shortcodes/trip-shortcode.css',
299 \Yatra\Providers\FrontendAssetsProvider::shortcodeStyleDependencies(),
300 $cssVer
301 );
302 wp_enqueue_script('yatra-trip-shortcode', \YATRA_PLUGIN_URL . 'assets/js/trip-shortcode.js', array('jquery'), \YATRA_VERSION, true);
303
304 // Localize script for AJAX
305 wp_localize_script('yatra-trip-shortcode', 'yatraTripShortcode', [
306 'ajaxurl' => admin_url('admin-ajax.php'),
307 'nonce' => wp_create_nonce('yatra_trip_shortcode_nonce')
308 ]);
309 }
310
311 /**
312 * Load template file
313 */
314 private static function loadTemplate(string $template_path, array $data = []): string
315 {
316 $full_path = \YATRA_PLUGIN_PATH . 'templates/' . $template_path;
317
318 if (!file_exists($full_path)) {
319 if (defined('WP_DEBUG') && WP_DEBUG) {
320 return sprintf(
321 '<div class="yatra-error">Template not found: %s</div>',
322 esc_html($full_path)
323 );
324 }
325 return '';
326 }
327
328 // Extract data to make variables available in template
329 if (!empty($data)) {
330 extract($data);
331 }
332
333 ob_start();
334 include $full_path;
335 return ob_get_clean();
336 }
337
338 /**
339 * @param array<string, mixed> $atts
340 *
341 * @return array{trips: \Yatra\Models\Trip[], max_pages: int, current_page: int, total_found: int}
342 */
343 private static function queryTripsForAtts(array $atts): array
344 {
345 try {
346 $tripRepository = new TripRepository();
347
348 $current_page = isset($atts['current_page'])
349 ? (int) $atts['current_page']
350 : (isset($_GET['trip_page']) ? (int) $_GET['trip_page'] : 1);
351 $current_page = max(1, $current_page);
352
353 $per_page = max(1, (int) ($atts['per_page'] ?? 10));
354
355 $filters = TripListingFilterBuilder::buildFindWithFiltersArray($atts);
356 $result = $tripRepository->findWithFilters($filters, $current_page, $per_page);
357
358 $trips = [];
359 foreach (($result['trips'] ?? []) as $tripData) {
360 $trip = \Yatra\Models\Trip::fromStdClass($tripData);
361 if (isset($tripData->booking_count)) {
362 $trip->bookings_count = (int) $tripData->booking_count;
363 }
364 if (isset($tripData->review_count)) {
365 $trip->reviews_count = (int) $tripData->review_count;
366 }
367 if (isset($tripData->average_rating)) {
368 $ar = (float) $tripData->average_rating;
369 $trip->average_rating = $ar;
370 $trip->avg_rating = $ar;
371 }
372 $trip->reviews = [];
373
374 $trips[] = $trip;
375 }
376
377 return [
378 'trips' => $trips,
379 'max_pages' => max(1, (int) ($result['pages'] ?? 1)),
380 'current_page' => max(1, (int) ($result['page'] ?? $current_page)),
381 'total_found' => (int) ($result['total'] ?? 0),
382 ];
383 } catch (\Exception $e) {
384 return [
385 'trips' => [],
386 'max_pages' => 1,
387 'current_page' => 1,
388 'total_found' => 0,
389 ];
390 }
391 }
392
393 /**
394 * Render activity block or shortcode
395 *
396 * @param array $attributes Block or shortcode attributes
397 * @return string Rendered HTML
398 */
399 public static function renderActivityBlock(array $attributes): string
400 {
401 // Create shortcode instance to reuse its logic
402 $shortcode = new ActivityShortcode();
403
404 $merged = wp_parse_args(is_array($attributes) ? $attributes : [], self::defaultActivityBlockAttributes());
405
406 // Use shortcode's render method
407 return $shortcode->render(self::mapActivityAttributes($merged));
408 }
409
410 /**
411 * Render destination block or shortcode
412 *
413 * @param array $attributes Block or shortcode attributes
414 * @return string Rendered HTML
415 */
416 public static function renderDestinationBlock(array $attributes): string
417 {
418 // Create shortcode instance to reuse its logic
419 $shortcode = new DestinationShortcode();
420
421 $merged = wp_parse_args(is_array($attributes) ? $attributes : [], self::defaultDestinationBlockAttributes());
422
423 // Use shortcode's render method
424 return $shortcode->render(self::mapDestinationAttributes($merged));
425 }
426
427 /**
428 * Render trip category block (same card UI as destinations).
429 *
430 * @param array<string, mixed> $attributes
431 */
432 public static function renderTripCategoryBlock(array $attributes): string
433 {
434 $shortcode = new TripCategoryShortcode();
435 $merged = wp_parse_args(is_array($attributes) ? $attributes : [], self::defaultTripCategoryBlockAttributes());
436
437 return $shortcode->render(self::mapTripCategoryAttributes($merged));
438 }
439
440 /**
441 * @param array<string, mixed> $attributes
442 * @param string ...$legacyCsvKeys
443 */
444 private static function classificationIdsCsvForShortcode(
445 array $attributes,
446 string $arrayKey,
447 string ...$legacyCsvKeys
448 ): string {
449 $ids = TripListingFilterBuilder::positiveIntIdsFromAtts($attributes, $arrayKey, ...$legacyCsvKeys);
450
451 return $ids === [] ? '' : implode(',', $ids);
452 }
453
454 /**
455 * Map activity block attributes to shortcode format (full set for shortcode_atts merge).
456 *
457 * @param array<string, mixed> $attributes Merged with defaults
458 * @return array<string, string>
459 */
460 private static function mapActivityAttributes(array $attributes): array
461 {
462 $order = strtolower((string) ($attributes['order'] ?? 'asc'));
463 $order = in_array($order, ['asc', 'desc'], true) ? $order : 'asc';
464
465 $perPage = (int) ($attributes['per_page'] ?? 10);
466 if ($perPage === -1) {
467 $perPage = 10;
468 }
469 $perPage = max(1, min(100, $perPage));
470
471 $cols = max(1, min(self::LISTING_COLUMNS_MAX, (int) ($attributes['columns'] ?? 3)));
472
473 $showPag = self::coerceToBool($attributes['show_pagination'] ?? true, true);
474 // Forward the same visibility / hide-empty toggles the
475 // trip-category block has exposed since v3.0 so all three
476 // taxonomy blocks share one user-facing surface. hide_empty
477 // is what actually skips activities with zero published
478 // trips (see ActivityShortcode::getActivities).
479 $showTripCount = self::coerceToBool($attributes['show_trip_count'] ?? true, true);
480 $showDescription = self::coerceToBool($attributes['show_description'] ?? true, true);
481 $showImage = self::coerceToBool($attributes['show_image'] ?? true, true);
482 $hideEmpty = self::coerceToBool($attributes['hide_empty'] ?? true, true);
483
484 return [
485 'order' => $order,
486 'per_page' => (string) $perPage,
487 'columns' => (string) $cols,
488 'title' => sanitize_text_field((string) ($attributes['title'] ?? 'Activity Listings')),
489 'show_pagination' => $showPag ? 'yes' : 'no',
490 'activity' => self::classificationIdsCsvForShortcode(
491 $attributes,
492 'activityIds',
493 'activity_ids',
494 'activity'
495 ),
496 'show_trip_count' => $showTripCount ? 'yes' : 'no',
497 'show_description' => $showDescription ? 'yes' : 'no',
498 'show_image' => $showImage ? 'yes' : 'no',
499 'hide_empty' => $hideEmpty ? 'yes' : 'no',
500 ];
501 }
502
503 /**
504 * @param array<string, mixed> $attributes Merged with defaults
505 * @return array<string, string>
506 */
507 private static function mapDestinationAttributes(array $attributes): array
508 {
509 $order = strtolower((string) ($attributes['order'] ?? 'asc'));
510 $order = in_array($order, ['asc', 'desc'], true) ? $order : 'asc';
511
512 $perPage = (int) ($attributes['per_page'] ?? 10);
513 if ($perPage === -1) {
514 $perPage = 10;
515 }
516 $perPage = max(1, min(100, $perPage));
517
518 $cols = max(1, min(self::LISTING_COLUMNS_MAX, (int) ($attributes['columns'] ?? 3)));
519
520 $showPag = self::coerceToBool($attributes['show_pagination'] ?? true, true);
521 // Forward the visibility + hide_empty + featured_only toggles
522 // so the destination block exposes the same controls the
523 // [yatra_destination] shortcode already accepts.
524 $showTripCount = self::coerceToBool($attributes['show_trip_count'] ?? true, true);
525 $showDescription = self::coerceToBool($attributes['show_description'] ?? true, true);
526 $showImage = self::coerceToBool($attributes['show_image'] ?? true, true);
527 $hideEmpty = self::coerceToBool($attributes['hide_empty'] ?? true, true);
528 $featuredOnly = self::coerceToBool($attributes['featured_only'] ?? false, false);
529
530 return [
531 'order' => $order,
532 'per_page' => (string) $perPage,
533 'columns' => (string) $cols,
534 'title' => sanitize_text_field((string) ($attributes['title'] ?? 'Destination Showcase')),
535 'show_pagination' => $showPag ? 'yes' : 'no',
536 'destination' => self::classificationIdsCsvForShortcode(
537 $attributes,
538 'destinationIds',
539 'destination_ids',
540 'destination'
541 ),
542 'show_trip_count' => $showTripCount ? 'yes' : 'no',
543 'show_description' => $showDescription ? 'yes' : 'no',
544 'show_image' => $showImage ? 'yes' : 'no',
545 'hide_empty' => $hideEmpty ? 'yes' : 'no',
546 'featured_only' => $featuredOnly ? 'yes' : 'no',
547 ];
548 }
549
550 /**
551 * @param array<string, mixed> $attributes
552 * @return array<string, string>
553 */
554 private static function mapTripCategoryAttributes(array $attributes): array
555 {
556 $order = strtolower((string) ($attributes['order'] ?? 'desc'));
557 $order = in_array($order, ['asc', 'desc'], true) ? $order : 'desc';
558
559 $perPage = (int) ($attributes['per_page'] ?? 10);
560 if ($perPage === -1) {
561 $perPage = 10;
562 }
563 $perPage = max(1, min(100, $perPage));
564
565 $cols = max(1, min(self::LISTING_COLUMNS_MAX, (int) ($attributes['columns'] ?? 3)));
566
567 $showPag = self::coerceToBool($attributes['show_pagination'] ?? true, true);
568 $showTripCount = self::coerceToBool($attributes['show_trip_count'] ?? true, true);
569 $showDescription = self::coerceToBool($attributes['show_description'] ?? true, true);
570 $showImage = self::coerceToBool($attributes['show_image'] ?? true, true);
571 $hideEmpty = self::coerceToBool($attributes['hide_empty'] ?? true, true);
572 $featuredOnly = self::coerceToBool($attributes['featured_only'] ?? false, false);
573
574 return [
575 'order' => $order,
576 'per_page' => (string) $perPage,
577 'columns' => (string) $cols,
578 'title' => sanitize_text_field((string) ($attributes['title'] ?? 'Trip Categories')),
579 'show_pagination' => $showPag ? 'yes' : 'no',
580 'category' => self::classificationIdsCsvForShortcode(
581 $attributes,
582 'categoryIds',
583 'category_ids',
584 'category'
585 ),
586 'show_trip_count' => $showTripCount ? 'yes' : 'no',
587 'show_description' => $showDescription ? 'yes' : 'no',
588 'show_image' => $showImage ? 'yes' : 'no',
589 'hide_empty' => $hideEmpty ? 'yes' : 'no',
590 'featured_only' => $featuredOnly ? 'yes' : 'no',
591 ];
592 }
593 }
594