PluginProbe ʕ •ᴥ•ʔ
FAPI Member / 2.2.24
FAPI Member v2.2.24
2.2.33 2.2.32 trunk 1.9.47 2.1.18 2.2.24 2.2.25 2.2.26 2.2.28 2.2.29 2.2.30 2.2.31
fapi-member / src / Service / StatisticsService.php
fapi-member / src / Service Last commit date
ApiService.php 11 months ago ElementService.php 11 months ago EmailService.php 11 months ago FormService.php 11 months ago LevelOrderService.php 11 months ago LevelService.php 11 months ago MembershipService.php 11 months ago RedirectService.php 11 months ago SanitizationService.php 11 months ago StatisticsService.php 11 months ago UserService.php 11 months ago
StatisticsService.php
427 lines
1 <?php
2
3 namespace FapiMember\Service;
4
5 use DateTimeImmutable;
6 use FapiMember\Container\Container;
7 use FapiMember\Model\Enums\Format;
8 use FapiMember\Model\Enums\Types\MembershipChangeType;
9 use FapiMember\Model\Membership;
10 use FapiMember\Model\MembershipChange;
11 use FapiMember\Repository\LevelRepository;
12 use FapiMember\Repository\MemberActivityRepository;
13 use FapiMember\Repository\MembershipChangeRepository;
14 use FapiMember\Repository\UserRepository;
15 use FapiMember\Utils\DateTimeHelper;
16
17 class StatisticsService
18 {
19 private MembershipChangeRepository $membershipChangeRepository;
20 private MemberActivityRepository $memberActivityRepository;
21 private LevelRepository $levelRepository;
22 private UserRepository $userRepository;
23
24 public function __construct()
25 {
26 $this->membershipChangeRepository = Container::get(MembershipChangeRepository::class);
27 $this->memberActivityRepository = Container::get(MemberActivityRepository::class);
28 $this->levelRepository = Container::get(LevelRepository::class);
29 $this->userRepository = Container::get(UserRepository::class);
30 }
31
32 /**
33 * @param array<Membership> $oldMemberships
34 * @param array<Membership> $newMemberships
35 */
36 public function saveChanges(array $oldMemberships, array $newMemberships): void
37 {
38 $missingMemberships = $oldMemberships;
39 $createdMemberships = $newMemberships;
40
41 foreach ($oldMemberships as $oldKey => $oldMembership) {
42 foreach ($newMemberships as $newKey => $newMembership) {
43 if ($oldMembership->getLevelId() === $newMembership->getLevelId()) {
44 unset($missingMemberships[$oldKey]);
45 unset($createdMemberships[$newKey]);
46
47 if ($oldMembership->getUntil() !== null &&
48 ($newMembership->getUntil() === null ||
49 $oldMembership->getUntil() < $newMembership->getUntil())
50 ) {
51 $this->membershipChangeRepository->addChange(
52 $newMembership->toMembershipChange(MembershipChangeType::EXTENDED),
53 );
54 } elseif (
55 $oldMembership->getUntil()?->format(Format::DATE_TIME) !== $newMembership->getUntil()?->format(Format::DATE_TIME) ||
56 $oldMembership->getRegistered()->format(Format::DATE_TIME) !== $newMembership->getRegistered()->format(Format::DATE_TIME)
57 ) {
58 $this->membershipChangeRepository->addChange(
59 $newMembership->toMembershipChange(MembershipChangeType::UPDATED),
60 );
61 }
62 }
63 }
64 }
65
66 foreach ($missingMemberships as $membership) {
67 if ($membership->getUntil() !== null && $membership->getUntil() < DateTimeHelper::getNow()) {
68 $this->membershipChangeRepository->addChange(
69 $membership->toMembershipChange(MembershipChangeType::EXPIRED),
70 );
71 } else {
72 $this->membershipChangeRepository->addChange(
73 $membership->toMembershipChange(MembershipChangeType::DELETED),
74 );
75 }
76 }
77
78 foreach ($createdMemberships as $membership) {
79 $this->membershipChangeRepository->addChange(
80 $membership->toMembershipChange(MembershipChangeType::CREATED),
81 );
82 }
83 }
84
85 public function getMemberCountsForPeriod(
86 DateTimeImmutable $dateFrom,
87 DateTimeImmutable $dateTo,
88 array $levelIds,
89 bool $groupLevels,
90 ): array
91 {
92 $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo);
93 $data = [];
94
95 foreach ($periods as $period) {
96 $dateString = $period['date_to']->format(Format::DATE_CZECH);
97
98 if ($groupLevels) {
99 $data[$dateString]['Počet'] = 0;
100 }
101
102 $lastChanges = $this->membershipChangeRepository->getLastChangesForLevels(
103 $levelIds,
104 $period['date_to'],
105 );
106
107 $counts = $this->calculateActiveMembershipCounts($lastChanges, $levelIds);
108
109 foreach ($counts as $levelId => $count) {
110 if ($groupLevels) {
111 $data[$dateString]['Počet'] += $count;
112 } else {
113 $data[$dateString][$this->levelRepository->getLevelById($levelId)->getName()]
114 = $count;
115 }
116 }
117 }
118
119 return $data;
120 }
121
122 public function getMemberCountChangesForPeriod(
123 DateTimeImmutable $dateFrom,
124 DateTimeImmutable $dateTo,
125 array $levelIds,
126 bool $groupLevels,
127 ): array
128 {
129 $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo);
130 $data = [];
131
132 foreach ($periods as $period) {
133 $dateString = $period['date_to']->format(Format::DATE_CZECH);
134
135 if ($groupLevels) {
136 $data[$dateString]['+'] = 0;
137 $data[$dateString]['-'] = 0;
138 }
139
140 $changesBefore = $this->membershipChangeRepository->getLastChangesForLevels(
141 $levelIds,
142 $period['date_from'],
143 );
144
145 $changesAfter = $this->membershipChangeRepository->getLastChangesForLevels(
146 $levelIds,
147 $period['date_to'],
148 );
149
150 $lostCounts = $this->calculateActiveMembershipChangedCounts(
151 $changesBefore,
152 $changesAfter,
153 $levelIds,
154 true,
155 );
156
157 $gainedCounts = $this->calculateActiveMembershipChangedCounts(
158 $changesBefore,
159 $changesAfter,
160 $levelIds,
161 );
162
163 foreach ($lostCounts as $levelId => $count) {
164 if ($groupLevels) {
165 $data[$dateString]['-'] += $count;
166 } else {
167 $data
168 [$dateString]
169 ['- ' . $this->levelRepository->getLevelById($levelId)->getName()]
170 = $count;
171 }
172 }
173
174 foreach ($gainedCounts as $levelId => $count) {
175 if ($groupLevels) {
176 $data[$dateString]['+'] += $count;
177 } else {
178 $data
179 [$dateString]
180 ['+ ' . $this->levelRepository->getLevelById($levelId)->getName()]
181 = $count;
182 }
183 }
184
185 }
186
187 return $data;
188 }
189
190 public function getAcquisitionOrChurnRate(
191 DateTimeImmutable $dateFrom,
192 DateTimeImmutable $dateTo,
193 array $levelIds,
194 bool $groupLevels,
195 bool $calculateChurn = true,
196 ): array
197 {
198 $data = [];
199 $arrayKey = $calculateChurn ? 'Churn rate' : 'Acquisition rate';
200
201 $changesBefore = $this->membershipChangeRepository->getLastChangesForLevels(
202 $levelIds,
203 $dateFrom,
204 );
205
206 $changesAfter = $this->membershipChangeRepository->getLastChangesForLevels(
207 $levelIds,
208 $dateTo,
209 );
210
211 $changedCounts = $this->calculateActiveMembershipChangedCounts(
212 $changesBefore,
213 $changesAfter,
214 $levelIds,
215 $calculateChurn,
216 );
217
218 $activeCounts = $this->calculateActiveMembershipCounts($changesBefore, $levelIds);
219
220 $totalActiveCount = 0;
221 $totalChangedCount = 0;
222
223 foreach ($changedCounts as $levelId => $changedCount) {
224 if (!isset($activeCounts[$levelId]) || $activeCounts[$levelId] === 0) {
225 continue;
226 }
227
228 if ($groupLevels) {
229 $totalActiveCount += $activeCounts[$levelId];
230 $totalChangedCount += abs($changedCount);
231 } else {
232 $levelName = $this->levelRepository->getLevelById($levelId)->getName();
233
234 $data[$levelName] = [
235 $levelName => number_format((abs($changedCount) / $activeCounts[$levelId]) * 100, 2),
236 ];
237 }
238 }
239
240 if ($groupLevels && $totalActiveCount !== 0) {
241 $data[$arrayKey][$arrayKey] = number_format((abs($totalChangedCount) / $totalActiveCount) * 100, 2);
242 } elseif ($groupLevels) {
243 $data[$arrayKey][$arrayKey] = 0;
244 }
245
246 return $data;
247 }
248
249 public function getActiveCountsForPeriod(
250 DateTimeImmutable $dateFrom,
251 DateTimeImmutable $dateTo,
252 ): array
253 {
254 $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo);
255 $data = [];
256
257 foreach ($periods as $period) {
258 $dateString = $period['date_to']->format(Format::DATE_CZECH);
259
260 $data[$dateString]['Aktivních'] = count($this->memberActivityRepository->getAllForPeriod($period['date_from'], $period['date_to']));
261 }
262
263 return $data;
264 }
265
266 public function getAverageChurnRatePeriodsForLevels(
267 bool $groupLevels,
268 array $levelIds,
269 ): array
270 {
271 $firstCreatedChanges = $this->membershipChangeRepository
272 ->getFirstCreatedForLevels($levelIds);
273
274 $totalCount = count($firstCreatedChanges);
275
276 if ($totalCount === 0) {
277 return [];
278 }
279
280 $graphColumns = [
281 'Měsíc' => '+1 month',
282 '2 Měsíce' => '+2 months',
283 '3 Měsíce' => '+3 months',
284 '4 Měsíce' => '+4 months',
285 '5 Měsíců' => '+5 months',
286 'Půl roku' => '+6 months',
287 'Rok' => '+1 year',
288 '2 Roky' => '+2 years',
289 ];
290
291 $churnedOutCounts = [];
292
293 foreach ($firstCreatedChanges as $firstCreatedChange) {
294 $startDate = $firstCreatedChange->getTimestamp();
295 $levelKey = $groupLevels
296 ? 'Churn rate'
297 : $this->levelRepository
298 ->getLevelById($firstCreatedChange->getLevelId())
299 ?->getName();
300
301 foreach ($graphColumns as $key => $value) {
302 if (!isset($churnedOutCounts[$key][$levelKey])) {
303 $churnedOutCounts[$key][$levelKey] = 0;
304 }
305
306 $endDate = $startDate->modify($value)->modify('-1 day');
307
308 if ($endDate > DateTimeHelper::getNow()) {
309 break;
310 }
311
312 $lastChange = $this->membershipChangeRepository->findLastChange(
313 $firstCreatedChange->getUserId(),
314 $firstCreatedChange->getLevelId(),
315 $endDate,
316 );
317
318 if ($firstCreatedChange->isActive() && !$lastChange->isActive()) {
319 $churnedOutCounts[$key][$levelKey]++;
320 break;
321 }
322 }
323 }
324
325 $churnedOutRates = [];
326
327 foreach ($churnedOutCounts as $key => $churnedOutPeriod) {
328 foreach ($churnedOutPeriod as $levelKey => $churnedOutLevel) {
329 $churnedOutRates[$key][$levelKey] = number_format(($churnedOutLevel) / $totalCount * 100, 2);
330 }
331 }
332
333 return $churnedOutRates;
334 }
335
336 /** @param array<MembershipChange> $changes */
337 private function calculateActiveMembershipCounts(array $changes, array $levelIds): array
338 {
339 $data = [];
340
341 $levels = $this->levelRepository->getAllAsLevels();
342
343 foreach ($levels as $level) {
344 if (in_array($level->getId(), $levelIds) || $levelIds === []) {
345 $data[$level->getId()] = 0;
346 }
347 }
348
349 foreach ($changes as $change) {
350 if (!isset($data[$change->getLevelId()])) {
351 $data[$change->getLevelId()] = 0;
352 }
353
354 if ($change->isActive()) {
355 $data[$change->getLevelId()]++;
356 }
357 }
358
359 return $data;
360 }
361
362 /**
363 * @param array<MembershipChange> $changesBefore
364 * @param array<MembershipChange> $changesAfter
365 */
366 private function calculateActiveMembershipChangedCounts(
367 array $changesBefore,
368 array $changesAfter,
369 array $levelIds,
370 bool $countLost = false,
371 ): array
372 {
373 $data = [];
374
375 $levels = $this->levelRepository->getAllAsLevels();
376
377 foreach ($levels as $level) {
378 if (in_array($level->getId(), $levelIds) || $levelIds === []) {
379 $data[$level->getId()] = 0;
380 }
381 }
382
383 foreach ($changesAfter as $index => $change) {
384 $activeBefore = isset($changesBefore[$index]) && $changesBefore[$index]->isActive();
385
386 $activeAfter = $change->isActive();
387
388 $changed = $countLost
389 ? ($activeBefore && !$activeAfter)
390 : (!$activeBefore && $activeAfter);
391
392
393 if ($changed) {
394 $data[$change->getLevelId()] += $countLost ? -1 : 1;
395 }
396 }
397
398 return $data;
399 }
400
401 public function handleUserActive(): void
402 {
403 $user = $this->userRepository->getCurrentUser();
404 if ($user === null || !$user->isMemberOrSubscriber() || !$this->memberActivityRepository->tableExists()) {
405 return;
406 }
407
408 $now = DateTimeHelper::getNow();
409
410 $dayStart = $now->setTime(0, 0, 0);
411 $dayEnd = $now->setTime(23, 59, 59);
412
413 $activityToday = $this->memberActivityRepository->getOneForPeriod(
414 $user->getId(),
415 $dayStart,
416 $dayEnd,
417 );
418
419 if (count($activityToday) < 1) {
420 $this->memberActivityRepository->addActivity($user->getId());
421 } else {
422 $this->memberActivityRepository->updateActivity($user->getId());
423 }
424 }
425
426 }
427