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 / Services / BlockDataService.php

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

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