Lines 82.44% 202 / 245
Methods 77.50% 31 / 40
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 takeLowerSlotValue 100.00% 7 / 7 100.00% 1 / 1 5
 setEmptySlotValues 100.00% 6 / 6 100.00% 1 / 1 2
 isAvailableForAll 100.00% 4 / 4 100.00% 1 / 1 3
 getByDateTime 100.00% 4 / 4 100.00% 1 / 1 4
 withSlotsForAppointment 94.73% 18 / 19 0.00% 0 / 1 8.01
 extendList 0.00% 0 / 13 0.00% 0 / 1 6
 removeAppointment 75.00% 12 / 16 0.00% 0 / 1 7.77
 getSlot 100.00% 4 / 4 100.00% 1 / 1 2
 getSummerizedSlot 100.00% 6 / 6 100.00% 1 / 1 3
 withTimeGreaterThan 100.00% 9 / 9 100.00% 1 / 1 3
 getFreeProcesses 100.00% 24 / 24 100.00% 1 / 1 7
 withReducedSlots 100.00% 10 / 10 100.00% 1 / 1 6
 __toString 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] __construct 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] getFirst 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] getLast 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] sortByName 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] sortByContactName 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] sortByCustomKey 100.00% 4 / 4 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] sortByCustomStringKey 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] __clone 100.00% 2 / 2 100.00% 1 / 1 2
 [BO\Zmsentities\Collection\Base] hasEntity 100.00% 4 / 4 100.00% 1 / 1 4
 [BO\Zmsentities\Collection\Base] getEntity 100.00% 4 / 4 100.00% 1 / 1 4
 [BO\Zmsentities\Collection\Base] addEntity 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] offsetSet 100.00% 9 / 9 100.00% 1 / 1 3
 [BO\Zmsentities\Collection\Base] addData 91.66% 11 / 12 0.00% 0 / 1 5.01
 [BO\Zmsentities\Collection\Base] addList 100.00% 3 / 3 100.00% 1 / 1 2
 [BO\Zmsentities\Collection\Base] getIds 100.00% 4 / 4 100.00% 1 / 1 2
 [BO\Zmsentities\Collection\Base] getIdsCsv 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] getCsvForProperty 0.00% 0 / 5 0.00% 0 / 1 12
 [BO\Zmsentities\Collection\Base] getCsvForPropertyList 100.00% 11 / 11 100.00% 1 / 1 6
 [BO\Zmsentities\Collection\Base] withValueFor 0.00% 0 / 4 0.00% 0 / 1 6
 [BO\Zmsentities\Collection\Base] withLessData 100.00% 4 / 4 100.00% 1 / 1 2
 [BO\Zmsentities\Collection\Base] setJsonCompressLevel 0.00% 0 / 2 0.00% 0 / 1 6
 [BO\Zmsentities\Collection\Base] getAsArray 0.00% 0 / 4 0.00% 0 / 1 6
 [BO\Zmsentities\Collection\Base] jsonSerialize 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] getResolveLevel 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] setResolveLevel 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsentities\Collection\Base] withResolveLevel 100.00% 5 / 5 100.00% 1 / 1 2
 [BO\Zmsentities\Collection\Base] chunk 0.00% 0 / 9 0.00% 0 / 1 12
