PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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.3, at app/Services/DestinationService.php

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