PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.16 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 All 84 releases
yatra / app / Services / AttributeService.php

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

827 lines 27.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\AttributeRepository;
8 use Yatra\Repositories\TripAttributeRepository;
9 use Yatra\Utils\Logger;
10 use Yatra\Utils\Cache;
11 use Yatra\Helpers\FormatHelper;
12
13 /**
14 * Attribute Service
15 * Handles business logic for trip attributes
16 */
17 class AttributeService extends BaseService
18 {
19 /**
20 * @var AttributeRepository
21 */
22 private $attributeRepository;
23
24 /**
25 * @var TripAttributeRepository
26 */
27 private $tripAttributeRepository;
28
29 /**
30 * Constructor
31 */
32 public function __construct()
33 {
34 $this->attributeRepository = new AttributeRepository();
35 $this->tripAttributeRepository = new TripAttributeRepository();
36 }
37
38 /**
39 * Get repository instance
40 */
41 protected function getRepository()
42 {
43 return $this->attributeRepository;
44 }
45
46 /**
47 * Get all published attributes for frontend
48 */
49 public function getFrontendAttributes(): array
50 {
51 try {
52 return $this->attributeRepository->withQueryCache(
53 'yatra_frontend_attributes',
54 function () {
55 return $this->attributeRepository->getAllPublished();
56 },
57 3600
58 );
59 } catch (\Exception $e) {
60 Logger::error('Failed to get frontend attributes: ' . $e->getMessage());
61 return [];
62 }
63 }
64
65 /**
66 * Get filterable attributes
67 */
68 public function getFilterableAttributes(): array
69 {
70 try {
71 return $this->attributeRepository->withQueryCache(
72 'yatra_filterable_attributes',
73 function () {
74 return $this->attributeRepository->getFilterableAttributes();
75 },
76 1800
77 );
78 } catch (\Exception $e) {
79 Logger::error('Failed to get filterable attributes: ' . $e->getMessage());
80 return [];
81 }
82 }
83
84 /**
85 * Get attributes for a specific trip
86 */
87 public function getTripAttributes(int $tripId): array
88 {
89 try {
90 return $this->tripAttributeRepository->withQueryCache(
91 "yatra_trip_attributes_{$tripId}",
92 function () use ($tripId) {
93 return $this->tripAttributeRepository->getTripAttributes($tripId);
94 },
95 1800
96 );
97 } catch (\Exception $e) {
98 Logger::error("Failed to get attributes for trip {$tripId}: " . $e->getMessage());
99 return [];
100 }
101 }
102
103 /**
104 * Set attribute value for a trip
105 */
106 public function setTripAttribute(int $tripId, int $attributeId, $value): bool
107 {
108 try {
109 if (!$tripId || !$attributeId) {
110 throw new \InvalidArgumentException('Trip ID and Attribute ID are required');
111 }
112
113 // Validate attribute exists
114 $attribute = $this->attributeRepository->find($attributeId);
115 if (!$attribute || $attribute->status !== 'publish') {
116 throw new \InvalidArgumentException('Attribute not found or not published');
117 }
118
119 // Validate value based on field type
120 $this->validateAttributeValue($attribute, $value);
121
122 $result = $this->tripAttributeRepository->setTripAttribute($tripId, $attributeId, $value);
123
124 if ($result) {
125 // Clear cache
126 $this->clearTripAttributeCache($tripId);
127
128 // Log action
129 Logger::info("Set attribute {$attributeId} value for trip {$tripId}");
130
131 // Fire action hook
132 do_action('yatra_trip_attribute_set', $tripId, $attributeId, $value);
133 }
134
135 return $result;
136
137 } catch (\Exception $e) {
138 Logger::error("Failed to set attribute for trip {$tripId}: " . $e->getMessage());
139 return false;
140 }
141 }
142
143 /**
144 * Remove attribute value from a trip
145 */
146 public function removeTripAttribute(int $tripId, int $attributeId): bool
147 {
148 try {
149 if (!$tripId || !$attributeId) {
150 throw new \InvalidArgumentException('Trip ID and Attribute ID are required');
151 }
152
153 $result = $this->tripAttributeRepository->deleteTripAttribute($tripId, $attributeId);
154
155 if ($result) {
156 // Clear cache
157 $this->clearTripAttributeCache($tripId);
158
159 // Log action
160 Logger::info("Removed attribute {$attributeId} from trip {$tripId}");
161
162 // Fire action hook
163 do_action('yatra_trip_attribute_removed', $tripId, $attributeId);
164 }
165
166 return $result;
167
168 } catch (\Exception $e) {
169 Logger::error("Failed to remove attribute from trip {$tripId}: " . $e->getMessage());
170 return false;
171 }
172 }
173
174 /**
175 * Get trips by attribute value
176 */
177 public function getTripsByAttributeValue(int $attributeId, string $value): array
178 {
179 try {
180 return $this->attributeRepository->withQueryCache(
181 "yatra_trips_by_attr_{$attributeId}_" . md5($value),
182 function () use ($attributeId, $value) {
183 $tripAttributeTable = $this->tripAttributeRepository->getTableName();
184 $query = "SELECT DISTINCT t.* FROM {$this->attributeRepository->getTableName()} a
185 INNER JOIN {$tripAttributeTable} ta ON ta.attribute_id = a.id
186 INNER JOIN {$this->attributeRepository->getTripsTableName()} t ON t.id = ta.trip_id
187 WHERE a.id = %d AND ta.value = %s AND t.status = 'publish'";
188
189 return $this->attributeRepository->wpdb->get_results(
190 $this->attributeRepository->wpdb->prepare($query, $attributeId, $value)
191 ) ?: [];
192 },
193 1800
194 );
195 } catch (\Exception $e) {
196 Logger::error("Failed to get trips by attribute value: " . $e->getMessage());
197 return [];
198 }
199 }
200
201 /**
202 * Get all possible values for an attribute (for filter dropdowns)
203 */
204 public function getAttributeValues(int $attributeId): array
205 {
206 try {
207 return $this->attributeRepository->withQueryCache(
208 "yatra_attribute_values_{$attributeId}",
209 function () use ($attributeId) {
210 $tripAttributeTable = $this->tripAttributeRepository->getTableName();
211 $query = "SELECT DISTINCT value, COUNT(*) as count FROM {$tripAttributeTable}
212 WHERE attribute_id = %d AND value IS NOT NULL AND value != ''
213 GROUP BY value ORDER BY count DESC, value ASC";
214
215 return $this->attributeRepository->wpdb->get_results(
216 $this->attributeRepository->wpdb->prepare($query, $attributeId)
217 ) ?: [];
218 },
219 3600
220 );
221 } catch (\Exception $e) {
222 Logger::error("Failed to get attribute values: " . $e->getMessage());
223 return [];
224 }
225 }
226
227 /**
228 * Search attributes
229 */
230 public function search(string $term): array
231 {
232 try {
233 return $this->attributeRepository->withQueryCache(
234 'yatra_search_attributes_' . md5($term),
235 function () use ($term) {
236 return $this->attributeRepository->search($term);
237 },
238 1800
239 );
240 } catch (\Exception $e) {
241 Logger::error("Failed to search attributes: " . $e->getMessage());
242 return [];
243 }
244 }
245
246 /**
247 * Clear related entity caches
248 */
249 protected function clearRelatedEntityCaches(int $id, string $operation): void
250 {
251 // Clear frontend attributes cache
252 $this->clearCacheByPattern('yatra_frontend_attributes');
253
254 // Clear filterable attributes cache
255 $this->clearCacheByPattern('yatra_filterable_attributes');
256
257 // Clear trip-related attribute caches
258 $this->clearCacheByPattern('yatra_trip_attributes_');
259
260 // Clear search attributes cache
261 $this->clearCacheByPattern('yatra_search_attributes_');
262
263 // Clear attribute values cache
264 $this->clearCacheByPattern('yatra_attribute_values_');
265
266 // Clear trips by attribute value cache
267 $this->clearCacheByPattern('yatra_trips_by_attr_');
268
269 // Clear trip listing caches since attributes affect trip filtering
270 $this->clearCacheByPattern('trip_listing_');
271
272 // Clear query result caches
273 $this->clearCacheByPattern(Cache::PREFIX_QUERY_RESULT);
274
275 Logger::debug("Attribute related caches cleared", [
276 'attribute_id' => $id,
277 'operation' => $operation
278 ]);
279 }
280
281 /**
282 * Clear cache by pattern
283 */
284 private function clearCacheByPattern(string $pattern): void
285 {
286 try {
287 Cache::clearByPrefix($pattern);
288 } catch (\Exception $e) {
289 Logger::warning("Failed to clear attribute cache pattern", [
290 'pattern' => $pattern,
291 'error' => $e->getMessage()
292 ]);
293 }
294 }
295
296 /**
297 * Normalize field_options from JSON string or array into a clean list of label/value pairs.
298 *
299 * @param array<string, mixed> $data
300 */
301 private function normalizeFieldOptionsInPlace(array &$data): void
302 {
303 if (!array_key_exists('field_options', $data)) {
304 return;
305 }
306 $fo = $data['field_options'];
307 if (is_string($fo)) {
308 $t = trim($fo);
309 if ($t === '') {
310 $data['field_options'] = [];
311 return;
312 }
313 $decoded = json_decode($t, true);
314 $fo = is_array($decoded) ? $decoded : [];
315 }
316 if (!is_array($fo)) {
317 $data['field_options'] = [];
318 return;
319 }
320 $clean = [];
321 foreach ($fo as $row) {
322 if (!is_array($row)) {
323 continue;
324 }
325 $label = isset($row['label']) ? sanitize_text_field((string) $row['label']) : '';
326 $value = isset($row['value']) ? sanitize_text_field((string) $row['value']) : '';
327 if ($label === '' && $value === '') {
328 continue;
329 }
330 if ($value === '' && $label !== '') {
331 $value = sanitize_title($label);
332 }
333 $clean[] = ['label' => $label, 'value' => $value];
334 }
335 $data['field_options'] = $clean;
336 }
337
338 /**
339 * Create new attribute
340 */
341 public function createAttribute(array $data): int
342 {
343 try {
344 // Validate required fields
345 if (empty($data['name'])) {
346 throw new \InvalidArgumentException('Attribute name is required');
347 }
348
349 // Generate slug if not provided
350 if (empty($data['slug'])) {
351 $data['slug'] = sanitize_title($data['name']);
352 }
353
354 // Check if slug already exists
355 if ($this->attributeRepository->slugExists($data['slug'])) {
356 throw new \InvalidArgumentException('Attribute with this slug already exists');
357 }
358
359 // Set default values
360 $data = array_merge([
361 'field_type' => 'text_field',
362 'required' => 0,
363 'show_on_frontend' => 1,
364 'show_in_filters' => 0,
365 'filter_type' => 'exact',
366 'searchable' => 0,
367 'status' => 'publish',
368 'display_order' => $this->attributeRepository->getMaxDisplayOrder() + 1
369 ], $data);
370
371 $this->normalizeFieldOptionsInPlace($data);
372
373 // Validate field type
374 $validFieldTypes = ['text_field', 'number', 'email', 'url', 'textarea', 'select', 'radio', 'checkbox', 'date', 'time', 'color', 'file'];
375 if (!in_array($data['field_type'], $validFieldTypes)) {
376 throw new \InvalidArgumentException('Invalid field type');
377 }
378
379 // Validate field options for select/radio/checkbox
380 if (in_array($data['field_type'], ['select', 'radio', 'checkbox'], true) && empty($data['field_options'])) {
381 throw new \InvalidArgumentException('Field options are required for select, radio, and checkbox fields');
382 }
383
384 // Process field options
385 if (isset($data['field_options']) && is_array($data['field_options'])) {
386 $data['field_options'] = wp_json_encode($data['field_options']);
387 }
388
389 // Process validation rules
390 if (isset($data['validation_rules']) && is_array($data['validation_rules'])) {
391 $data['validation_rules'] = wp_json_encode($data['validation_rules']);
392 }
393
394 // Sanitize description as rich text
395 if (isset($data['description'])) {
396 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
397 }
398
399 // Process icon field
400 if (isset($data['icon'])) {
401 if (is_array($data['icon'])) {
402 // Sanitize icon array
403 $icon = [
404 'type' => isset($data['icon']['type']) && in_array($data['icon']['type'], ['icon', 'image'], true)
405 ? $data['icon']['type']
406 : 'icon',
407 'value' => isset($data['icon']['value'])
408 ? sanitize_text_field($data['icon']['value'])
409 : '',
410 ];
411 $data['icon'] = maybe_serialize($icon);
412 } elseif (is_string($data['icon'])) {
413 // If it's already a string, sanitize it
414 $data['icon'] = sanitize_text_field($data['icon']);
415 }
416 } else {
417 }
418
419 $attributeId = $this->attributeRepository->create($data);
420
421 if ($attributeId) {
422 // Clear cache
423 $this->clearAttributeCache();
424
425 // Log action
426 Logger::info("Created attribute: {$data['name']} (ID: {$attributeId})");
427
428 // Fire action hook
429 do_action('yatra_attribute_created', $attributeId, $data);
430 }
431
432 return $attributeId;
433
434 } catch (\Exception $e) {
435 Logger::error("Failed to create attribute: " . $e->getMessage());
436 throw $e;
437 }
438 }
439
440 /**
441 * Update attribute
442 */
443 public function updateAttribute(int $id, array $data): bool
444 {
445 try {
446 if (!$id) {
447 throw new \InvalidArgumentException('Attribute ID is required');
448 }
449
450 // Check if attribute exists
451 $existing = $this->attributeRepository->find($id);
452 if (!$existing) {
453 throw new \InvalidArgumentException('Attribute not found');
454 }
455
456 // Update slug if name changed
457 if (isset($data['name']) && !isset($data['slug'])) {
458 $data['slug'] = sanitize_title($data['name']);
459 }
460
461 // Check slug uniqueness (excluding current attribute)
462 if (isset($data['slug']) && $this->attributeRepository->slugExists($data['slug'], $id)) {
463 throw new \InvalidArgumentException('Attribute with this slug already exists');
464 }
465
466 if (array_key_exists('field_options', $data)) {
467 $this->normalizeFieldOptionsInPlace($data);
468 }
469
470 // Process field options
471 if (isset($data['field_options']) && is_array($data['field_options'])) {
472 $data['field_options'] = wp_json_encode($data['field_options']);
473 }
474
475 // Process validation rules
476 if (isset($data['validation_rules']) && is_array($data['validation_rules'])) {
477 $data['validation_rules'] = wp_json_encode($data['validation_rules']);
478 }
479
480 // Sanitize description as rich text
481 if (isset($data['description'])) {
482 $data['description'] = FormatHelper::sanitizeQuillHtml($data['description']);
483 }
484
485 // Process icon field
486 if (isset($data['icon'])) {
487 if (is_array($data['icon'])) {
488 // Sanitize icon array
489 $icon = [
490 'type' => isset($data['icon']['type']) && in_array($data['icon']['type'], ['icon', 'image'], true)
491 ? $data['icon']['type']
492 : 'icon',
493 'value' => isset($data['icon']['value'])
494 ? sanitize_text_field($data['icon']['value'])
495 : '',
496 ];
497 $data['icon'] = maybe_serialize($icon);
498 } elseif (is_string($data['icon'])) {
499 // If it's already a string, sanitize it
500 $data['icon'] = sanitize_text_field($data['icon']);
501 }
502 } else {
503 }
504
505 $result = $this->attributeRepository->update($id, $data);
506
507 if ($result) {
508 // Clear cache
509 $this->clearAttributeCache();
510
511 // Log action
512 Logger::info("Updated attribute ID: {$id}");
513
514 // Fire action hook
515 do_action('yatra_attribute_updated', $id, $data);
516 }
517
518 return $result;
519
520 } catch (\Exception $e) {
521 Logger::error("Failed to update attribute {$id}: " . $e->getMessage());
522 throw $e;
523 }
524 }
525
526 /**
527 * Delete attribute (soft delete)
528 */
529 public function deleteAttribute(int $id): bool
530 {
531 try {
532 if (!$id) {
533 throw new \InvalidArgumentException('Attribute ID is required');
534 }
535
536 $result = $this->attributeRepository->delete($id);
537
538 if ($result) {
539 // Clear cache
540 $this->clearAttributeCache();
541
542 // Log action
543 Logger::info("Deleted attribute ID: {$id}");
544
545 // Fire action hook
546 do_action('yatra_attribute_deleted', $id);
547 }
548
549 return $result;
550
551 } catch (\Exception $e) {
552 Logger::error("Failed to delete attribute {$id}: " . $e->getMessage());
553 throw $e;
554 }
555 }
556
557 /**
558 * Permanently delete attribute
559 */
560 public function forceDeleteAttribute(int $id): bool
561 {
562 try {
563 if (!$id) {
564 throw new \InvalidArgumentException('Attribute ID is required');
565 }
566
567 $result = $this->attributeRepository->forceDelete($id);
568
569 if ($result) {
570 // Clear cache
571 $this->clearAttributeCache();
572
573 // Log action
574 Logger::info("Force deleted attribute ID: {$id}");
575
576 // Fire action hook
577 do_action('yatra_attribute_force_deleted', $id);
578 }
579
580 return $result;
581
582 } catch (\Exception $e) {
583 Logger::error("Failed to force delete attribute {$id}: " . $e->getMessage());
584 throw $e;
585 }
586 }
587
588 /**
589 * Update attribute display orders
590 */
591 public function updateDisplayOrders(array $orders): bool
592 {
593 try {
594 if (empty($orders) || !is_array($orders)) {
595 throw new \InvalidArgumentException('Orders array is required');
596 }
597
598 $result = $this->attributeRepository->updateDisplayOrders($orders);
599
600 if ($result) {
601 // Clear cache
602 $this->clearAttributeCache();
603
604 // Log action
605 Logger::info('Updated attribute display orders');
606
607 // Fire action hook
608 do_action('yatra_attribute_orders_updated', $orders);
609 }
610
611 return $result;
612
613 } catch (\Exception $e) {
614 Logger::error("Failed to update attribute display orders: " . $e->getMessage());
615 throw $e;
616 }
617 }
618
619 /**
620 * Validate attribute value based on field type and rules
621 */
622 private function validateAttributeValue(\stdClass $attribute, $value): void
623 {
624 // Basic validation based on field type
625 switch ($attribute->field_type) {
626 case 'number':
627 if (!is_numeric($value)) {
628 throw new \InvalidArgumentException('Value must be a number');
629 }
630 break;
631
632 case 'email':
633 if (!empty($value) && !is_email($value)) {
634 throw new \InvalidArgumentException('Invalid email address');
635 }
636 break;
637
638 case 'url':
639 if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
640 throw new \InvalidArgumentException('Invalid URL');
641 }
642 break;
643
644 case 'date':
645 if (!empty($value) && !strtotime($value)) {
646 throw new \InvalidArgumentException('Invalid date format');
647 }
648 break;
649
650 case 'time':
651 if (!empty($value) && !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $value)) {
652 throw new \InvalidArgumentException('Invalid time format (HH:MM)');
653 }
654 break;
655
656 case 'select':
657 case 'radio':
658 $options = $attribute->field_options ? json_decode($attribute->field_options, true) : [];
659 if (!empty($value) && !in_array($value, array_column($options, 'value'))) {
660 throw new \InvalidArgumentException('Invalid option selected');
661 }
662 break;
663
664 case 'checkbox':
665 $checkboxVals = $value;
666 if (is_bool($value)) {
667 $checkboxVals = $value ? ['1'] : [];
668 } elseif (!is_array($value)) {
669 $checkboxVals = ($value !== '' && $value !== null) ? [(string) $value] : [];
670 }
671 $options = $attribute->field_options ? json_decode($attribute->field_options, true) : [];
672 if (!empty($checkboxVals) && is_array($options)) {
673 $validOptions = array_column($options, 'value');
674 foreach ($checkboxVals as $val) {
675 $match = false;
676 foreach ($validOptions as $vo) {
677 if ((string) $val === (string) $vo) {
678 $match = true;
679 break;
680 }
681 }
682 if (!$match) {
683 throw new \InvalidArgumentException('Invalid checkbox option: ' . $val);
684 }
685 }
686 }
687 break;
688 }
689
690 // Check if required field is empty
691 if ($attribute->required && (empty($value) || (is_string($value) && trim($value) === ''))) {
692 throw new \InvalidArgumentException('This attribute is required');
693 }
694
695 // Additional validation rules
696 if (!empty($attribute->validation_rules)) {
697 $rules = json_decode($attribute->validation_rules, true);
698 if (is_array($rules)) {
699 $this->applyValidationRules($value, $rules);
700 }
701 }
702 }
703
704 /**
705 * Apply custom validation rules
706 */
707 private function applyValidationRules($value, array $rules): void
708 {
709 foreach ($rules as $rule => $ruleValue) {
710 switch ($rule) {
711 case 'min_length':
712 if (strlen($value) < $ruleValue) {
713 throw new \InvalidArgumentException("Minimum length is {$ruleValue} characters");
714 }
715 break;
716
717 case 'max_length':
718 if (strlen($value) > $ruleValue) {
719 throw new \InvalidArgumentException("Maximum length is {$ruleValue} characters");
720 }
721 break;
722
723 case 'min_value':
724 if (is_numeric($value) && $value < $ruleValue) {
725 throw new \InvalidArgumentException("Minimum value is {$ruleValue}");
726 }
727 break;
728
729 case 'max_value':
730 if (is_numeric($value) && $value > $ruleValue) {
731 throw new \InvalidArgumentException("Maximum value is {$ruleValue}");
732 }
733 break;
734
735 case 'pattern':
736 if (!preg_match($ruleValue, $value)) {
737 throw new \InvalidArgumentException('Value format is invalid');
738 }
739 break;
740 }
741 }
742 }
743
744 /**
745 * Clear attribute-related cache
746 */
747 private function clearAttributeCache(): void
748 {
749 Cache::delete('yatra_frontend_attributes');
750 Cache::delete('yatra_filterable_attributes');
751 Cache::delete(\Yatra\Utils\Cache::KEY_AVAILABLE_ATTRIBUTES);
752
753 // Clear search cache by prefix
754 Cache::clearByPrefix('yatra_search_attributes_');
755 }
756
757 /**
758 * Clear trip attribute cache
759 */
760 private function clearTripAttributeCache(int $tripId): void
761 {
762 Cache::delete("yatra_trip_attributes_{$tripId}");
763 }
764
765 /**
766 * Bulk update trip attributes
767 */
768 public function bulkUpdateTripAttributes(int $tripId, array $attributes): bool
769 {
770 try {
771 if (!$tripId) {
772 return false;
773 }
774
775 $this->attributeRepository->validatePayloadCoversRequiredAttributes($attributes);
776
777 // Repository commits, busts trip attribute cache, fires yatra_trip_attributes_bulk_updated.
778 $success = $this->tripAttributeRepository->saveTripAttributes($tripId, $attributes);
779
780 return $success;
781
782 } catch (\InvalidArgumentException $e) {
783 throw $e;
784 } catch (\Exception $e) {
785 Logger::error("Failed to bulk update trip attributes: " . $e->getMessage());
786 return false;
787 }
788 }
789
790 /**
791 * Check if slug already exists
792 */
793 public function slugExists(string $slug, ?int $excludeId = null): bool
794 {
795 try {
796 return $this->attributeRepository->slugExists($slug, $excludeId);
797 } catch (\Exception $e) {
798 Logger::error('Error checking slug existence: ' . $e->getMessage());
799 return false;
800 }
801 }
802
803 /**
804 * Get status counts for attributes
805 */
806 public function getStatusCounts(): array
807 {
808 $counts = $this->attributeRepository->getStatusCounts();
809
810 $publish = $counts['publish'] ?? 0;
811 $draft = $counts['draft'] ?? 0;
812 $trash = $counts['trash'] ?? 0;
813
814 // Calculate total from all statuses, not just the main three
815 $all = array_sum(array_values($counts));
816
817 $result = [
818 'all' => (int) $all,
819 'publish' => (int) $publish,
820 'draft' => (int) $draft,
821 'trash' => (int) $trash,
822 ];
823
824 return $result;
825 }
826 }
827