| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Yatra\Core\Handlers; |
| 6 |
|
| 7 |
use Yatra\Database\Tables\ClassificationsTable; |
| 8 |
use Yatra\Repositories\TripRepository; |
| 9 |
|
| 10 |
/** |
| 11 |
* Taxonomy Page Handler |
| 12 |
* |
| 13 |
* Handles single taxonomy page requests (destination, activity, category) |
| 14 |
*/ |
| 15 |
class TaxonomyPageHandler extends BasePageHandler |
| 16 |
{ |
| 17 |
/** |
| 18 |
* Handle taxonomy page request |
| 19 |
* |
| 20 |
* @param array $route_data Route data from RouteMatcher |
| 21 |
* @return bool True if handled successfully |
| 22 |
*/ |
| 23 |
public function handle(array $route_data): bool |
| 24 |
{ |
| 25 |
$taxonomy_type = $route_data['taxonomy_type']; |
| 26 |
$slug = $route_data['slug']; |
| 27 |
$base = $route_data['base']; |
| 28 |
|
| 29 |
$taxonomy_data = $this->getTaxonomyData($taxonomy_type, $slug); |
| 30 |
|
| 31 |
if (!$taxonomy_data) { |
| 32 |
$this->set404(); |
| 33 |
return false; |
| 34 |
} |
| 35 |
|
| 36 |
$tripRepository = new TripRepository(); |
| 37 |
$filter_key = $this->getFilterKey($taxonomy_type); |
| 38 |
|
| 39 |
$pageNum = !empty($route_data['paged']) ? max(1, (int) $route_data['paged']) : \yatra_get_archive_listing_paged(); |
| 40 |
$perPage = \yatra_get_posts_per_page(); |
| 41 |
|
| 42 |
$filters = [$filter_key => $slug]; |
| 43 |
// AND with complementary slug filters from the query string (same semantics as /trip/?destination=&activity=). |
| 44 |
if ($taxonomy_type === 'destination' && !empty($_GET['activity']) && is_string($_GET['activity'])) { |
| 45 |
$a = sanitize_text_field(wp_unslash($_GET['activity'])); |
| 46 |
if ($a !== '') { |
| 47 |
$filters['activity'] = $a; |
| 48 |
} |
| 49 |
} |
| 50 |
if ($taxonomy_type === 'activity' && !empty($_GET['destination']) && is_string($_GET['destination'])) { |
| 51 |
$d = sanitize_text_field(wp_unslash($_GET['destination'])); |
| 52 |
if ($d !== '') { |
| 53 |
$filters['destination'] = $d; |
| 54 |
} |
| 55 |
} |
| 56 |
$sort = isset($_GET['sort']) ? sanitize_text_field(wp_unslash((string) $_GET['sort'])) : ''; |
| 57 |
$allowedSorts = ['most_popular', 'price_low', 'price_high', 'rating_high', 'duration_short', 'duration_long']; |
| 58 |
if ($sort !== '' && in_array($sort, $allowedSorts, true)) { |
| 59 |
$filters['sort'] = $sort; |
| 60 |
} |
| 61 |
|
| 62 |
$trips_data = $tripRepository->findWithFilters($filters, $pageNum, $perPage); |
| 63 |
|
| 64 |
$taxonomy_data->trips_total = (int) ($trips_data['total'] ?? 0); |
| 65 |
$taxonomy_data->trips_pages = (int) ($trips_data['pages'] ?? 1); |
| 66 |
$taxonomy_data->trips_per_page = (int) ($trips_data['per_page'] ?? $perPage); |
| 67 |
$taxonomy_data->trips_current_page = (int) ($trips_data['page'] ?? $pageNum); |
| 68 |
$taxonomy_data->trips = $trips_data['trips'] ?? []; |
| 69 |
|
| 70 |
$tripsWithReviews = []; |
| 71 |
foreach ($taxonomy_data->trips as $trip) { |
| 72 |
$trip->reviews = $this->getReviewsForTrip((int) $trip->id); |
| 73 |
$trip->average_rating = $this->calculateAverageRating($trip->reviews); |
| 74 |
$trip->review_count = count($trip->reviews); |
| 75 |
|
| 76 |
$tripsWithReviews[] = $trip; |
| 77 |
} |
| 78 |
|
| 79 |
$taxonomy_data->trips = $tripsWithReviews; |
| 80 |
|
| 81 |
// Configure $wp_query + virtual WP_Post so FSE block themes resolve an archive |
| 82 |
// template (not 404.html) for taxonomy pages. |
| 83 |
$this->setupPageEnvironment('archive', [ |
| 84 |
'title' => (string) ($taxonomy_data->name ?? $taxonomy_data->title ?? $slug), |
| 85 |
'object_id' => (int) ($taxonomy_data->id ?? 0), |
| 86 |
'post_type' => 'page', |
| 87 |
'post_name' => $slug, |
| 88 |
]); |
| 89 |
|
| 90 |
$this->setGlobal('yatra_taxonomy_data', $taxonomy_data); |
| 91 |
|
| 92 |
$this->setQueryVars([ |
| 93 |
'yatra_taxonomy_type' => $taxonomy_data->type, |
| 94 |
'yatra_taxonomy_slug' => $taxonomy_data->slug, |
| 95 |
'yatra_taxonomy' => $taxonomy_data, |
| 96 |
'yatra_page' => $base, |
| 97 |
]); |
| 98 |
|
| 99 |
return $this->selectTemplate('single-taxonomy', null, 'taxonomy-' . $taxonomy_type); |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Get taxonomy data by type and slug |
| 104 |
* |
| 105 |
* @param string $type Taxonomy type |
| 106 |
* @param string $slug Taxonomy slug |
| 107 |
* @return object|null Taxonomy data or null if not found |
| 108 |
*/ |
| 109 |
private function getTaxonomyData(string $type, string $slug): ?object |
| 110 |
{ |
| 111 |
global $wpdb; |
| 112 |
|
| 113 |
$table = ClassificationsTable::getTableName(); |
| 114 |
|
| 115 |
$sql = $wpdb->prepare( |
| 116 |
"SELECT * FROM {$table} WHERE type = %s AND slug = %s AND status = 'publish' LIMIT 1", |
| 117 |
$type, |
| 118 |
$slug |
| 119 |
); |
| 120 |
|
| 121 |
$data = $wpdb->get_row($sql); |
| 122 |
|
| 123 |
if ($data) { |
| 124 |
if (isset($data->metadata) && is_string($data->metadata)) { |
| 125 |
$data->metadata = maybe_unserialize($data->metadata); |
| 126 |
} |
| 127 |
|
| 128 |
if (isset($data->icon) && is_string($data->icon)) { |
| 129 |
$data->icon = maybe_unserialize($data->icon); |
| 130 |
} |
| 131 |
|
| 132 |
if (isset($data->icon) && is_numeric($data->icon)) { |
| 133 |
$data->icon = wp_get_attachment_url($data->icon); |
| 134 |
} |
| 135 |
} |
| 136 |
|
| 137 |
return $data ?: null; |
| 138 |
} |
| 139 |
|
| 140 |
/** |
| 141 |
* Get filter key for taxonomy type |
| 142 |
* |
| 143 |
* @param string $taxonomy_type Taxonomy type |
| 144 |
* @return string Filter key for TripRepository |
| 145 |
*/ |
| 146 |
private function getFilterKey(string $taxonomy_type): string |
| 147 |
{ |
| 148 |
switch ($taxonomy_type) { |
| 149 |
case 'category': |
| 150 |
return 'trip_category'; |
| 151 |
case 'activity': |
| 152 |
return 'activity'; |
| 153 |
case 'destination': |
| 154 |
return 'destination'; |
| 155 |
case 'difficulty': |
| 156 |
return 'difficulty'; |
| 157 |
default: |
| 158 |
return $taxonomy_type; |
| 159 |
} |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Get reviews for a specific trip (same as SingleTripController) |
| 164 |
* |
| 165 |
* @param int $trip_id Trip ID |
| 166 |
* @return array Reviews |
| 167 |
*/ |
| 168 |
private function getReviewsForTrip(int $trip_id): array |
| 169 |
{ |
| 170 |
global $wpdb; |
| 171 |
|
| 172 |
$reviewsTable = \Yatra\Database\Tables\ReviewsTable::getTableName(); |
| 173 |
|
| 174 |
$table_exists = $wpdb->get_var( |
| 175 |
$wpdb->prepare( |
| 176 |
'SHOW TABLES LIKE %s', |
| 177 |
$reviewsTable |
| 178 |
) |
| 179 |
); |
| 180 |
|
| 181 |
if (!$table_exists) { |
| 182 |
return []; |
| 183 |
} |
| 184 |
|
| 185 |
$sql = $wpdb->prepare( |
| 186 |
"SELECT * FROM {$reviewsTable} |
| 187 |
WHERE trip_id = %d |
| 188 |
AND status = 'approved' |
| 189 |
ORDER BY created_at DESC |
| 190 |
LIMIT 10", |
| 191 |
$trip_id |
| 192 |
); |
| 193 |
|
| 194 |
$reviews = $wpdb->get_results($sql); |
| 195 |
|
| 196 |
return $reviews ?: []; |
| 197 |
} |
| 198 |
|
| 199 |
/** |
| 200 |
* Calculate average rating from reviews (same as SingleTripController) |
| 201 |
* |
| 202 |
* @param array $reviews Reviews array |
| 203 |
* @return float Average rating |
| 204 |
*/ |
| 205 |
private function calculateAverageRating(array $reviews): float |
| 206 |
{ |
| 207 |
if (empty($reviews)) { |
| 208 |
return 0.0; |
| 209 |
} |
| 210 |
|
| 211 |
$total = 0; |
| 212 |
foreach ($reviews as $review) { |
| 213 |
$total += (float) ($review->rating ?? 0); |
| 214 |
} |
| 215 |
|
| 216 |
return round($total / count($reviews), 1); |
| 217 |
} |
| 218 |
} |
| 219 |
|