PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
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 / Sitemap / SitemapService.php

SitemapService.php in Yatra – Travel Booking & Tour Operator Software 3.0.14.2, at app/Sitemap/SitemapService.php

286 lines 8.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\Sitemap;
6
7 use Yatra\Database\Tables\ClassificationsTable;
8 use Yatra\Database\Tables\TripsTable;
9
10 /**
11 * Single source of truth for every public Yatra URL that belongs in a sitemap.
12 *
13 * Yatra stores trips and classifications (destinations / activities /
14 * categories) in custom tables, not as WordPress posts, so no sitemap
15 * generator (WP core, Yoast, Rank Math, AIOSEO) can discover them on its own.
16 * This service enumerates every such URL with its last-modified date.
17 *
18 * Everything is exposed as ONE consolidated list (getAllEntries) so Yatra
19 * publishes a single, clean /yatra-sitemap.xml. Per-type access is kept
20 * internal/available but the public sitemap is one file.
21 *
22 * Each entry is: ['loc' => absolute URL, 'lastmod' => W3C datetime|''].
23 */
24 class SitemapService
25 {
26 public const TYPE_ARCHIVE = 'archive';
27 public const TYPE_TRIP = 'trip';
28 public const TYPE_DESTINATION = 'destination';
29 public const TYPE_ACTIVITY = 'activity';
30 public const TYPE_CATEGORY = 'category';
31
32 /** Order entries appear in the consolidated sitemap. */
33 private const TYPES = [
34 self::TYPE_ARCHIVE,
35 self::TYPE_TRIP,
36 self::TYPE_DESTINATION,
37 self::TYPE_ACTIVITY,
38 self::TYPE_CATEGORY,
39 ];
40
41 private const CACHE_KEY = 'yatra_sitemap_entries_all';
42 private const CACHE_TTL = HOUR_IN_SECONDS;
43
44 /** sitemaps.org caps a single sitemap file at 50,000 URLs. */
45 private const MAX_URLS = 50000;
46
47 /**
48 * Every public Yatra URL, merged into one de-duplicated, cached list.
49 *
50 * @return array<int, array{loc: string, lastmod: string}>
51 */
52 public function getAllEntries(): array
53 {
54 static $runtime = null;
55 if ($runtime !== null) {
56 return $runtime;
57 }
58
59 $cached = get_transient(self::CACHE_KEY);
60 if (is_array($cached)) {
61 $runtime = $cached;
62 return $cached;
63 }
64
65 $entries = [];
66 foreach (self::TYPES as $type) {
67 foreach ($this->entriesForType($type) as $entry) {
68 $entries[] = $entry;
69 }
70 }
71 $entries = $this->dedupe($entries);
72
73 /**
74 * Final say over the sitemap's entries — lets integrations drop
75 * noindex URLs, add multilingual alternates, etc.
76 *
77 * @param array<int, array{loc: string, lastmod: string}> $entries
78 */
79 $entries = (array) apply_filters('yatra_sitemap_entries', $entries);
80
81 // Never emit more than a single sitemap file may legally hold. Realistic
82 // travel catalogues are orders of magnitude below this; the cap is a
83 // safety valve so an extreme catalogue can't produce an invalid file.
84 $max = (int) apply_filters('yatra_sitemap_max_urls', self::MAX_URLS);
85 if ($max > 0 && count($entries) > $max) {
86 $entries = array_slice($entries, 0, $max);
87 }
88
89 set_transient(self::CACHE_KEY, $entries, self::CACHE_TTL);
90 $runtime = $entries;
91
92 return $entries;
93 }
94
95 public function getCount(): int
96 {
97 return count($this->getAllEntries());
98 }
99
100 /**
101 * Newest lastmod across all entries (for index <lastmod> in SEO plugins).
102 */
103 public function getLatestLastmod(): string
104 {
105 $latest = '';
106 foreach ($this->getAllEntries() as $entry) {
107 if ($entry['lastmod'] !== '' && $entry['lastmod'] > $latest) {
108 $latest = $entry['lastmod'];
109 }
110 }
111
112 return $latest;
113 }
114
115 /**
116 * Drop the cached list. Call when a trip or classification changes.
117 */
118 public static function flushCache(): void
119 {
120 delete_transient(self::CACHE_KEY);
121 }
122
123 /**
124 * @return array<int, array{loc: string, lastmod: string}>
125 */
126 private function entriesForType(string $type): array
127 {
128 switch ($type) {
129 case self::TYPE_ARCHIVE:
130 return $this->archiveEntries();
131 case self::TYPE_TRIP:
132 return $this->tripEntries();
133 case self::TYPE_DESTINATION:
134 case self::TYPE_ACTIVITY:
135 case self::TYPE_CATEGORY:
136 return $this->classificationEntries($type);
137 default:
138 return [];
139 }
140 }
141
142 /**
143 * The trip listing page plus each taxonomy index page.
144 *
145 * @return array<int, array{loc: string, lastmod: string}>
146 */
147 private function archiveEntries(): array
148 {
149 $entries = [];
150
151 $listing = yatra_get_trip_listing_url();
152 if ($listing !== '') {
153 $entries[] = ['loc' => $listing, 'lastmod' => ''];
154 }
155
156 if (function_exists('yatra_get_taxonomy_listing_url')) {
157 foreach (['destination', 'activity', 'trip-category'] as $listingType) {
158 $url = yatra_get_taxonomy_listing_url($listingType);
159 if ($url !== '' && $url !== home_url('/')) {
160 $entries[] = ['loc' => $url, 'lastmod' => ''];
161 }
162 }
163 }
164
165 return $entries;
166 }
167
168 /**
169 * @return array<int, array{loc: string, lastmod: string}>
170 */
171 private function tripEntries(): array
172 {
173 global $wpdb;
174
175 $table = TripsTable::getTableName();
176 $rows = $wpdb->get_results(
177 "SELECT id, slug, updated_at
178 FROM `{$table}`
179 WHERE status IN ('publish', 'published')
180 AND (deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00')
181 AND slug <> ''
182 ORDER BY updated_at DESC"
183 );
184
185 $entries = [];
186 foreach ((array) $rows as $row) {
187 $loc = yatra_get_trip_permalink($row);
188 if ($loc === '') {
189 continue;
190 }
191 $entries[] = ['loc' => $loc, 'lastmod' => $this->toW3c((string) ($row->updated_at ?? ''))];
192 }
193
194 return $entries;
195 }
196
197 /**
198 * @return array<int, array{loc: string, lastmod: string}>
199 */
200 private function classificationEntries(string $type): array
201 {
202 global $wpdb;
203
204 $table = ClassificationsTable::getTableName();
205 $rows = $wpdb->get_results(
206 $wpdb->prepare(
207 "SELECT id, slug, updated_at
208 FROM `{$table}`
209 WHERE type = %s
210 AND status = 'publish'
211 AND slug <> ''
212 ORDER BY updated_at DESC",
213 $type
214 )
215 );
216
217 $resolver = $this->permalinkResolver($type);
218
219 $entries = [];
220 foreach ((array) $rows as $row) {
221 $loc = $resolver($row);
222 if ($loc === '') {
223 continue;
224 }
225 $entries[] = ['loc' => $loc, 'lastmod' => $this->toW3c((string) ($row->updated_at ?? ''))];
226 }
227
228 return $entries;
229 }
230
231 /**
232 * @return callable(object): string
233 */
234 private function permalinkResolver(string $type): callable
235 {
236 switch ($type) {
237 case self::TYPE_DESTINATION:
238 return static fn ($row) => yatra_get_destination_permalink($row);
239 case self::TYPE_ACTIVITY:
240 return static fn ($row) => yatra_get_activity_permalink($row);
241 case self::TYPE_CATEGORY:
242 return static fn ($row) => yatra_get_category_permalink($row);
243 default:
244 return static fn ($row) => '';
245 }
246 }
247
248 /**
249 * Convert a MySQL datetime to a W3C / ISO-8601 string for <lastmod>.
250 */
251 private function toW3c(string $mysqlDate): string
252 {
253 $mysqlDate = trim($mysqlDate);
254 if ($mysqlDate === '' || strpos($mysqlDate, '0000-00-00') === 0) {
255 return '';
256 }
257
258 $timestamp = strtotime($mysqlDate);
259 if ($timestamp === false) {
260 return '';
261 }
262
263 return gmdate('Y-m-d\TH:i:s+00:00', $timestamp);
264 }
265
266 /**
267 * @param array<int, array{loc: string, lastmod: string}> $entries
268 * @return array<int, array{loc: string, lastmod: string}>
269 */
270 private function dedupe(array $entries): array
271 {
272 $seen = [];
273 $out = [];
274 foreach ($entries as $entry) {
275 $loc = $entry['loc'];
276 if ($loc === '' || isset($seen[$loc])) {
277 continue;
278 }
279 $seen[$loc] = true;
280 $out[] = $entry;
281 }
282
283 return $out;
284 }
285 }
286