Passed
Push — master ( 89fe32...89349c )
by
unknown
09:50
created

ensureDaysLeftHydrated()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\State;
8
9
use ApiPlatform\Doctrine\Orm\Extension\PaginationExtension;
10
use ApiPlatform\Doctrine\Orm\Paginator;
11
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
12
use ApiPlatform\Metadata\Operation;
13
use ApiPlatform\State\Pagination\TraversablePaginator;
14
use ApiPlatform\State\ProviderInterface;
15
use Chamilo\CoreBundle\Entity\AccessUrl;
16
use Chamilo\CoreBundle\Entity\Session;
17
use Chamilo\CoreBundle\Entity\User;
18
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
19
use Chamilo\CoreBundle\Helpers\UserHelper;
20
use Chamilo\CoreBundle\Repository\Node\UserRepository;
21
use Chamilo\CoreBundle\Repository\SessionRepository;
22
use DateTime;
23
use Exception;
24
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
25
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
26
27
/**
28
 * @template-implements ProviderInterface<Session>
29
 */
30
class UserSessionSubscriptionsStateProvider implements ProviderInterface
31
{
32
    public function __construct(
33
        private readonly UserHelper $userHelper,
34
        private readonly AccessUrlHelper $accessUrlHelper,
35
        private readonly UserRepository $userRepository,
36
        private readonly SessionRepository $sessionRepository,
37
        private readonly PaginationExtension $paginationExtension,
38
    ) {}
39
40
    /**
41
     * @throws Exception
42
     */
43
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable
44
    {
45
        /** @var User|null $user */
46
        $user = $this->userRepository->find($uriVariables['id'] ?? null);
47
48
        if (!$user) {
49
            throw new NotFoundHttpException('User not found');
50
        }
51
52
        $currentUser = $this->userHelper->getCurrent();
53
54
        $isAllowed = $user === $currentUser || ($currentUser && $currentUser->isAdmin());
55
        if (!$isAllowed) {
56
            throw new AccessDeniedException();
57
        }
58
59
        $url = $this->accessUrlHelper->getCurrent() ?? $this->accessUrlHelper->getFirstAccessUrl();
60
        if (!$url instanceof AccessUrl) {
61
            throw new \RuntimeException('Access URL not found');
62
        }
63
64
        if ('user_session_subscriptions_past' === $operation->getName()) {
65
            $sessions = $this->sessionRepository->getPastSessionsOfUserInUrl($user, $url);
66
67
            // Ensure duration sessions have daysLeft for display consistency
68
            foreach ($sessions as $session) {
69
                $this->ensureDaysLeftHydrated($session, $user);
70
            }
71
72
            return $sessions;
73
        }
74
75
        if ('user_session_subscriptions_current' === $operation->getName()) {
76
            return $this->getCurrentSessionsPagedAndFiltered($context, $user, $url);
77
        }
78
79
        // Upcoming can stay as a pure DB filter (duration sessions won't be upcoming anyway)
80
        $qb = $this->sessionRepository->getUpcomingSessionsOfUserInUrl($user, $url);
81
82
        $this->paginationExtension->applyToCollection(
83
            $qb,
84
            new QueryNameGenerator(),
85
            Session::class,
86
            $operation,
87
            $context
88
        );
89
90
        $paginator = $this->paginationExtension->getResult($qb, Session::class, $operation, $context);
91
92
        if ($paginator instanceof Paginator) {
93
            $sessions = iterator_to_array($paginator);
94
            foreach ($sessions as $session) {
95
                $this->ensureDaysLeftHydrated($session, $user);
96
            }
97
98
            return new TraversablePaginator(
99
                new \ArrayIterator($sessions),
100
                (int) ($context['filters']['page'] ?? 1),
101
                (int) ($context['filters']['itemsPerPage'] ?? $context['pagination_items_per_page'] ?? 10),
102
                $paginator->getTotalItems()
103
            );
104
        }
105
106
        if (is_iterable($paginator)) {
107
            $sessions = \is_array($paginator) ? $paginator : iterator_to_array($paginator);
108
            foreach ($sessions as $session) {
109
                $this->ensureDaysLeftHydrated($session, $user);
110
            }
111
112
            return $sessions;
113
        }
114
115
        return [];
116
    }
117
118
    /**
119
     * We must filter expired duration sessions in PHP (depends on user and "daysLeft").
120
     * To keep pagination correct, we build the page slice AFTER filtering and return
121
     * a TraversablePaginator with the real total count.
122
     */
123
    private function getCurrentSessionsPagedAndFiltered(array $context, User $user, AccessUrl $url): TraversablePaginator
124
    {
125
        $filters = $context['filters'] ?? [];
126
127
        $page = (int) ($filters['page'] ?? 1);
128
        if ($page < 1) {
129
            $page = 1;
130
        }
131
132
        $itemsPerPage = (int) (
133
            $filters['itemsPerPage']
134
            ?? $context['pagination_items_per_page']
135
            ?? 10
136
        );
137
        if ($itemsPerPage < 1) {
138
            $itemsPerPage = 10;
139
        }
140
141
        $wantedOffset = ($page - 1) * $itemsPerPage;
142
        $wantedEnd = $wantedOffset + $itemsPerPage;
143
144
        // Base query: current sessions candidates (includes duration sessions)
145
        $baseQb = $this->sessionRepository->getCurrentSessionsOfUserInUrl($user, $url);
146
147
        $scanOffset = 0;
148
        $scanSize = max($itemsPerPage * 5, 50);
149
150
        $pageItems = [];
151
        $totalAccepted = 0;
152
153
        while (true) {
154
            $qb = clone $baseQb;
155
            $qb->setFirstResult($scanOffset);
156
            $qb->setMaxResults($scanSize);
157
158
            /** @var Session[] $chunk */
159
            $chunk = $qb->getQuery()->getResult();
160
            if (empty($chunk)) {
161
                break;
162
            }
163
164
            foreach ($chunk as $session) {
165
                // Always hydrate daysLeft for duration sessions (for UI display)
166
                $this->ensureDaysLeftHydrated($session, $user);
167
168
                // Hide expired duration sessions for non-coaches
169
                if ($session->getDuration() > 0 && !$session->hasCoach($user)) {
170
                    $daysLeft = $session->getDaysLeft();
171
172
                    if (null !== $daysLeft && $daysLeft < 0) {
173
                        continue;
174
                    }
175
                }
176
177
                // Accepted session after filtering
178
                if ($totalAccepted >= $wantedOffset && $totalAccepted < $wantedEnd) {
179
                    $pageItems[] = $session;
180
                }
181
182
                $totalAccepted++;
183
            }
184
185
            $scanOffset += $scanSize;
186
        }
187
188
        return new TraversablePaginator(
189
            new \ArrayIterator($pageItems),
190
            $page,
191
            $itemsPerPage,
192
            $totalAccepted
193
        );
194
    }
195
196
    /**
197
     * Ensures duration sessions have daysLeft for the given user.
198
     */
199
    private function ensureDaysLeftHydrated(Session $session, User $user): void
200
    {
201
        if ($session->getDuration() <= 0) {
202
            return;
203
        }
204
205
        if (null !== $session->getDaysLeft()) {
206
            return;
207
        }
208
209
        $session->setDaysLeft($session->getDaysLeftByUser($user));
210
    }
211
}
212