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

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

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