Lines 82.36% 369 / 448
Methods 62.79% 27 / 43
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 readByAppointment 100.00% 10 / 10 100.00% 1 / 1 7
 readByAvailability 93.33% 14 / 15 0.00% 0 / 1 3.00
 lockSlotsForAppointment 91.66% 22 / 24 0.00% 0 / 1 4.01
 hasScopeRelevantChanges 86.66% 13 / 15 0.00% 0 / 1 5.06
 isAvailabilityOutdated 93.47% 43 / 46 0.00% 0 / 1 18.09
 writeByAvailability 77.77% 42 / 54 0.00% 0 / 1 20.17
 writeByScope 0.00% 0 / 10 0.00% 0 / 1 12
 writeSlotListForDate 91.30% 21 / 23 0.00% 0 / 1 7.03
 writeAncestorIDs 100.00% 12 / 12 100.00% 1 / 1 3
 readLastChangedTime 83.33% 5 / 6 0.00% 0 / 1 2.02
 readLastChangedTimeByScope 0.00% 0 / 9 0.00% 0 / 1 6
 readLastChangedTimeByAvailability 88.88% 8 / 9 0.00% 0 / 1 2.01
 updateSlotProcessMapping 0.00% 0 / 10 0.00% 0 / 1 12
 deleteSlotProcessOnSlot 0.00% 0 / 7 0.00% 0 / 1 6
 deleteSlotProcessOnProcess 0.00% 0 / 10 0.00% 0 / 1 12
 writeSlotProcessMappingFor 100.00% 4 / 4 100.00% 1 / 1 1
 deleteSlotProcessMappingFor 100.00% 4 / 4 100.00% 1 / 1 1
 writeCanceledByTimeAndScope 100.00% 11 / 11 100.00% 1 / 1 2
 writeCanceledByTime 100.00% 9 / 9 100.00% 1 / 1 2
 deleteSlotsOlderThan 100.00% 7 / 7 100.00% 1 / 1 2
 readRowsByScopeAndDate 100.00% 7 / 7 100.00% 1 / 1 1
 writeOptimizedSlotTables 76.92% 10 / 13 0.00% 0 / 1 4.20
 getLastGeneratedSlotDate 88.88% 8 / 9 0.00% 0 / 1 2.01
 getOldestSlotVersionByAvailability 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] __construct 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] init 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] getWriter 100.00% 4 / 4 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] getReader 100.00% 5 / 5 100.00% 1 / 1 3
 [BO\Zmsbackend\Base] fetchPreparedStatement 100.00% 9 / 9 100.00% 1 / 1 3
 [BO\Zmsbackend\Base] startExecute 100.00% 5 / 5 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] pdoExceptionHandler 100.00% 13 / 13 100.00% 1 / 1 4
 [BO\Zmsbackend\Base] fetchStatement 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchOne 100.00% 7 / 7 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] fetchRow 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchValue 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchAll 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchHandle 100.00% 5 / 5 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchList 100.00% 8 / 8 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] writeItem 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] deleteItem 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] perform 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchAffected 100.00% 8 / 8 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] readResolvedReferences n/a 0 / 0 n/a 0 / 0 1
 [BO\Zmsbackend\Base] hashStringValue 50.00% 5 / 10 0.00% 0 / 1 8.12