11class SlotList extends Base
12{
13    public const string ENTITY_CLASS = '\BO\Zmsentities\Slot';
14
15    /**
16     * Compare two slots and return the lower values
17     *
18     * @param array $slotA
19     * @param array $slotB
20     *
21     * @return static
22     *
23     */
24    public function takeLowerSlotValue(int $indexA, int $indexB): static
25    {
26        if ($this->offsetExists($indexA) && $this->offsetExists($indexB)) {
27            $slotA = $this[$indexA];
28            $slotB = $this[$indexB];
29            $slotA->type = Slot::REDUCED;
30            foreach (['public', 'intern'] as $type) {
31                $slotA[$type] = $slotA[$type] < $slotB[$type] ? $slotA[$type] : $slotB[$type];
32            }
33        }
34        return $this;
35    }
36
37    public function setEmptySlotValues(int|string $index): static
38    {
39        $slot = $this->getSlot($index);
40        if (null !== $slot) {
41            $slot['public'] = 0;
42            $slot['intern'] = 0;
43            $slot->type = Slot::REDUCED;
44        }
45        return $this;
46    }
47
48    public function isAvailableForAll(mixed $slotType): bool
49    {
50        foreach ($this as $slot) {
51            if ($slot[$slotType] < 1) {
52                return false;
53            }
54        }
55        return true;
56    }
57
58    /**
59     * Get a slot for a given time
60     */
61    public function getByDateTime(\DateTimeInterface $dateTime): Slot|false
62    {
63        foreach ($this as $slot) {
64            if ($slot->hasTime() && $slot->time == $dateTime->format('H:i')) {
65                return $slot;
66            }
67        }
68        return false;
69    }
70
71    /**
72     * Get all slots for an appointment
73     *
74     */
75    public function withSlotsForAppointment(\BO\Zmsentities\Appointment $appointment, bool $extendSlotList = false): mixed
76    {
77        $slotList = new SlotList();
78        $takeFollowingSlot = 0;
79        $startTime = $appointment->toDateTime();
80        $currentSlot = null;
81        foreach ($this as $slot) {
82            $currentSlot = clone $slot;
83            if ($takeFollowingSlot > 0) {
84                $takeFollowingSlot--;
85                $slotList[] = $currentSlot;
86            }
87            if ($slot->hasTime() && $slot->time == $startTime->format('H:i')) {
88                $slotList[] = $currentSlot;
89                $takeFollowingSlot = $appointment['slotCount'] - 1;
90            }
91        }
92        if (0 < $takeFollowingSlot && ! $extendSlotList) {
93            throw new \BO\Zmsentities\Exception\AppointmentNotFitInSlotList(
94                "$appointment does not fit in $this"
95            );
96        }
97        if (0 < $takeFollowingSlot) {
98            $slotList = $this->extendList($slotList, $currentSlot, $appointment);
99        }
100        return $slotList;
101    }
102
103    public function extendList(self $slotList, Slot|null $prevSlot, \BO\Zmsentities\Appointment $appointment): \BO\Zmsentities\Collection\SlotList
104    {
105        if ($prevSlot === null) {
106            return $slotList;
107        }
108        $slotMinutes = (string) (int) $appointment->getAvailability()->slotTimeInMinutes;
109        $startTime = \BO\Zmsentities\Helper\DateTime::create(
110            $appointment->toDateTime()->format('Y-m-d') . ' ' . $prevSlot->time
111        )->modify('+' . $slotMinutes . 'minute');
112        $stopTime = $appointment->getEndTime();
113        do {
114            $slot = clone $prevSlot;
115            $slot->setTime($startTime);
116            $slotList[] = $slot;
117            $startTime = $startTime->modify('+' . $slotMinutes . 'minute');
118            // Only add a slot, if at least a minute is left, otherwise do not ("<" instead "<=")
119        } while ($startTime->getTimestamp() < $stopTime->getTimestamp());
120        return $slotList;
121    }
122
123
124    /**
125     * Reduce free appointments on slot matching appointment
126     *
127     * -----------|----------------|------------
128     * first loop | intern slots 2 | slotCount 3
129     *
130     *
131     * @return bool true on success and false if no matching slot is found or no appointments are free
132     */
133
134
135    public function removeAppointment(\BO\Zmsentities\Appointment $appointment)
136    {
137        $takeFollowingSlot = 0;
138        $startTime = $appointment->toDateTime()->format('H:i');
139        $containsAppointment = false;
140        foreach ($this as $slot) {
141            if ($takeFollowingSlot > 0) {
142                if (0 == $slot['intern']) {
143                    return false;
144                }
145                $slot->removeAppointment();
146                $takeFollowingSlot--;
147            }
148            if ($slot->hasTime() && $slot->time == $startTime) {
149                $takeFollowingSlot = $appointment['slotCount'] - 1;
150                if (0 < $slot['intern']) {
151                    $containsAppointment = true;
152                    $slot->removeAppointment();
153                } else {
154                    return false;
155                }
156            }
157        }
158        return $containsAppointment;
159    }
160
161    /**
162     * @param int|string $index
163     */
164    public function getSlot(string|int $index): Slot|null
165    {
166        $index = intval($index);
167        if (!isset($this[$index])) {
168            return null;
169        }
170        return $this[$index];
171    }
172
173    public function getSummerizedSlot(mixed $slot = null): Slot
174    {
175        $sum = ($slot instanceof Slot) ? $slot : new Slot();
176        $sum->type = Slot::SUM;
177        foreach ($this as $slot) {
178            $sum['public'] += $slot['public'];
179            $sum['intern'] += $slot['intern'];
180        }
181        return $sum;
182    }
183
184    /*
185     * reduce slotlist from slots smaller than reference time (today) + 1800 seconds
186     */
187    public function withTimeGreaterThan(\DateTimeInterface $dateTime): static
188    {
189        $slotList = clone $this;
190        $referenceTime = $dateTime->getTimestamp() + 1800;
191        foreach ($this as $index => $slot) {
192            $slotTime = \BO\Zmsentities\Helper\DateTime::create(
193                $dateTime->format('Y-m-d') . ' ' . $slot->time
194            )->getTimeStamp();
195            if ($referenceTime > $slotTime) {
196                $slotList->setEmptySlotValues($index);
197            }
198        }
199        return $slotList;
200    }
201
202    public function getFreeProcesses(
203        mixed $selectedDate,
204        \BO\Zmsentities\Scope $scope,
205        \BO\Zmsentities\Availability $availability,
206        mixed $slotType,
207        mixed $requests,
208        mixed $slotsRequired
209    ): ProcessList {
210        $processList = new ProcessList();
211        foreach ($this as $slot) {
212            if ($slotsRequired > 1 && $slot->type != Slot::REDUCED) {
213                throw new \BO\Zmsentities\Exception\SlotRequiredWithoutReducing(
214                    "With $slotsRequired slots required, "
215                    . "do not use SlotList::getFreeProcesses without reduced slots: $slot"
216                );
217            }
218            if ($slot[$slotType] > 0) {
219                $appointment = new \BO\Zmsentities\Appointment(array(
220                    'scope' => $scope,
221                    'availability' => $availability,
222                    'slotCount' => $slotsRequired
223                ));
224                if (!$slot->hasTime()) {
225                    throw new \BO\Zmsentities\Exception\SlotMissingTime("Time on slot not set: $slot");
226                }
227                $appointment->setDateByString($selectedDate . ' ' . $slot->getTimeString());
228                $process = new \BO\Zmsentities\Process(array(
229                    'scope' => $scope,
230                    'requests' => $requests
231                ));
232                for ($count = 0; $count < $slot[$slotType]; $count++) {
233                    $process->addAppointment($appointment);
234                }
235                $processList[] = $process;
236            }
237        }
238        return $processList;
239    }
240
241    public function withReducedSlots(mixed $slotsRequired): static
242    {
243        $slotList = clone $this;
244        if ($slotsRequired > 1) {
245            $slotLength = count($slotList);
246            for ($slotIndex = 0; $slotIndex < $slotLength; $slotIndex++) {
247                if ($slotIndex + $slotsRequired - 1 < $slotLength) {
248                    for ($slotRelative = 1; $slotRelative < $slotsRequired; $slotRelative++) {
249                        if ($slotIndex + $slotRelative < $slotLength) {
250                            $slotList->takeLowerSlotValue($slotIndex, $slotIndex + $slotRelative);
251                        }
252                    }
253                } else {
254                    $slotList->setEmptySlotValues($slotIndex);
255                }
256            }
257        }
258        return $slotList;
259    }
260
261    public function __toString()
262    {
263        $count = count($this);
264        $sum = $this->getSummerizedSlot();
265        return "slotlist#$count âˆ‘$sum";
266    }
267}

