PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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 / DestinationService.php

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

427 lines 14.5 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\Repositories\DestinationRepository;
8 use Yatra\Helpers\ClassificationLandingPageMetadata;
9 use Yatra\Helpers\SlugHelper;
10 use Yatra\Helpers\FormatHelper;
11 use Yatra\Database\Tables\ClassificationsTable;
12
13 /**
14 * Destination Service
15 * Contains business logic for destinations
16 */
17 class DestinationService extends BaseService
18 {
19 /**
20 * @var DestinationRepository
21 */
22 private DestinationRepository $repository;
23
24 /**
25 * Constructor
26 */
27 public function __construct()
28 {
29 $this->repository = new DestinationRepository();
30 }
31
32 /**
33 * Get repository
34 */
35 protected function getRepository(): DestinationRepository
36 {
37 return $this->repository;
38 }
39
40 /**
41 * Validate destination data
42 */
43 protected function validate(array $data, ?int $id = null): void
44 {
45 if (empty($data['name'])) {
46 throw new \InvalidArgumentException('Destination name is required');
47 }
48
49 // Slug will be auto-generated from name, so we don't need to validate it here
50 // The SlugHelper will ensure uniqueness
51
52 // Validate status - accept both old and new values for backward compatibility
53 $allowed_statuses = ['draft', 'active', 'inactive', 'publish', 'trash'];
54 if (isset($data['status']) && !in_array($data['status'], $allowed_statuses, true)) {
55 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
56 }
57 }
58
59 /**
60 * Process before create
61 */
62 protected function processBeforeCreate(array $data): array
63 {
64 // Sanitize name
65 if (isset($data['name'])) {
66 $data['name'] = sanitize_text_field($data['name']);
67 }
68
69 // Always auto-generate slug from name (backend ensures uniqueness)
70 if (!empty($data['name'])) {
71 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
72 $data['name'],
73 ClassificationsTable::getTableName(),
74 'slug'
75 );
76 } elseif (isset($data['slug'])) {
77 // If name is empty but slug is provided, sanitize it
78 $data['slug'] = SlugHelper::generate($data['slug']);
79 }
80
81 // Sanitize Quill HTML description
82 if (isset($data['description'])) {
83 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
84 }
85
86 // Sanitize status
87 if (isset($data['status'])) {
88 // Validate status
89 $allowed_statuses = ['draft', 'publish', 'trash'];
90 $data['status'] = in_array($data['status'], $allowed_statuses, true)
91 ? $data['status']
92 : 'draft';
93 } else {
94 $data['status'] = 'draft';
95 }
96
97 // Set created_by and updated_by to current user
98 $current_user_id = get_current_user_id();
99 $data['created_by'] = absint($current_user_id);
100 $data['updated_by'] = absint($current_user_id);
101
102 // Sanitize and serialize icon if it's an array
103 if (isset($data['icon'])) {
104 if (is_array($data['icon'])) {
105 // Convert URL back to attachment ID if possible
106 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
107 $value = $data['icon']['value'];
108
109 // If it's a URL, try to find the attachment ID
110 if (filter_var($value, FILTER_VALIDATE_URL)) {
111 $attachment_id = attachment_url_to_postid($value);
112 if ($attachment_id) {
113 $data['icon']['value'] = $attachment_id;
114 }
115 }
116 // If it's already numeric, keep it as is
117 elseif (is_numeric($value)) {
118 $data['icon']['value'] = (int) $value;
119 }
120 }
121
122 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
123 $data['icon'] = maybe_serialize($data['icon']);
124 } elseif (is_string($data['icon'])) {
125 // If it's already a string, sanitize it
126 $data['icon'] = sanitize_text_field($data['icon']);
127 }
128 }
129
130 // IMPORTANT: Force type to 'destination' - this overrides any frontend type
131 $data['type'] = 'destination';
132
133 // Handle SEO metadata
134 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
135 $existing_metadata = ClassificationLandingPageMetadata::baseMetadataMergedWithRequest(
136 null,
137 $this->repository,
138 $data
139 );
140
141 // Add SEO fields to metadata
142 if (isset($data['seo_title'])) {
143 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
144 unset($data['seo_title']); // Remove from main data
145 }
146 if (isset($data['seo_description'])) {
147 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
148 unset($data['seo_description']); // Remove from main data
149 }
150 if (isset($data['seo_keywords'])) {
151 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
152 unset($data['seo_keywords']); // Remove from main data
153 }
154
155 $data['metadata'] = $existing_metadata;
156 }
157
158 ClassificationLandingPageMetadata::mergeLandingPageIntoData($data, null, $this->repository);
159
160 return $data;
161 }
162
163 /**
164 * Process before update
165 */
166 protected function processBeforeUpdate(int $id, array $data): array
167 {
168
169 // Remove preserve_slug flag if sent from frontend (not a database column)
170 unset($data['preserve_slug']);
171
172 // Sanitize name
173 if (isset($data['name'])) {
174 $data['name'] = sanitize_text_field($data['name']);
175 }
176
177 // Handle slug in EDIT mode: Only update if explicitly provided
178 // Do NOT auto-generate from name in edit mode
179 if (isset($data['slug']) && !empty($data['slug'])) {
180 // Slug was explicitly provided - ensure uniqueness
181 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
182 $data['slug'],
183 ClassificationsTable::getTableName(),
184 'slug',
185 $id // Exclude current record when checking uniqueness
186 );
187 }
188 // If slug is not provided, don't modify it (keep existing slug)
189
190 // Sanitize Quill HTML description
191 if (isset($data['description'])) {
192 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
193 }
194
195 // Sanitize status
196 if (isset($data['status'])) {
197 // Validate status
198 $allowed_statuses = ['draft', 'publish', 'trash'];
199 $data['status'] = in_array($data['status'], $allowed_statuses, true)
200 ? $data['status']
201 : 'draft';
202 }
203
204 // Set updated_by to current user
205 $data['updated_by'] = absint(get_current_user_id());
206
207 // Sanitize and serialize icon if it's an array
208 if (isset($data['icon'])) {
209 if (is_array($data['icon'])) {
210 // Convert URL back to attachment ID if possible
211 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
212 $value = $data['icon']['value'];
213
214 // If it's a URL, try to find the attachment ID
215 if (filter_var($value, FILTER_VALIDATE_URL)) {
216 $attachment_id = attachment_url_to_postid($value);
217 if ($attachment_id) {
218 $data['icon']['value'] = $attachment_id;
219 }
220 }
221 // If it's already numeric, keep it as is
222 elseif (is_numeric($value)) {
223 $data['icon']['value'] = (int) $value;
224 }
225 }
226
227 $data['icon'] = yatra_normalize_icon_picker_for_storage($data['icon']);
228 $data['icon'] = maybe_serialize($data['icon']);
229 } elseif (is_string($data['icon'])) {
230 // If it's already a string, sanitize it
231 $data['icon'] = sanitize_text_field($data['icon']);
232 }
233 }
234
235 // IMPORTANT: Ensure type remains 'destination' - prevent frontend from changing type
236 $data['type'] = 'destination';
237
238 // Handle SEO metadata
239 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
240 $existing_metadata = ClassificationLandingPageMetadata::baseMetadataMergedWithRequest(
241 $id,
242 $this->repository,
243 $data
244 );
245
246 // Add/update SEO fields in metadata
247 if (isset($data['seo_title'])) {
248 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
249 unset($data['seo_title']); // Remove from main data
250 }
251 if (isset($data['seo_description'])) {
252 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
253 unset($data['seo_description']); // Remove from main data
254 }
255 if (isset($data['seo_keywords'])) {
256 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
257 unset($data['seo_keywords']); // Remove from main data
258 }
259
260 $data['metadata'] = $existing_metadata;
261 }
262
263 ClassificationLandingPageMetadata::mergeLandingPageIntoData($data, $id, $this->repository);
264
265 return $data;
266 }
267
268 /**
269 * Get status counts for destinations
270 */
271 public function getAll(array $args = []): array
272 {
273 // IMPORTANT: Always filter by type = 'destination' for destinations
274 $args['where']['type'] = 'destination';
275
276 // Sanitize and handle search
277 if (!empty($args['search'])) {
278 $search = sanitize_text_field($args['search']);
279 return $this->repository->search($search, $args);
280 }
281
282 // Sanitize and handle status filter
283 if (!empty($args['status']) && $args['status'] !== 'all') {
284 $allowed_statuses = ['draft', 'publish', 'trash'];
285 $status = in_array($args['status'], $allowed_statuses, true)
286 ? $args['status']
287 : null;
288 if ($status) {
289 $args['where']['status'] = $status;
290 }
291 }
292
293 return $this->repository->all($args);
294 }
295
296 /**
297 * Get published destinations
298 */
299 public function getPublished(array $args = []): array
300 {
301 return $this->repository->getPublished($args);
302 }
303
304 /**
305 * Get published destinations along with aggregated stats such as
306 * trips_count derived from related trip records.
307 */
308 public function getPublishedWithStats(): array
309 {
310 // For now this simply delegates to a repository method that attaches
311 // trips_count using the yatra_trip_destinations relation table.
312 return $this->repository->getPublishedWithTripCounts();
313 }
314
315 /**
316 * Count items
317 */
318 public function count(array $args = []): int
319 {
320 // IMPORTANT: Always filter by type = 'destination' for destinations
321 $args['where']['type'] = 'destination';
322
323 // Sanitize and handle search
324 if (!empty($args['search'])) {
325 $search = sanitize_text_field($args['search']);
326 $items = $this->repository->search($search, $args);
327 return count($items);
328 }
329
330 // Sanitize and handle status filter
331 if (!empty($args['status']) && $args['status'] !== 'all') {
332 $allowed_statuses = ['draft', 'publish', 'trash'];
333 $status = in_array($args['status'], $allowed_statuses, true)
334 ? $args['status']
335 : null;
336 if ($status) {
337 $args['where']['status'] = $status;
338 }
339 }
340
341 return $this->repository->count($args);
342 }
343
344 /**
345 * Bulk update status for multiple destinations
346 */
347 public function bulkUpdateStatus(array $ids, string $status): int
348 {
349 $allowed_statuses = ['draft', 'publish', 'trash'];
350 if (!in_array($status, $allowed_statuses, true)) {
351 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
352 }
353
354 $updated = 0;
355 foreach ($ids as $id) {
356 $id = absint($id);
357 if ($this->repository->update($id, ['status' => $status])) {
358 $updated++;
359 }
360 }
361
362 return $updated;
363 }
364
365 /**
366 * Get trip count for a destination
367 */
368 public function getTripCount(int $destinationId): int
369 {
370 $destinationRepository = new \Yatra\Repositories\DestinationRepository();
371 return $destinationRepository->getTripCount($destinationId);
372 }
373
374 /**
375 * Get trip count for destination (direct field method)
376 */
377 public function getTripCountDirect(int $destinationId): int
378 {
379 $destinationRepository = new \Yatra\Repositories\DestinationRepository();
380 return $destinationRepository->getTripCountDirect($destinationId);
381 }
382
383 /**
384 * Bulk delete destinations
385 */
386 public function bulkDelete(array $ids): int
387 {
388 $deleted = 0;
389 foreach ($ids as $id) {
390 $id = absint($id);
391 if ($this->repository->delete($id)) {
392 $deleted++;
393 }
394 }
395
396 return $deleted;
397 }
398
399 /**
400 * Get status counts for list views
401 */
402 public function getStatusCounts(): array
403 {
404 $counts = $this->repository->getStatusCounts();
405
406 $publish = $counts['publish'] ?? 0;
407 $draft = $counts['draft'] ?? 0;
408 $trash = $counts['trash'] ?? 0;
409
410 // Calculate total from all statuses, not just the main three
411 $all = $counts['total']??0;
412
413 $result = [
414 'all' => (int) $all,
415 'publish' => (int) $publish,
416 'draft' => (int) $draft,
417 'trash' => (int) $trash,
418 ];
419
420 // Add legacy status keys for backward compatibility
421 $result['active'] = $result['publish'];
422 $result['inactive'] = $result['trash'];
423
424 return $result;
425 }
426 }
427