16class Slot extends \BO\Zmsbackend\Base
17{
18    /**
19     * maximum number of slots per appointment
20     */
21    const int MAX_SLOTS = 25;
22
23    const int MAX_DAYS_OF_SLOT_CALCULATION = 180;
24
25    /**
26     * @return \BO\Zmsentities\Collection\SlotList
27     *
28     */
29    public function readByAppointment(
30        \BO\Zmsentities\Appointment $appointment,
31        int|null $overwriteSlotsCount = null,
32        bool $extendSlotList = false,
33        bool $lockSlots = false
34    ) {
35        $appointment = clone $appointment;
36        $availability = (new \BO\Zmsbackend\Availability\Service\Availability())->readByAppointment($appointment);
37        // Check if availability allows multiple slots, but allow overwrite
38        if (!$availability->multipleSlotsAllowed || $overwriteSlotsCount >= 1) {
39            $appointment->slotCount = ($overwriteSlotsCount >= 1) ? $overwriteSlotsCount : 1;
40        }
41        $slotList = $availability->getSlotList()->withSlotsForAppointment($appointment, $extendSlotList);
42        if ($lockSlots && count($slotList) > 0) {
43            // One FOR UPDATE for all slots avoids row-by-row lock interleaving (deadlocks with calculateSlots).
44            $this->lockSlotsForAppointment($slotList, $availability, $appointment->toDateTime());
45        } else {
46            foreach ($slotList as $slot) {
47                $this->readByAvailability($slot, $availability, $appointment->toDateTime(), false);
48            }
49        }
50        return $slotList;
51    }
52
53    public function readByAvailability(
54        \BO\Zmsentities\Slot $slot,
55        AvailabilityEntity $availability,
56        \DateTimeInterface $date,
57        bool $getLock = false
58    ) {
59        $data = array();
60        $data['scopeID'] = $availability['scope']['id'];
61        $data['availabilityID'] = $availability['id'];
62        $data['year'] = $date->format('Y');
63        $data['month'] = $date->format('m');
64        $data['day'] = $date->format('d');
65        $data['time'] = $slot->getTimeString();
66        $sql = \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_SLOT;
67        if ($getLock) {
68            $sql .= ' FOR UPDATE';
69        }
70        $slotID = $this->fetchRow(
71            $sql,
72            $data
73        );
74        return $slotID ? $slotID['slotID'] : false ;
75    }
76
77    /**
78     * Lock all slots required for an appointment in a single SELECT ... FOR UPDATE.
79     * Uses ORDER BY slotID ASC so lock acquisition prefers primary-key order.
80     */
81    protected function lockSlotsForAppointment(
82        Collection $slotList,
83        AvailabilityEntity $availability,
84        \DateTimeInterface $date
85    ): void {
86        $times = [];
87        foreach ($slotList as $slot) {
88            if (!$slot instanceof Entity) {
89                continue;
90            }
91            $times[] = $slot->getTimeString();
92        }
93        $times = array_values(array_unique($times));
94        if ($times === []) {
95            return;
96        }
97
98        $placeholders = implode(',', array_fill(0, count($times), '?'));
99        $sql = sprintf(
100            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_SLOTS_FOR_UPDATE,
101            $placeholders
102        );
103        $params = array_merge(
104            [
105                $availability['scope']['id'],
106                $availability['id'],
107                $date->format('Y'),
108                $date->format('m'),
109                $date->format('d'),
110            ],
111            $times
112        );
113        $this->fetchAll($sql, $params);
114    }
115
116    /**
117     * @return null|true
118     */
119    public function hasScopeRelevantChanges(
120        \BO\Zmsentities\Scope $scope,
121        ?\DateTimeInterface $slotLastChange = null
122    ) {
123        $startInDaysDefault = (new \BO\Zmsbackend\Preferences\Service\Preferences())
124            ->readProperty('scope', $scope->id, 'appointment', 'startInDaysDefault');
125        $endInDaysDefault = (new \BO\Zmsbackend\Preferences\Service\Preferences())
126            ->readProperty('scope', $scope->id, 'appointment', 'endInDaysDefault');
127        if (
128            $scope->preferences['appointment']['startInDaysDefault'] != $startInDaysDefault
129            || $scope->preferences['appointment']['endInDaysDefault'] != $endInDaysDefault
130        ) {
131            (new \BO\Zmsbackend\Scope\Service\Scope())->replacePreferences($scope); //TODO remove after ZMS1 is deactivated
132            return true;
133        }
134        $startInDaysChange = (new \BO\Zmsbackend\Preferences\Service\Preferences())
135            ->readChangeDateTime('scope', $scope->id, 'appointment', 'startInDaysDefault');
136        $endInDaysChange = (new \BO\Zmsbackend\Preferences\Service\Preferences())
137            ->readChangeDateTime('scope', $scope->id, 'appointment', 'endInDaysDefault');
138        if (
139            $startInDaysChange->getTimestamp() > $slotLastChange->getTimestamp()
140            || $endInDaysChange->getTimestamp() > $slotLastChange->getTimestamp()
141        ) {
142            return true;
143        }
144    }
145
146    public function isAvailabilityOutdated(
147        \BO\Zmsentities\Availability $availability,
148        \DateTimeInterface $now,
149        ?\DateTimeInterface $slotLastChange = null,
150        int $oldestSlotVersion = 1
151    ): bool {
152        $proposedChange = new \BO\Zmsbackend\Helper\AvailabilitySnapShot($availability, $now);
153        $formerChange = new \BO\Zmsbackend\Helper\AvailabilitySnapShot($availability, $slotLastChange);
154
155        if ($availability->version > $oldestSlotVersion) {
156            $availability['processingNote'][] = 'outdated: availability version change';
157            return true;
158        }
159
160        if ($formerChange->hasOutdatedAvailability()) {
161            $availability['processingNote'][] = 'outdated: availability change';
162            return true;
163        }
164        if (
165            $formerChange->hasOutdatedScope()
166            && $this->hasScopeRelevantChanges($availability->scope, $slotLastChange)
167        ) {
168            $availability['processingNote'][] = 'outdated: scope change';
169            return true;
170        }
171        if ($formerChange->hasOutdatedDayoff()) {
172            $availability['processingNote'][] = 'outdated: dayoff change';
173            return true;
174        }
175        // Be aware, that last slot change and current time might differ serval days
176        //  if the rebuild fails in some way
177        if (
178            1
179            // First check if the bookable end date on current time was already calculated on last slot change
180            && !$formerChange->hasBookableDateTime($proposedChange->getLastBookableDateTime())
181            // Second check if between last slot change and current time could be a bookable slot
182            && (
183                (
184                    !$formerChange->isOpenedOnLastBookableDay()
185                    && $proposedChange->hasBookableDateTimeAfter($formerChange->getLastBookableDateTime())
186                )
187                // if calculation already happened the day before, check if lastChange time was before opening
188                || (
189                    $formerChange->isOpenedOnLastBookableDay()
190                    && (
191                        !$formerChange->isTimeOpenedOnLastBookableDay()
192                        || $proposedChange->hasBookableDateTimeAfter(
193                            $formerChange->getLastBookableDateTime()->modify('+1day 00:00:00')
194                        )
195                    )
196                )
197            )
198            // Check if daytime is after booking start time if bookable end of now is calculated
199            && (
200                !$proposedChange->isOpenedOnLastBookableDay()
201                || $proposedChange->isTimeOpenedOnLastBookableDay()
202            )
203        ) {
204            $availability['processingNote'][] = 'outdated: new slots required';
205            return true;
206        }
207        if (
208            $availability->getBookableStart($slotLastChange) != $availability->getBookableStart($now)
209            // First check, if bookable start from lastChange was not included in bookable time from now
210            && !$availability->hasDate($availability->getBookableStart($slotLastChange), $now)
211            // Second check, if availability had a bookable time on lastChange before bookable start from now
212            && $availability->hasDateBetween(
213                $availability->getBookableStart($slotLastChange),
214                $availability->getBookableStart($now),
215                $slotLastChange
216            )
217        ) {
218            $availability['processingNote'][] = 'outdated: slots invalidated by bookable start';
219            return true;
220        }
221        $availability['processingNote'][] = 'not outdated';
222        return false;
223    }
224
225    /**
226     * @return bool TRUE if there were changes on slots
227     */
228    public function writeByAvailability(
229        \BO\Zmsentities\Availability $availability,
230        \DateTimeInterface $now,
231        ?\DateTimeInterface $slotLastChange = null
232    ) {
233        $now = \BO\Zmsentities\Helper\DateTime::create($now);
234        $calculateSlotsUntilDate = \BO\Zmsentities\Helper\DateTime::create($now)->modify('+' . self::MAX_DAYS_OF_SLOT_CALCULATION . ' days');
235        if (!$slotLastChange) {
236            $slotLastChange = $this->readLastChangedTimeByAvailability($availability);
237        }
238        $lastGeneratedSlotDate = $this->getLastGeneratedSlotDate($availability);
239        $oldestSlotVersion = $this->getOldestSlotVersionByAvailability($availability);
240        $availability['processingNote'][] = 'lastchange=' . $slotLastChange->format('c');
241        if (!$this->isAvailabilityOutdated($availability, $now, $slotLastChange, $oldestSlotVersion)) {
242            return false;
243        }
244        $startDate = $availability->getBookableStart($now)->modify('00:00:00');
245        $stopDate = $availability->getBookableEnd($now);
246        $generateNew = empty($lastGeneratedSlotDate) || $availability->version > $oldestSlotVersion;
247        (new \BO\Zmsbackend\Availability\Service\Availability())->readLock($availability->id);
248        $cancelledSlots = 0;
249        $cancelledSlots += $this->fetchAffected(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_CANCEL_AVAILABILITY_BEFORE_BOOKABLE, [
250            'availabilityID' => $availability->id,
251            'providedDate' => $startDate->format('Y-m-d')
252        ]);
253        // Cancel slots only if previously generated beyond new bookable end
254        if ($lastGeneratedSlotDate && $lastGeneratedSlotDate->getTimestamp() > $stopDate->getTimestamp()) {
255            $cancelledSlots += $this->fetchAffected(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_CANCEL_AVAILABILITY_AFTER_BOOKABLE, [
256                'availabilityID' => $availability->id,
257                'providedDate' => $stopDate->format('Y-m-d')
258            ]);
259        }
260        if ($generateNew) {
261            $cancelledSlots += $this->fetchAffected(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_CANCEL_AVAILABILITY, [
262                'availabilityID' => $availability->id,
263            ]);
264
265            if (!$availability->withData(['bookable' => ['startInDays' => 0]])->hasBookableDates($now)) {
266                $availability['processingNote'][] = "cancelled $cancelledSlots slots: availability not bookable ";
267                return ($cancelledSlots > 0) ? true : false;
268            }
269
270            \App::$log->info('availability: ', [
271                'generate_new' => $generateNew,
272                'availability_id' => $availability->id,
273                'cancelledSlots' => $cancelledSlots
274            ]);
275
276            $availability['processingNote'][] = "cancelled $cancelledSlots slots";
277        } elseif ($cancelledSlots > 0) {
278            $availability['processingNote'][] = "cancelled $cancelledSlots slots";
279        }
280
281        $slotlist = $availability->getSlotList();
282        $slotlistIntern = $slotlist->withValueFor('public', 0);
283        $time = $now->modify('00:00:00');
284        if (!$generateNew) {
285            $time = $lastGeneratedSlotDate->modify('+1 day')->modify('00:00:00');
286        }
287        $status = false;
288        do {
289            if ($availability->withData(['bookable' => ['startInDays' => 0]])->hasDate($time, $now)) {
290                $writeStatus = $this->writeSlotListForDate(
291                    $time,
292                    ($time->getTimestamp() < $startDate->getTimestamp()) ? $slotlistIntern : $slotlist,
293                    $availability
294                );
295                $status = $writeStatus ? $writeStatus : $status;
296            }
297            $time = $time->modify('+1day');
298        } while ($time->getTimestamp() <= $stopDate->getTimestamp() && $time->getTimestamp() < $calculateSlotsUntilDate->getTimestamp());
299
300        return $status || (isset($cancelledSlots) && $cancelledSlots > 0);
301    }
302
303    public function writeByScope(\BO\Zmsentities\Scope $scope, \DateTimeInterface $now): \BO\Zmsentities\Collection\AvailabilityList
304    {
305        $slotLastChange = $this->readLastChangedTimeByScope($scope);
306        $availabilityList = (new \BO\Zmsbackend\Availability\Service\Availability())
307            ->readAvailabilityListByScope($scope, 0, $slotLastChange->modify('-1 day'))
308            ;
309        $updatedList = new \BO\Zmsentities\Collection\AvailabilityList();
310        foreach ($availabilityList as $availability) {
311            $availability->scope = clone $scope; //dayoff is required
312            if ($this->writeByAvailability($availability, $now)) {
313                $updatedList->addEntity($availability);
314            }
315        }
316        return $updatedList;
317    }
318
319    protected function writeSlotListForDate(
320        \DateTimeInterface $time,
321        Collection $slotlist,
322        AvailabilityEntity $availability
323    ) {
324        $ancestors = [];
325        $hasAddedSlots = false;
326
327        foreach ($slotlist as $slot) {
328            $slot = clone $slot;
329            $slotID = $this->readByAvailability($slot, $availability, $time);
330            if ($slotID) {
331                $query = new \BO\Zmsbackend\Slot\Repository\Slot(\BO\Zmsbackend\Query\Base::UPDATE);
332                $query->addConditionSlotId($slotID);
333            } else {
334                $query = new \BO\Zmsbackend\Slot\Repository\Slot(\BO\Zmsbackend\Query\Base::INSERT);
335                $hasAddedSlots = true;
336            }
337            $slot->status = 'free';
338            $values = $query->reverseEntityMapping($slot, $availability, $time);
339            $values['createTimestamp'] = time();
340            $query->addValues($values);
341            $writeStatus = $this->writeItem($query);
342            if ($writeStatus && !$slotID) {
343                $slotID = $this->getWriter()->lastInsertId();
344            }
345            $ancestors[] = $slotID;
346            // TODO: Check if slot changed before writing ancestor IDs
347            $this->writeAncestorIDs($slotID, $ancestors);
348            $status = $writeStatus ? $writeStatus : $status;
349        }
350        if ($hasAddedSlots) {
351            $availability['processingNote'][] = 'Added ' . $time->format('Y-m-d');
352        }
353        return $status;
354    }
355
356    protected function writeAncestorIDs($slotID, array $ancestors): void
357    {
358        $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_ANCESTOR, [
359            'slotID' => $slotID,
360        ]);
361        $ancestorLevel = count($ancestors);
362        foreach ($ancestors as $ancestorID) {
363            if ($ancestorLevel <= self::MAX_SLOTS) {
364                $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_INSERT_ANCESTOR, [
365                    'slotID' => $slotID,
366                    'ancestorID' => $ancestorID,
367                    'ancestorLevel' => $ancestorLevel,
368                ]);
369            }
370            $ancestorLevel--;
371        }
372    }
373
374    public function readLastChangedTime(): \DateTimeImmutable
375    {
376        $last = $this->fetchRow(
377            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_LAST_CHANGED
378        );
379        if (!$last['dateString']) {
380            $last['dateString'] = '1970-01-01 12:00';
381        }
382        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsbackend\Connection\Select::$connectionTimezone);
383    }
384
385    public function readLastChangedTimeByScope(ScopeEntity $scope): \DateTimeImmutable
386    {
387        $last = $this->fetchRow(
388            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_LAST_CHANGED_SCOPE,
389            [
390                'scopeID' => $scope->id,
391            ]
392        );
393        if (!$last['dateString']) {
394            $last['dateString'] = '1970-01-01 12:00';
395        }
396        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsbackend\Connection\Select::$connectionTimezone);
397    }
398
399    public function readLastChangedTimeByAvailability(AvailabilityEntity $availabiliy): \DateTimeImmutable
400    {
401        $last = $this->fetchRow(
402            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_LAST_CHANGED_AVAILABILITY,
403            [
404                'availabilityID' => $availabiliy->id,
405            ]
406        );
407        if (!$last['dateString']) {
408            $last['dateString'] = '1970-01-01 12:00';
409        }
410        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsbackend\Connection\Select::$connectionTimezone);
411    }
412
413    public function updateSlotProcessMapping($scopeID = null): int
414    {
415        if ($scopeID) {
416            $processIdList = $this->fetchAll(
417                \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_MISSING_PROCESS
418                . \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_MISSING_PROCESS_BY_SCOPE,
419                ['scopeID' => $scopeID]
420            );
421        } else {
422            $processIdList = $this->fetchAll(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_MISSING_PROCESS, []);
423        }
424        // Client side INSERT ... SELECT ... to reduce table locking
425        foreach ($processIdList as $processId) {
426            $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_INSERT_SLOT_PROCESS, array_values($processId));
427        }
428        return count($processIdList);
429    }
430
431    public function deleteSlotProcessOnSlot($scopeID = null): void
432    {
433        if ($scopeID) {
434            $this->perform(
435                \BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED
436                . \BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED_BY_SCOPE,
437                ['scopeID' => $scopeID]
438            );
439        } else {
440            $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_PROCESS_CANCELLED, []);
441        }
442    }
443
444    public function deleteSlotProcessOnProcess($scopeID = null): int
445    {
446        if ($scopeID) {
447            $processIdList = $this->fetchAll(
448                \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS
449                . \BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS_BY_SCOPE,
450                ['scopeID' => $scopeID]
451            );
452        } else {
453            $processIdList = $this->fetchAll(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_DELETABLE_SLOT_PROCESS);
454        }
455        // Client side INSERT ... SELECT ... to reduce table locking
456        foreach ($processIdList as $processId) {
457            $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_PROCESS_ID, $processId);
458        }
459        return count($processIdList);
460    }
461
462    public function writeSlotProcessMappingFor($processId): static
463    {
464        $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_INSERT_SLOT_PROCESS_ID, [
465            'processId' => $processId,
466        ]);
467        return $this;
468    }
469
470    public function deleteSlotProcessMappingFor($processId): static
471    {
472        $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_PROCESS_ID, [
473            'processId' => $processId,
474        ]);
475        return $this;
476    }
477
478    public function writeCanceledByTimeAndScope(\DateTimeInterface $dateTime, \BO\Zmsentities\Scope $scope): bool
479    {
480        $status = $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY_BY_SCOPE, [
481            'dateString' => $dateTime->format('Y-m-d'),
482            'scopeID' => $scope->id,
483        ]);
484
485        return $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_CANCEL_SLOT_OLD_BY_SCOPE, [
486            'year' => $dateTime->format('Y'),
487            'month' => $dateTime->format('m'),
488            'day' => $dateTime->format('d'),
489            'time' => $dateTime->format('H:i:s'),
490            'scopeID' => $scope->id,
491        ]) && $status;
492    }
493
494    public function writeCanceledByTime(\DateTimeInterface $dateTime): bool
495    {
496        $status = $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_UPDATE_SLOT_MISSING_AVAILABILITY, [
497            'dateString' => $dateTime->format('Y-m-d'),
498        ]);
499        return $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_CANCEL_SLOT_OLD, [
500            'year' => $dateTime->format('Y'),
501            'month' => $dateTime->format('m'),
502            'day' => $dateTime->format('d'),
503            'time' => $dateTime->format('H:i:s'),
504        ]) && $status;
505    }
506
507    public function deleteSlotsOlderThan(\DateTimeInterface $dateTime): bool
508    {
509        $status = $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_OLD, [
510            'year' => $dateTime->format('Y'),
511            'month' => $dateTime->format('m'),
512            'day' => $dateTime->format('d'),
513        ]);
514        $status = ($status && $this->perform(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_DELETE_SLOT_HIERA));
515        return $status;
516    }
517
518    /**
519     * This function is for debugging
520     */
521    public function readRowsByScopeAndDate(
522        \BO\Zmsentities\Scope $scope,
523        \DateTimeInterface $dateTime
524    ) {
525        $list = $this->fetchAll(\BO\Zmsbackend\Slot\Repository\Slot::QUERY_SELECT_BY_SCOPE_AND_DAY, [
526            'year' => $dateTime->format('Y'),
527            'month' => $dateTime->format('m'),
528            'day' => $dateTime->format('d'),
529            'scopeID' => $scope->id,
530        ]);
531        return $list;
532    }
533
534    public function writeOptimizedSlotTables(): bool
535    {
536        $queries = [
537            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_OPTIMIZE_SLOT,
538            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_OPTIMIZE_SLOT_HIERA,
539            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_OPTIMIZE_SLOT_PROCESS,
540            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_OPTIMIZE_PROCESS,
541        ];
542
543        $status = true;
544        foreach ($queries as $query) {
545            try {
546                $status = $status && $this->perform($query);
547            } catch (\PDOException $e) {
548                \App::$log->error("Failed to optimize table with query: $query. Error: " . $e->getMessage(), []);
549
550                return false;
551            }
552        }
553
554        return $status;
555    }
556
557    private function getLastGeneratedSlotDate(AvailabilityEntity $availability): \DateTimeImmutable|null
558    {
559        $last = $this->fetchRow(
560            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_LAST_IN_AVAILABILITY,
561            [
562                'availabilityID' => $availability->id,
563            ]
564        );
565
566        if (!isset($last['dateString'])) {
567            return null;
568        }
569
570        return new \DateTimeImmutable($last['dateString'] . \BO\Zmsbackend\Connection\Select::$connectionTimezone);
571    }
572
573    private function getOldestSlotVersionByAvailability(AvailabilityEntity $availability)
574    {
575        $last = $this->fetchRow(
576            \BO\Zmsbackend\Slot\Repository\Slot::QUERY_OLDEST_VERSION_IN_AVAILABILITY,
577            [
578                'availabilityID' => $availability->id,
579            ]
580        );
581
582        return $last['version'] ?? 1;
583    }
584}

