| @@ -1,0 +1,125 @@ | ||
| 1 | +<?php | |
| 2 | + | |
| 3 | +declare(strict_types=1); | |
| 4 | + | |
| 5 | +namespace Metricool\Services\Analytics; | |
| 6 | + | |
| 7 | +use Metricool\Vendor\Carbon\Carbon; | |
| 8 | +use Metricool\Support\Helpers\Collection; | |
| 9 | +use Metricool\Http\Metricool\Entities\TimelineStatistics; | |
| 10 | + | |
| 11 | +class TrendService | |
| 12 | +{ | |
| 13 | + public const TREND_UP = 'up'; | |
| 14 | + public const TREND_DOWN = 'down'; | |
| 15 | + public const TREND_STABLE = 'stable'; | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * Method returns the trend for the given statistic module based on the | |
| 19 | + * given filters. To be able to calculate the trend the filters need | |
| 20 | + * at least a start and an end date in Ymd format. Otherwise, a | |
| 21 | + * 'stable' trend is returned | |
| 22 | + * | |
| 23 | + * @param array $filters Optional filters to override the filters used on | |
| 24 | + * the TimelineStatistics instance. Must contain 'start' and 'end' keys | |
| 25 | + * in Ymd format. | |
| 26 | + */ | |
| 27 | + public function getTrend(TimelineStatistics $statistic, Collection $currentStatistics, array $filters = []): string | |
| 28 | + { | |
| 29 | + $cacheName = get_class($statistic) . ':' . $statistic->getMetric() . '#' . md5(json_encode($filters)); | |
| 30 | + $cacheValue = wp_cache_get($cacheName, 'metricool', false, $found); | |
| 31 | + if ($found && is_string($cacheValue)) { | |
| 32 | + return $cacheValue; | |
| 33 | + } | |
| 34 | + | |
| 35 | + try { | |
| 36 | + $filters = $this->getCurrentPeriodFilters($filters, $statistic); | |
| 37 | + $previousStatistics = $statistic->filter( | |
| 38 | + $this->getPreviousPeriodFilters($filters) | |
| 39 | + )->get(); | |
| 40 | + } catch (\Throwable $e) { | |
| 41 | + return self::TREND_STABLE; | |
| 42 | + } | |
| 43 | + | |
| 44 | + $trend = $this->calculateTrendFromPeriods($currentStatistics, $previousStatistics); | |
| 45 | + | |
| 46 | + wp_cache_set($cacheName, $trend, 'metricool', (5 * MINUTE_IN_SECONDS)); | |
| 47 | + return $trend; | |
| 48 | + } | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * Method returns filters for the previous period based on the given | |
| 52 | + * filters. A period is defined as the difference between start and | |
| 53 | + * end date. | |
| 54 | + * | |
| 55 | + * @param array $filters Must contain 'start' and 'end' keys and should | |
| 56 | + * reflect the current period to calculate the previous period from. | |
| 57 | + * | |
| 58 | + * @throws \InvalidArgumentException When start or end filters are missing | |
| 59 | + */ | |
| 60 | + public function getPreviousPeriodFilters(array $filters): array | |
| 61 | + { | |
| 62 | + if (empty($filters) || empty($filters['start']) || empty($filters['end'])) { | |
| 63 | + throw new \InvalidArgumentException("Filters 'start' and 'end' are required to get the previous period"); | |
| 64 | + } | |
| 65 | + | |
| 66 | + $start = Carbon::createFromFormat('Ymd', $filters['start']); | |
| 67 | + $end = Carbon::createFromFormat('Ymd', $filters['end']); | |
| 68 | + $diffInDays = $start->diffInDays($end); | |
| 69 | + | |
| 70 | + // Previous end is one day before current start | |
| 71 | + $previousEnd = $start->copy()->subDay(); | |
| 72 | + $previousStart = $previousEnd->copy()->subDays($diffInDays); | |
| 73 | + | |
| 74 | + return [ | |
| 75 | + 'start' => $previousStart->format('Ymd'), | |
| 76 | + 'end' => $previousEnd->format('Ymd'), | |
| 77 | + ]; | |
| 78 | + } | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * Method is used to calculate the trend based on the sums of the "amount" | |
| 82 | + * key from the two given periods. | |
| 83 | + * | |
| 84 | + * @param Collection $currentPeriod Statistics for the current period | |
| 85 | + * @param Collection $previousPeriod Statistics for the previous period | |
| 86 | + * | |
| 87 | + * @return string One of the TREND_* constants | |
| 88 | + */ | |
| 89 | + private function calculateTrendFromPeriods(Collection $currentPeriod, Collection $previousPeriod): string | |
| 90 | + { | |
| 91 | + $statisticSumCurrentPeriod = $currentPeriod->sum('amount'); | |
| 92 | + $statisticSumPreviousPeriod = $previousPeriod->sum('amount'); | |
| 93 | + | |
| 94 | + if ($statisticSumCurrentPeriod > $statisticSumPreviousPeriod) { | |
| 95 | + return self::TREND_UP; | |
| 96 | + } | |
| 97 | + | |
| 98 | + if ($statisticSumCurrentPeriod < $statisticSumPreviousPeriod) { | |
| 99 | + return self::TREND_DOWN; | |
| 100 | + } | |
| 101 | + | |
| 102 | + return self::TREND_STABLE; | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * Returns the used 'start' and 'end' filters for the given statistic. Uses | |
| 107 | + * the given filters first, and if they are missing, uses the filters from | |
| 108 | + * the used statistic instance. | |
| 109 | + * | |
| 110 | + * @throws \InvalidArgumentException When start or end filters are missing | |
| 111 | + * even when using the statistic's filters. | |
| 112 | + */ | |
| 113 | + private function getCurrentPeriodFilters(array $filters, TimelineStatistics $statistic): array | |
| 114 | + { | |
| 115 | + if (empty($filters['start']) || empty($filters['end'])) { | |
| 116 | + $filters = $statistic->getFilters(); | |
| 117 | + } | |
| 118 | + | |
| 119 | + if (empty($filters) || empty($filters['start']) || empty($filters['end'])) { | |
| 120 | + throw new \InvalidArgumentException("Filters 'start' and 'end' are required to process statistic filters"); | |
| 121 | + } | |
| 122 | + | |
| 123 | + return $filters; | |
| 124 | + } | |
| 125 | +} | |