PluginProbe ʕ •ᴥ•ʔ
FAPI Member / trunk
FAPI Member vtrunk
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
AdminMenuService.php 2 years ago ApiService.php 1 day ago ElementService.php 7 months ago EmailService.php 1 day ago FormService.php 1 year ago LevelOrderService.php 1 year ago LevelService.php 1 year ago MembershipService.php 1 year ago RedirectService.php 1 year ago SanitizationService.php 2 years ago StatisticsService.php 1 year ago UserService.php 1 year 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