Inherited from BO\Zmsentities\Collection\Base

32    public function __construct(object|array $array = [], int $flags = 0, string $iteratorClass = "ArrayIterator")
33    {
34        /** @var class-string<\ArrayIterator> $iteratorClass */
35        parent::__construct($array, $flags, $iteratorClass);
36    }
41    public function getFirst()
42    {
43        $item = $this->getIterator()->current();
44        return $item;
45    }
51    public function getLast()
52    {
53        $copy = $this->getArrayCopy();
54        $item = end($copy);
55        return $item;
56    }
62    public function sortByName()
63    {
64        $this->uasort(function ($a, $b) {
65            return strcmp(
66                Sorter::toSortableString(ucfirst($a->name ?? '')),
67                Sorter::toSortableString(ucfirst($b->name ?? ''))
68            );
69        });
70        return $this;
71    }
73    public function sortByContactName(): static
74    {
75        $this->uasort(function ($a, $b) {
76            return strcmp(
77                Sorter::toSortableString(ucfirst($a->contact['name'] ?? '')),
78                Sorter::toSortableString(ucfirst($b->contact['name'] ?? ''))
79            );
80        });
81        return $this;
82    }
84    public function sortByCustomKey(string $key): static
85    {
86        $this->uasort(function ($a, $b) use ($key): mixed {
87            return ($a[$key] - $b[$key]);
88        });
89        return $this;
90    }
92    public function sortByCustomStringKey(mixed $key): static
93    {
94        $this->uasort(function ($a, $b) use ($key) {
95            return strcmp(
96                Sorter::toSortableString(ucfirst($a[$key] ?? '')),
97                Sorter::toSortableString(ucfirst($b[$key] ?? ''))
98            );
99        });
100        return $this;
101    }
103    public function __clone()
104    {
105        foreach ($this as $key => $property) {
106            $this[$key] = clone $property;
107        }
108    }
110    public function hasEntity(mixed $primary): bool
111    {
112        foreach ($this as $entity) {
113            if (isset($entity->{$entity::PRIMARY}) && $primary == $entity->{$entity::PRIMARY}) {
114                return true;
115            }
116        }
117        return false;
118    }
124    public function getEntity(string|int $primary)
125    {
126        foreach ($this as $entity) {
127            if (isset($entity->{$entity::PRIMARY}) && $primary == $entity->{$entity::PRIMARY}) {
128                return $entity;
129            }
130        }
131        return null;
132    }
134    public function addEntity(\BO\Zmsentities\Schema\Entity $entity): static
135    {
136        /** @var T $entity */
137        $this->append($entity);
138        return $this;
139    }
146    public function offsetSet(mixed $index, mixed $value): void
147    {
148        $className = $this::ENTITY_CLASS;
149        if (is_a($value, $className)) {
150            /** @var T $entity */
151            $entity = $value;
152            /** @psalm-suppress PossiblyNullArgument */
153            parent::offsetSet($index, $entity);
154        } elseif (is_array($value)) {
155            /** @var T $entity */
156            $entity = new $className($value);
157            /** @psalm-suppress PossiblyNullArgument */
158            parent::offsetSet($index, $entity);
159        } else {
160            $given = get_debug_type($value);
161            throw new \Exception('Invalid entity ' . $given . ' for collection ' . __CLASS__);
162        }
163    }
165    public function addData(mixed $mergeData): static
166    {
167        foreach ($mergeData as $item) {
168            if ($item instanceof Entity) {
169                if (null === $item->getResolveLevel()) {
170                    $item->setResolveLevel($this->getResolveLevel());
171                }
172                $this->addEntity($item);
173            } else {
174                $className = $this::ENTITY_CLASS;
175                $entity = new $className($item);
176                if (!$entity instanceof Entity) {
177                    throw new \Exception('Invalid entity ' . get_debug_type($entity) . ' for collection ' . __CLASS__);
178                }
179                $entity->setResolveLevel($this->getResolveLevel());
180                $this->addEntity($entity);
181            }
182        }
183        return $this;
184    }
189    public function addList(Base $list): static
190    {
191        foreach ($list as $item) {
192            $this->addEntity($item);
193        }
194        return $this;
195    }
201    public function getIds()
202    {
203        $list = [];
204        foreach ($this as $entity) {
205            $list[] = $entity->id;
206        }
207        return $list;
208    }
210    public function getIdsCsv(): string
211    {
212        return implode(',', $this->getIds());
213    }
215    public function getCsvForProperty(mixed $propertyName, string $csvSeperator = ','): string
216    {
217        $list = [];
218        foreach ($this as $entry) {
219            if ($entry->hasProperty($propertyName)) {
220                $list[] = $entry->getProperty($propertyName);
221            }
222        }
223        return implode($csvSeperator, $list);
224    }
226    public function getCsvForPropertyList(array $propertyList, string $propertySeperator = '', string $csvSeperator = ','): string
227    {
228        $list = [];
229        foreach ($this as $entry) {
230            $propertyConcat = '';
231            foreach ($propertyList as $propertyName) {
232                if ($entry->hasProperty($propertyName)) {
233                    if ($propertyConcat) {
234                        $propertyConcat .= $propertySeperator;
235                    }
236                    $propertyConcat .= $entry->getProperty($propertyName);
237                }
238            }
239            if ($propertyConcat) {
240                $list[] = $propertyConcat;
241            }
242        }
243        return implode($csvSeperator, $list);
244    }
250    public function withValueFor(mixed $param, mixed $newValue): static
251    {
252        /** @psalm-suppress UnsafeGenericInstantiation */
253        $list = new static();
254        foreach ($this as $entry) {
255            $list[] = $entry->withProperty($param, $newValue);
256        }
257        return $list;
258    }
266    public function withLessData(array $keepArray = [])
267    {
268        /** @psalm-suppress UnsafeGenericInstantiation */
269        $list = new static();
270        foreach ($this as $key => $item) {
271            /** @psalm-suppress TooManyArguments */
272            $list[$key] = $item->withLessData($keepArray);
273        }
274        return $list;
275    }
277    public function setJsonCompressLevel(mixed $jsonCompressLevel): void
278    {
279        foreach ($this as $item) {
280            $item->setJsonCompressLevel($jsonCompressLevel);
281        }
282    }
288    public function getAsArray(): array
289    {
290        $array = [];
291        foreach ($this as $item) {
292            $array[] = $item;
293        }
294
295        return $array;
296    }
299    public function jsonSerialize(): mixed
300    {
301        return $this->getArrayCopy();
302    }
316    public function getResolveLevel()
317    {
318        return $this->resolveLevel;
319    }
325    public function setResolveLevel($resolveLevel)
326    {
327        $this->resolveLevel = $resolveLevel;
328        return $this;
329    }
335    public function withResolveLevel($resolveLevel)
336    {
337        /** @psalm-suppress UnsafeGenericInstantiation */
338        $collection = new static();
339        foreach ($this as $entity) {
340            $reduced = $entity->withResolveLevel($resolveLevel);
341            $collection[] = $reduced;
342        }
343        return $collection;
344    }
350    public function chunk(mixed $length): array
351    {
352        /** @psalm-suppress UnsafeGenericInstantiation */
353        $chunks = [new static()];
354        $id = 0;
355        foreach ($this as $entry) {
356            $chunkIndex = (int) floor($id / $length);
357            if (! isset($chunks[$chunkIndex])) {
358                /** @psalm-suppress UnsafeGenericInstantiation */
359                $chunks[$chunkIndex] = new static();
360            }
361
362            $chunks[$chunkIndex][] = $entry;
363
364            $id++;
365        }
366
367        return $chunks;
368    }