PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.16 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 All 84 releases
yatra / app / Services / TripListingPageContextFactory.php

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

402 lines 15.3 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 /**
8 * Builds view data for the trip listing template (no business queries — only request + listing payload shaping).
9 */
10 final class TripListingPageContextFactory
11 {
12 /**
13 * @param array<string, mixed> $tripListData Result of TripListingService::getFilteredTrips()
14 * @param array<string, mixed>|null $taxonomyContext Optional $GLOBALS['yatra_taxonomy_context']
15 * @return array<string, mixed>
16 */
17 public function buildForTripArchive(array $tripListData, string $requestUri, ?array $taxonomyContext = null, array $filterData = []): array
18 {
19 $activeFilters = $this->parseActiveFiltersFromRequest();
20
21 $taxonomy = $this->resolveTaxonomyPresentation($taxonomyContext);
22
23 $trips = [];
24 $total = 0;
25 $pages = 1;
26 $currentPage = 1;
27 $destOptions = [];
28 $actOptions = [];
29 $perPage = max(1, (int) ($tripListData['per_page'] ?? \yatra_get_posts_per_page()));
30
31 if (array_key_exists('trips', $tripListData)) {
32 $trips = $tripListData['trips'];
33 $total = (int) ($tripListData['total'] ?? count($trips));
34 $pages = max(1, (int) ($tripListData['pages'] ?? 1));
35 $currentPage = max(1, (int) ($tripListData['page'] ?? 1));
36 $perPage = max(1, (int) ($tripListData['per_page'] ?? $perPage));
37 $destOptions = $tripListData['destinations'] ?? [];
38 $actOptions = $tripListData['activities'] ?? [];
39
40 if (!empty($tripListData['filters']) && is_array($tripListData['filters'])) {
41 foreach (array_keys($activeFilters) as $k) {
42 if (array_key_exists($k, $tripListData['filters'])) {
43 $activeFilters[$k] = $tripListData['filters'][$k];
44 }
45 }
46 }
47
48 // Plain permalinks: trip archive is ?yatra_page={trip_base}&paged=N — must survive pagination/sort links.
49 $activeFilters['yatra_page'] = $this->getYatraPageRoutingParam($_GET);
50 } elseif (is_array($taxonomyContext) && !empty($taxonomyContext['trips'])) {
51 $trips = $taxonomyContext['trips'];
52 $total = count($trips);
53 }
54
55 if ($pages < $currentPage) {
56 $currentPage = $pages;
57 }
58
59 $listingTitle = $this->buildListingTitle($taxonomy);
60 $displayTotal = $total > 0 ? $total : count($trips);
61 $startItem = $displayTotal > 0 ? ($currentPage - 1) * $perPage + 1 : 0;
62 $endItem = $displayTotal > 0 ? min($currentPage * $perPage, $displayTotal) : 0;
63
64 $basePath = $this->normalizeListingBasePath($requestUri);
65
66 return [
67 'trip_list' => $tripListData,
68 'trips' => $trips,
69 'total' => $total,
70 'pages' => $pages,
71 'current_page' => $currentPage,
72 'destinations' => $destOptions,
73 'activities' => $actOptions,
74 'active_filters' => $activeFilters,
75 'filter_data' => $filterData,
76 'listing_title' => $listingTitle,
77 'taxonomy_type' => $taxonomy['type'],
78 'taxonomy_slug' => $taxonomy['slug'],
79 'taxonomy_name' => $taxonomy['name'],
80 'show_taxonomy_context' => $taxonomy['type'] !== '' && $taxonomy['slug'] !== '',
81 'results' => [
82 'display_total' => $displayTotal,
83 'start_item' => $startItem,
84 'end_item' => $endItem,
85 'per_page' => $perPage,
86 ],
87 'pagination' => $this->buildPagination($pages, $currentPage, $activeFilters, $basePath),
88 'sort_options' => $this->buildSortOptions($activeFilters, $basePath),
89 ];
90 }
91
92 /**
93 * @return array{type: string, slug: string, name: string}
94 */
95 private function resolveTaxonomyPresentation(?array $taxonomyContext): array
96 {
97 $out = ['type' => '', 'slug' => '', 'name' => ''];
98 if (!is_array($taxonomyContext) || empty($taxonomyContext['type']) || empty($taxonomyContext['entity'])) {
99 return $out;
100 }
101 $entity = $taxonomyContext['entity'];
102 $out['type'] = (string) $taxonomyContext['type'];
103 $out['slug'] = isset($entity->slug) ? (string) $entity->slug : '';
104 $out['name'] = isset($entity->name) ? (string) $entity->name : '';
105
106 return $out;
107 }
108
109 /**
110 * @param array{type: string, slug: string, name: string} $taxonomy
111 */
112 private function buildListingTitle(array $taxonomy): string
113 {
114 if ($taxonomy['type'] === '' || $taxonomy['name'] === '') {
115 return __('All Trips', 'yatra');
116 }
117 if ($taxonomy['type'] === 'destination') {
118 return sprintf(
119 /* translators: %s is destination name */
120 __('Trips to %s', 'yatra'),
121 $taxonomy['name']
122 );
123 }
124 if ($taxonomy['type'] === 'activity') {
125 return sprintf(
126 /* translators: %s is activity name */
127 __('Trips with %s', 'yatra'),
128 $taxonomy['name']
129 );
130 }
131
132 return __('All Trips', 'yatra');
133 }
134
135 /**
136 * @return array<string, mixed>
137 */
138 private function parseActiveFiltersFromRequest(): array
139 {
140 $get = $_GET;
141 $attrs = [];
142 if (isset($get['attributes']) && is_array($get['attributes'])) {
143 foreach ($get['attributes'] as $id => $attrData) {
144 if (is_array($attrData)) {
145 $attrs[$id] = array_map('sanitize_text_field', $attrData);
146 } else {
147 $attrs[$id] = sanitize_text_field((string) $attrData);
148 }
149 }
150 }
151
152 return [
153 'yatra_page' => $this->getYatraPageRoutingParam($get),
154 's' => isset($get['s']) ? sanitize_text_field(wp_unslash((string) $get['s'])) : '',
155 'category' => isset($get['category']) ? sanitize_text_field(wp_unslash((string) $get['category'])) : '',
156 'destination' => isset($get['destination']) ? sanitize_text_field(wp_unslash((string) $get['destination'])) : '',
157 'activity' => isset($get['activity']) ? sanitize_text_field(wp_unslash((string) $get['activity'])) : '',
158 'price_min' => !empty($get['price_min']) ? (int) $get['price_min'] : '',
159 'price_max' => !empty($get['price_max']) ? (int) $get['price_max'] : '',
160 'budget' => isset($get['budget']) ? sanitize_text_field(wp_unslash((string) $get['budget'])) : '',
161 'duration' => isset($get['duration']) ? sanitize_text_field(wp_unslash((string) $get['duration'])) : '',
162 'trip_type' => isset($get['trip_type']) ? sanitize_text_field(wp_unslash((string) $get['trip_type'])) : '',
163 'sort' => isset($get['sort']) ? sanitize_text_field(wp_unslash((string) $get['sort'])) : '',
164 'difficulty' => $this->parseGetArray($get, 'difficulty', 'intval'),
165 'rating' => $this->parseGetArray($get, 'rating', 'intval'),
166 'categories' => $this->parseGetArray($get, 'categories', 'intval'),
167 'destinations' => $this->parseGetArray($get, 'destinations', 'intval'),
168 'activities' => $this->parseGetArray($get, 'activities', 'intval'),
169 'accommodation' => $this->parseGetArray($get, 'accommodation', 'sanitize_text_field'),
170 'services' => $this->parseGetArray($get, 'services', 'sanitize_text_field'),
171 'offers' => $this->parseGetArray($get, 'offers', 'sanitize_text_field'),
172 'booking' => $this->parseGetArray($get, 'booking', 'sanitize_text_field'),
173 'age' => $this->parseGetArray($get, 'age', 'sanitize_text_field'),
174 'included_services' => $this->parseGetArray($get, 'included_services', 'sanitize_text_field'),
175 'special_offers' => $this->parseGetArray($get, 'special_offers', 'sanitize_text_field'),
176 'booking_options' => $this->parseGetArray($get, 'booking_options', 'sanitize_text_field'),
177 'age_suitability' => $this->parseGetArray($get, 'age_suitability', 'sanitize_text_field'),
178 'attributes' => $attrs,
179 ];
180 }
181
182 /**
183 * @param array<string, mixed> $get
184 * @param callable(string):mixed $sanitizeCallback
185 * @return list<mixed>
186 */
187 /**
188 * Plain routing base (e.g. trip_base "tour") for ?yatra_page= — required on every trip listing URL when using query-string routing.
189 *
190 * @param array<string, mixed> $get
191 */
192 private function getYatraPageRoutingParam(array $get): string
193 {
194 if (isset($get['yatra_page']) && is_string($get['yatra_page'])) {
195 $v = trim(wp_unslash($get['yatra_page']));
196 if ($v !== '') {
197 return preg_replace('/[^a-zA-Z0-9_-]/', '', $v) ?? '';
198 }
199 }
200
201 $qv = get_query_var('yatra_page');
202 if ($qv !== null && $qv !== '' && is_scalar($qv)) {
203 $v = trim((string) $qv);
204 if ($v !== '') {
205 return preg_replace('/[^a-zA-Z0-9_-]/', '', $v) ?? '';
206 }
207 }
208
209 return '';
210 }
211
212 private function parseGetArray(array $get, string $key, callable $sanitizeCallback): array
213 {
214 if (!isset($get[$key])) {
215 return [];
216 }
217 $value = $get[$key];
218 if (is_array($value)) {
219 return array_values(array_filter(array_map($sanitizeCallback, $value), static fn ($v) => $v !== '' && $v !== null));
220 }
221 if (is_string($value) && $value !== '') {
222 $parts = array_filter(explode(',', $value));
223
224 return array_values(array_filter(array_map($sanitizeCallback, $parts), static fn ($v) => $v !== '' && $v !== null));
225 }
226
227 return [];
228 }
229
230 /**
231 * @param array<string, mixed> $activeFilters
232 * @return list<array{type: string, page?: int, url?: string, is_current?: bool}>
233 */
234 private function buildPagination(int $totalPages, int $currentPage, array $activeFilters, string $basePath): array
235 {
236 $totalPages = max(1, $totalPages);
237 $currentPage = max(1, min($currentPage, $totalPages));
238 $range = 2;
239 $items = [];
240
241 for ($p = 1; $p <= $totalPages; $p++) {
242 if ($p === 1 || $p === $totalPages || ($p >= $currentPage - $range && $p <= $currentPage + $range)) {
243 $items[] = [
244 'type' => 'page',
245 'page' => $p,
246 'url' => $this->buildFilteredUrl($basePath, $activeFilters, $p),
247 'is_current' => $p === $currentPage,
248 ];
249 } elseif ($p === $currentPage - $range - 1 || $p === $currentPage + $range + 1) {
250 $items[] = ['type' => 'ellipsis'];
251 }
252 }
253
254 return [
255 'total_pages' => $totalPages,
256 'current_page' => $currentPage,
257 'prev_url' => $currentPage > 1
258 ? $this->buildFilteredUrl($basePath, $activeFilters, $currentPage - 1)
259 : null,
260 'next_url' => $currentPage < $totalPages
261 ? $this->buildFilteredUrl($basePath, $activeFilters, $currentPage + 1)
262 : null,
263 'items' => $items,
264 ];
265 }
266
267 /**
268 * @param array<string, mixed> $activeFilters
269 * @return list<array{value: string, label: string, url: string, selected: bool}>
270 */
271 private function buildSortOptions(array $activeFilters, string $basePath): array
272 {
273 $definitions = [
274 '' => __('Recommended', 'yatra'),
275 'most_popular' => __('Most Popular', 'yatra'),
276 'price_low' => __('Price: Low to High', 'yatra'),
277 'price_high' => __('Price: High to Low', 'yatra'),
278 'rating_high' => __('Rating: Highest', 'yatra'),
279 'duration_short' => __('Duration: Shortest', 'yatra'),
280 'duration_long' => __('Duration: Longest', 'yatra'),
281 ];
282
283 $current = (string) ($activeFilters['sort'] ?? '');
284 $out = [];
285
286 foreach ($definitions as $value => $label) {
287 $q = $this->activeFiltersToQuery($activeFilters, ['sort']);
288 if ($value !== '') {
289 $q['sort'] = $value;
290 }
291 $out[] = [
292 'value' => $value,
293 'label' => $label,
294 'url' => $this->buildUrl($basePath, $q),
295 'selected' => $value === $current,
296 ];
297 }
298
299 return $out;
300 }
301
302 private function normalizeListingBasePath(string $requestUri): string
303 {
304 $path = strtok($requestUri, '?') ?: '';
305 $path = rtrim((string) $path, '/');
306 $path = preg_replace('#/page/[0-9]+#', '', $path) ?? $path;
307
308 return $path === '' ? '/' : $path;
309 }
310
311 /**
312 * @param array<string, mixed> $activeFilters
313 * @param list<string> $excludeKeys
314 * @return array<string, mixed>
315 */
316 private function activeFiltersToQuery(array $activeFilters, array $excludeKeys = []): array
317 {
318 $query = [];
319 foreach ($activeFilters as $k => $v) {
320 if (in_array($k, $excludeKeys, true)) {
321 continue;
322 }
323 if ($v === '' || $v === null || $v === []) {
324 continue;
325 }
326 if ($k === 'attributes' && is_array($v)) {
327 $clean = [];
328 foreach ($v as $attrId => $attrVal) {
329 if ($attrVal === '' || $attrVal === null) {
330 continue;
331 }
332 if (is_array($attrVal)) {
333 $inner = [];
334 foreach ($attrVal as $ik => $iv) {
335 if ($iv === '' || $iv === null) {
336 continue;
337 }
338 $inner[$ik] = $iv;
339 }
340 if ($inner !== []) {
341 $clean[$attrId] = $inner;
342 }
343 } else {
344 $clean[$attrId] = $attrVal;
345 }
346 }
347 if ($clean !== []) {
348 $query['attributes'] = $clean;
349 }
350
351 continue;
352 }
353 if (is_array($v)) {
354 $list = [];
355 foreach ($v as $item) {
356 if ($item !== '' && $item !== null) {
357 $list[] = $item;
358 }
359 }
360 if ($list !== []) {
361 $query[$k] = $list;
362 }
363 } else {
364 $query[$k] = $v;
365 }
366 }
367
368 return $query;
369 }
370
371 /**
372 * @param array<string, mixed> $activeFilters
373 */
374 private function buildFilteredUrl(string $basePath, array $activeFilters, int $page): string
375 {
376 $q = $this->activeFiltersToQuery($activeFilters, []);
377 if ($page > 1) {
378 $q['paged'] = $page;
379 }
380
381 return $this->buildUrl($basePath, $q);
382 }
383
384 /**
385 * @param array<string, mixed> $query
386 */
387 private function buildUrl(string $basePath, array $query): string
388 {
389 $path = trim((string) $basePath, '/');
390 $base = $path === ''
391 ? \home_url('/')
392 : \trailingslashit(\home_url('/' . $path . '/'));
393
394 $qs = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
395 if ($qs === '') {
396 return \esc_url($base);
397 }
398
399 return \esc_url($base . '?' . $qs);
400 }
401 }
402