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

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

434 lines 14.9 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'] = maybe_serialize($data['icon']);
122 } elseif (is_string($data['icon'])) {
123 // If it's already a string, sanitize it
124 $data['icon'] = sanitize_text_field($data['icon']);
125 }
126 }
127
128 // IMPORTANT: Force type to 'destination' - this overrides any frontend type
129 $data['type'] = 'destination';
130
131 // Handle SEO metadata
132 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
133 $existing_metadata = [];
134 if (isset($data['metadata']) && is_array($data['metadata'])) {
135 $existing_metadata = $data['metadata'];
136 } elseif (isset($data['metadata'])) {
137 $existing_metadata = maybe_unserialize($data['metadata']);
138 }
139
140 // Add SEO fields to metadata
141 if (isset($data['seo_title'])) {
142 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
143 unset($data['seo_title']); // Remove from main data
144 }
145 if (isset($data['seo_description'])) {
146 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
147 unset($data['seo_description']); // Remove from main data
148 }
149 if (isset($data['seo_keywords'])) {
150 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
151 unset($data['seo_keywords']); // Remove from main data
152 }
153
154 $data['metadata'] = $existing_metadata;
155 }
156
157 return $data;
158 }
159
160 /**
161 * Process before update
162 */
163 protected function processBeforeUpdate(int $id, array $data): array
164 {
165
166 // Remove preserve_slug flag if sent from frontend (not a database column)
167 unset($data['preserve_slug']);
168
169 // Sanitize name
170 if (isset($data['name'])) {
171 $data['name'] = sanitize_text_field($data['name']);
172 }
173
174 // Handle slug in EDIT mode: Only update if explicitly provided
175 // Do NOT auto-generate from name in edit mode
176 if (isset($data['slug']) && !empty($data['slug'])) {
177 // Slug was explicitly provided - ensure uniqueness
178 $data['slug'] = SlugHelper::generateUniqueFromDatabase(
179 $data['slug'],
180 ClassificationsTable::getTableName(),
181 'slug',
182 $id // Exclude current record when checking uniqueness
183 );
184 }
185 // If slug is not provided, don't modify it (keep existing slug)
186
187 // Sanitize Quill HTML description
188 if (isset($data['description'])) {
189 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
190 }
191
192 // Sanitize status
193 if (isset($data['status'])) {
194 // Validate status
195 $allowed_statuses = ['draft', 'publish', 'trash'];
196 $data['status'] = in_array($data['status'], $allowed_statuses, true)
197 ? $data['status']
198 : 'draft';
199 }
200
201 // Set updated_by to current user
202 $data['updated_by'] = absint(get_current_user_id());
203
204 // Sanitize and serialize icon if it's an array
205 if (isset($data['icon'])) {
206 if (is_array($data['icon'])) {
207 // Convert URL back to attachment ID if possible
208 if ($data['icon']['type'] === 'image' && !empty($data['icon']['value'])) {
209 $value = $data['icon']['value'];
210
211 // If it's a URL, try to find the attachment ID
212 if (filter_var($value, FILTER_VALIDATE_URL)) {
213 $attachment_id = attachment_url_to_postid($value);
214 if ($attachment_id) {
215 $data['icon']['value'] = $attachment_id;
216 }
217 }
218 // If it's already numeric, keep it as is
219 elseif (is_numeric($value)) {
220 $data['icon']['value'] = (int) $value;
221 }
222 }
223
224 $data['icon'] = maybe_serialize($data['icon']);
225 } elseif (is_string($data['icon'])) {
226 // If it's already a string, sanitize it
227 $data['icon'] = sanitize_text_field($data['icon']);
228 }
229 }
230
231 // IMPORTANT: Ensure type remains 'destination' - prevent frontend from changing type
232 $data['type'] = 'destination';
233
234 // Handle SEO metadata
235 if (isset($data['seo_title']) || isset($data['seo_description']) || isset($data['seo_keywords'])) {
236 // Get existing metadata
237 $existing_metadata = [];
238 if (isset($data['metadata']) && is_array($data['metadata'])) {
239 $existing_metadata = $data['metadata'];
240 } elseif (isset($data['metadata'])) {
241 $existing_metadata = maybe_unserialize($data['metadata']);
242 } else {
243 // Get existing metadata from database
244 $existing = $this->repository->find($id);
245 if ($existing && isset($existing->metadata)) {
246 $existing_metadata = maybe_unserialize($existing->metadata);
247 }
248 }
249
250 // `metadata` can be stored as serialized string or other scalar; ensure array before offset access.
251 if (!is_array($existing_metadata)) {
252 $existing_metadata = [];
253 }
254
255 // Add/update SEO fields in metadata
256 if (isset($data['seo_title'])) {
257 $existing_metadata['seo_title'] = sanitize_text_field($data['seo_title']);
258 unset($data['seo_title']); // Remove from main data
259 }
260 if (isset($data['seo_description'])) {
261 $existing_metadata['seo_description'] = sanitize_textarea_field($data['seo_description']);
262 unset($data['seo_description']); // Remove from main data
263 }
264 if (isset($data['seo_keywords'])) {
265 $existing_metadata['seo_keywords'] = sanitize_text_field($data['seo_keywords']);
266 unset($data['seo_keywords']); // Remove from main data
267 }
268
269 $data['metadata'] = $existing_metadata;
270 }
271
272 return $data;
273 }
274
275 /**
276 * Get status counts for destinations
277 */
278 public function getAll(array $args = []): array
279 {
280 // IMPORTANT: Always filter by type = 'destination' for destinations
281 $args['where']['type'] = 'destination';
282
283 // Sanitize and handle search
284 if (!empty($args['search'])) {
285 $search = sanitize_text_field($args['search']);
286 return $this->repository->search($search, $args);
287 }
288
289 // Sanitize and handle status filter
290 if (!empty($args['status']) && $args['status'] !== 'all') {
291 $allowed_statuses = ['draft', 'publish', 'trash'];
292 $status = in_array($args['status'], $allowed_statuses, true)
293 ? $args['status']
294 : null;
295 if ($status) {
296 $args['where']['status'] = $status;
297 }
298 }
299
300 return $this->repository->all($args);
301 }
302
303 /**
304 * Get published destinations
305 */
306 public function getPublished(array $args = []): array
307 {
308 return $this->repository->getPublished($args);
309 }
310
311 /**
312 * Get published destinations along with aggregated stats such as
313 * trips_count derived from related trip records.
314 */
315 public function getPublishedWithStats(): array
316 {
317 // For now this simply delegates to a repository method that attaches
318 // trips_count using the yatra_trip_destinations relation table.
319 return $this->repository->getPublishedWithTripCounts();
320 }
321
322 /**
323 * Count items
324 */
325 public function count(array $args = []): int
326 {
327 // IMPORTANT: Always filter by type = 'destination' for destinations
328 $args['where']['type'] = 'destination';
329
330 // Sanitize and handle search
331 if (!empty($args['search'])) {
332 $search = sanitize_text_field($args['search']);
333 $items = $this->repository->search($search, $args);
334 return count($items);
335 }
336
337 // Sanitize and handle status filter
338 if (!empty($args['status']) && $args['status'] !== 'all') {
339 $allowed_statuses = ['draft', 'publish', 'trash'];
340 $status = in_array($args['status'], $allowed_statuses, true)
341 ? $args['status']
342 : null;
343 if ($status) {
344 $args['where']['status'] = $status;
345 }
346 }
347
348 return $this->repository->count($args);
349 }
350
351 /**
352 * Bulk update status for multiple destinations
353 */
354 public function bulkUpdateStatus(array $ids, string $status): int
355 {
356 $allowed_statuses = ['draft', 'publish', 'trash'];
357 if (!in_array($status, $allowed_statuses, true)) {
358 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
359 }
360
361 $updated = 0;
362 foreach ($ids as $id) {
363 $id = absint($id);
364 if ($this->repository->update($id, ['status' => $status])) {
365 $updated++;
366 }
367 }
368
369 return $updated;
370 }
371
372 /**
373 * Get trip count for a destination
374 */
375 public function getTripCount(int $destinationId): int
376 {
377 $destinationRepository = new \Yatra\Repositories\DestinationRepository();
378 return $destinationRepository->getTripCount($destinationId);
379 }
380
381 /**
382 * Get trip count for destination (direct field method)
383 */
384 public function getTripCountDirect(int $destinationId): int
385 {
386 $destinationRepository = new \Yatra\Repositories\DestinationRepository();
387 return $destinationRepository->getTripCountDirect($destinationId);
388 }
389
390 /**
391 * Bulk delete destinations
392 */
393 public function bulkDelete(array $ids): int
394 {
395 $deleted = 0;
396 foreach ($ids as $id) {
397 $id = absint($id);
398 if ($this->repository->delete($id)) {
399 $deleted++;
400 }
401 }
402
403 return $deleted;
404 }
405
406 /**
407 * Get status counts for list views
408 */
409 public function getStatusCounts(): array
410 {
411 $counts = $this->repository->getStatusCounts();
412
413 $publish = $counts['publish'] ?? 0;
414 $draft = $counts['draft'] ?? 0;
415 $trash = $counts['trash'] ?? 0;
416
417 // Calculate total from all statuses, not just the main three
418 $all = $counts['total']??0;
419
420 $result = [
421 'all' => (int) $all,
422 'publish' => (int) $publish,
423 'draft' => (int) $draft,
424 'trash' => (int) $trash,
425 ];
426
427 // Add legacy status keys for backward compatibility
428 $result['active'] = $result['publish'];
429 $result['inactive'] = $result['trash'];
430
431 return $result;
432 }
433 }
434