Inherited from BO\Zmsbackend\Base

38    public function __construct(?Connection\Pdo $writeConnection = null, ?Connection\Pdo $readConnection = null)
39    {
40        $this->writeDb = $writeConnection;
41        $this->readDb = $readConnection;
42    }
44    public static function init(?Connection\Pdo $writeConnection = null, ?Connection\Pdo $readConnection = null): static
45    {
46        $instance = new static($writeConnection, $readConnection);
47        return $instance;
48    }
50    public function getWriter(): Connection\Pdo
51    {
52        if (null === $this->writeDb) {
53            $this->writeDb = Connection\Select::getWriteConnection();
54            $this->readDb = $this->writeDb;
55        }
56        return $this->writeDb;
57    }
59    public function getReader(): Connection\Pdo
60    {
61        if (null !== $this->writeDb) {
62            // if readDB gets a reset, still use writeDB
63            return $this->writeDb;
64        }
65        if (null === $this->readDb) {
66            $this->readDb = Connection\Select::getReadConnection();
67        }
68        return $this->readDb;
69    }
71    public function fetchPreparedStatement(Query\Base|string $query)
72    {
73        $sql = "$query";
74        $reader = $this->getReader();
75        if (spl_object_hash($reader) != static::$preparedConnectionId) {
76            // do not use prepared statements on a different connection
77            static::$preparedCache = [];
78            static::$preparedConnectionId = spl_object_hash($reader);
79        }
80        if (!isset(static::$preparedCache[$sql])) {
81            $prepared = $this->getReader()->prepare($sql);
82            static::$preparedCache[$sql] = $prepared;
83        }
84        return static::$preparedCache[$sql];
85    }
87    public function startExecute($statement, array $parameters)
88    {
89        $statement = static::pdoExceptionHandler(function () use ($statement, $parameters) {
90            $statement->execute($parameters);
91            return $statement;
92        });
93        return $statement;
94    }
96    protected static function pdoExceptionHandler(\Closure $pdoFunction, $parameters = [])
97    {
98        try {
99            $statement = $pdoFunction($parameters);
100        } catch (\PDOException $pdoException) {
101            if (stripos($pdoException->getMessage(), 'Lock wait timeout') !== false) {
102                throw new \BO\Zmsbackend\Exception\Pdo\LockTimeout();
103            }
104            //@codeCoverageIgnoreStart
105            if (stripos($pdoException->getMessage(), 'Deadlock found') !== false) {
106                throw new \BO\Zmsbackend\Exception\Pdo\DeadLockFound();
107            }
108            //@codeCoverageIgnoreEnd
109            $message = "SQL: "
110                . " Err: "
111                . $pdoException->getMessage()
112                //. " || Statement: "
113                //.$statement->queryString
114                //." || Parameters=". var_export($parameters, true)
115                ;
116            throw new \BO\Zmsbackend\Exception\Pdo\PDOFailed($message, 0, $pdoException);
117        }
118        return $statement;
119    }
121    public function fetchStatement(Query\Base $query)
122    {
123        $parameters = $query->getParameters();
124        $statement = $this->startExecute($this->fetchPreparedStatement($query), $parameters);
125        return $statement;
126    }
133    public function fetchOne(Query\Base $query, \BO\Zmsentities\Schema\Entity $entity)
134    {
135        $statement = $this->fetchStatement($query);
136        $data = $statement->fetch(\PDO::FETCH_ASSOC);
137        if ($data) {
138            $entity->exchangeArray($query->postProcessJoins($data));
139            $entity->setResolveLevel($query->getResolveLevel());
140        }
141        $statement->closeCursor();
142        return $entity;
143    }
145    public function fetchRow(Query\Base|string $query, array|null $parameters = null)
146    {
147        return static::pdoExceptionHandler(function () use ($query, $parameters) {
148            $prepared = $this->fetchPreparedStatement($query);
149            $prepared->execute($parameters);
150            $row = $prepared->fetch(\PDO::FETCH_ASSOC);
151            $prepared->closeCursor();
152            return $row;
153        });
154    }
156    public function fetchValue(Query\Base|string $query, array|null $parameters = null)
157    {
158        return static::pdoExceptionHandler(function () use ($query, $parameters) {
159            $prepared = $this->fetchPreparedStatement($query);
160            $prepared->execute($parameters);
161            $value = $prepared->fetchColumn();
162            $prepared->closeCursor();
163            return $value;
164        });
165    }
167    public function fetchAll(Query\Base|string $query, array|null $parameters = null)
168    {
169        return static::pdoExceptionHandler(function () use ($query, $parameters) {
170            $prepared = $this->fetchPreparedStatement($query);
171            $prepared->execute($parameters);
172            $list = $prepared->fetchAll(\PDO::FETCH_ASSOC);
173            $prepared->closeCursor();
174            return $list;
175        });
176    }
178    public function fetchHandle(Query\Base|string $query, array|null $parameters = null)
179    {
180        return static::pdoExceptionHandler(function () use ($query, $parameters) {
181            $prepared = $this->fetchPreparedStatement($query);
182            $prepared->execute($parameters);
183            return $prepared;
184        });
185    }
187    public function fetchList(Query\Base $query, \BO\Zmsentities\Schema\Entity $entity, array|\ArrayAccess $resultList = [])
188    {
189        $statement = $this->fetchStatement($query);
190        while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) {
191            $dataEntity = clone $entity;
192            $dataEntity->exchangeArray($query->postProcessJoins($data));
193            $dataEntity->setResolveLevel($query->getResolveLevel());
194            $resultList[] = $dataEntity;
195        }
196        $statement->closeCursor();
197        return $resultList;
198    }
206    public function writeItem(Query\Base $query)
207    {
208        return static::pdoExceptionHandler(function () use ($query) {
209            $this->getWriter(); //Switch to writer for write/delete
210            $statement = $this->fetchPreparedStatement($query);
211            $status = $statement->execute($query->getParameters());
212            $statement->closeCursor();
213            return $status;
214        });
215    }
221    public function deleteItem(Query\Base $query)
222    {
223        return $this->writeItem($query);
224    }
226    public function perform(string $sql, array|null $parameters = null)
227    {
228        return static::pdoExceptionHandler(function () use ($sql, $parameters) {
229            $this->getWriter(); //Switch to writer for perform
230            $prepared = $this->fetchPreparedStatement($sql);
231            $status = $prepared->execute($parameters);
232            $prepared->closeCursor();
233            return $status;
234        });
235    }
237    public function fetchAffected(string $sql, array|null $parameters = null)
238    {
239        return static::pdoExceptionHandler(function () use ($sql, $parameters) {
240            $this->getWriter(); //Switch to writer for perform
241            $prepared = $this->fetchPreparedStatement($sql);
242            $prepared->execute($parameters);
243            $count = $prepared->rowCount();
244            $prepared->closeCursor();
245            return $count;
246        });
247    }
256    public function readResolvedReferences(\BO\Zmsentities\Schema\Entity $entity, int $resolveReferences)
257    {
258        return $entity;
259    }
268    public function hashStringValue(string $value, ?callable $callable = null): string
269    {
270        if ($callable === null) {
271            $callable = 'sha1';
272        }
273
274        if (is_callable($callable)) {
275            $hash = $callable($value);
276
277            if (is_string($callable)) {
278                return $callable . ':' . $hash;
279            }
280            if ($callable instanceof \Closure) {
281                return 'closure:' . $hash;
282            }
283            // else
284            return 'custom:' . $hash;
285        }
286
287        throw new \InvalidArgumentException('hashStringValue() should be called with a callable as second parameter.');
288    }