Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.49% covered (success)
99.49%
197 / 198
94.74% covered (success)
94.74%
18 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cluster
99.49% covered (success)
99.49%
197 / 198
94.74% covered (success)
94.74%
18 / 19
67
0.00% covered (danger)
0.00%
0 / 1
 readEntity
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 readResolvedReferences
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 readEntityWithOpenedScopeStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 readList
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 readByScopeId
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 readByDepartmentId
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 readQueueList
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 readOpenedScopeList
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 readEnabledScopeList
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 readScopeWithShortestWaitingTime
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 readWithScopeWorkstationCount
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 writeImageData
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 readImageData
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 deleteImage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 deleteEntity
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 writeEntity
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 updateEntity
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 writeAssignedScopes
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 removeCache
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3namespace BO\Zmsbackend\Cluster\Service;
4
5use BO\Zmsentities\Cluster as Entity;
6use BO\Zmsentities\Collection\ClusterList as Collection;
7
8/**
9 *
10 * @SuppressWarnings(Public)
11 * @SuppressWarnings(Coupling)
12 * @SuppressWarnings(Complexity)
13 *
14 */
15class Cluster extends \BO\Zmsbackend\Base
16{
17    /**
18     * @param false|int|string $itemId
19     *
20     */
21    public function readEntity(string|int|false $itemId, int $resolveReferences = 0, $disableCache = false)
22    {
23        $cacheKey = "cluster-$itemId-$resolveReferences";
24
25        $cluster = null;
26        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
27            $cluster = \App::$cache->get($cacheKey);
28        }
29
30        if (empty($cluster)) {
31            $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::SELECT);
32            $query
33                ->addEntityMapping()
34                ->addResolvedReferences($resolveReferences)
35                ->addConditionClusterId($itemId);
36            $cluster = $this->fetchOne($query, new Entity());
37            if (! $cluster->hasId()) {
38                return null;
39            }
40
41            if (\App::$cache) {
42                \App::$cache->set($cacheKey, $cluster);
43                if (\App::$log) {
44                    \App::$log->info('Cluster cache set', ['cache_key' => $cacheKey]);
45                }
46            }
47        }
48
49        return $this->readResolvedReferences($cluster, $resolveReferences, $disableCache);
50    }
51
52    /**
53     * @return \BO\Zmsentities\Schema\Entity
54     */
55    #[\Override]
56    public function readResolvedReferences(
57        \BO\Zmsentities\Schema\Entity $entity,
58        $resolveReferences,
59        $disableCache = false
60    ) {
61        $entity['scopes'] = (new \BO\Zmsbackend\Scope\Service\Scope())->readByClusterId($entity->id, $resolveReferences, $disableCache);
62
63        return $entity;
64    }
65
66    public function readEntityWithOpenedScopeStatus($itemId, \DateTimeInterface $now, $resolveReferences = 0)
67    {
68        $entity = $this->readEntity($itemId, $resolveReferences);
69        foreach ($entity->scopes as $scope) {
70            $scope->setStatusAvailability('isOpened', (new \BO\Zmsbackend\Scope\Service\Scope())->readIsOpened($scope->getId(), $now));
71        }
72        return $entity;
73    }
74
75    public function readList($resolveReferences = 0): Collection
76    {
77        $clusterList = new Collection();
78        $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::SELECT);
79        $query
80            ->addResolvedReferences($resolveReferences)
81            ->addEntityMapping();
82        $result = $this->fetchList($query, new Entity());
83        if (count($result)) {
84            foreach ($result as $entity) {
85                if ($entity instanceof Entity) {
86                    $entity = $this->readResolvedReferences($entity, $resolveReferences);
87                    $clusterList->addEntity($entity);
88                }
89            }
90        }
91        return $clusterList;
92    }
93
94    public function readByScopeId($scopeId, int $resolveReferences = 0)
95    {
96        $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::SELECT);
97        $query
98            ->addEntityMapping()
99            ->addResolvedReferences($resolveReferences)
100            ->addConditionScopeId($scopeId);
101        $entity = $this->fetchOne($query, new Entity());
102        if (! $entity->hasId()) {
103            return null;
104        }
105        $entity = $this->readResolvedReferences($entity, $resolveReferences);
106        return $entity;
107    }
108
109    public function readByDepartmentId($departmentId, $resolveReferences = 0, $disableCache = false): Collection
110    {
111        $clusterList = new Collection();
112        $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::SELECT);
113        $query
114            ->addEntityMapping()
115            ->addResolvedReferences($resolveReferences)
116            ->addConditionDepartmentId($departmentId);
117        $result = $this->fetchList($query, new Entity());
118        if (count($result)) {
119            foreach ($result as $entity) {
120                if ($entity instanceof Entity && !$clusterList->hasEntity($entity->id)) {
121                    $entity = $this->readResolvedReferences($entity, $resolveReferences, $disableCache);
122                    $clusterList->addEntity($entity);
123                }
124            }
125        }
126        return $clusterList;
127    }
128
129    /**
130     * @param string[] $withEntities
131     *
132     */
133    public function readQueueList(
134        $clusterId,
135        \DateTimeInterface $dateTime,
136        int $resolveReferences = 0,
137        array $withEntities = []
138    ) {
139        $cluster = $this->readEntity($clusterId, 1);
140
141        return (new \BO\Zmsbackend\Scope\Service\Scope())
142            ->readScopesQueueListWithWaitingTime($cluster->scopes, $dateTime, $resolveReferences, $withEntities)
143            ->withSortedWaitingTime();
144    }
145
146    public function readOpenedScopeList($clusterId, \DateTimeInterface $dateTime): \BO\Zmsentities\Collection\ScopeList
147    {
148        $scopeList = new \BO\Zmsentities\Collection\ScopeList();
149        $cluster = $this->readEntity($clusterId, 1);
150        if ($cluster && $cluster->toProperty()->scopes->get()) {
151            foreach ($cluster->scopes as $scope) {
152                $availabilityList = (new \BO\Zmsbackend\Availability\Service\Availability())->readOpeningHoursListByDate($scope['id'], $dateTime, 2);
153                if ($availabilityList->isOpened($dateTime)) {
154                    $scope->setStatusAvailability('isOpened', true);
155                    $scopeList->addEntity($scope);
156                }
157            }
158        }
159        return $scopeList;
160    }
161
162    public function readEnabledScopeList($clusterId, \DateTimeInterface $dateTime): \BO\Zmsentities\Collection\ScopeList
163    {
164        $scopeList = new \BO\Zmsentities\Collection\ScopeList();
165        foreach ($this->readOpenedScopeList($clusterId, $dateTime) as $scope) {
166            if ((new \BO\Zmsbackend\Scope\Service\Scope())->readIsGivenNumberInContingent($scope['id'])) {
167                $scopeList->addEntity($scope);
168            }
169        }
170        return $scopeList;
171    }
172
173    public function readScopeWithShortestWaitingTime($clusterId, \DateTimeInterface $dateTime)
174    {
175        $scopeList = $this->readOpenedScopeList($clusterId, $dateTime)->getArrayCopy();
176        $nextScope = array_shift($scopeList);
177        $preferedScope = null;
178        $preferedWaitingTime = 0;
179        while ($nextScope) {
180            $scope = (new \BO\Zmsbackend\Scope\Service\Scope())->readWithWorkstationCount($nextScope->id, $dateTime);
181            $queueList = (new \BO\Zmsbackend\Scope\Service\Scope())->readQueueListWithWaitingTime($scope, $dateTime);
182            $data = $scope->getWaitingTimeFromQueueList($queueList, $dateTime);
183            if (
184                $scope->getCalculatedWorkstationCount() > 0 &&
185                $data &&
186                ($data['waitingTimeEstimate'] <= $preferedWaitingTime || 0 == $preferedWaitingTime)
187            ) {
188                $preferedWaitingTime = $data['waitingTimeEstimate'];
189                $preferedScope = $scope;
190            }
191            $nextScope = array_shift($scopeList);
192        }
193        if (! $preferedScope) {
194            throw new \BO\Zmsbackend\Cluster\Exception\ScopesWithoutWorkstationCount();
195        }
196        return $preferedScope;
197    }
198
199    public function readWithScopeWorkstationCount($clusterId, $dateTime, $resolveReferences = 0)
200    {
201        $scopeQuery = new \BO\Zmsbackend\Scope\Service\Scope();
202        $scopeList = new \BO\Zmsentities\Collection\ScopeList();
203        $cluster = $this->readEntity($clusterId, $resolveReferences);
204        if ($cluster->toProperty()->scopes->get()) {
205            foreach ($cluster->scopes as $scope) {
206                $entity = $scopeQuery->readWithWorkstationCount($scope->id, $dateTime, $resolveReferences = 0);
207                if ($entity) {
208                    $scopeList->addEntity($entity);
209                }
210            }
211        }
212        $cluster->scopes = $scopeList;
213        return $cluster;
214    }
215
216    public function writeImageData($clusterId, \BO\Zmsentities\Mimepart $entity): \BO\Zmsentities\Mimepart
217    {
218        if ($entity->mime && $entity->content) {
219            $this->deleteImage($clusterId);
220            $extension = $entity->getExtension();
221            if ($extension == 'jpeg') {
222                $extension = 'jpg'; //compatibility ZMS1
223            }
224            $imageName = 'c_' . $clusterId . '_bild.' . $extension;
225            $this->perform(
226                (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::REPLACE))->getQueryWriteImageData(),
227                array(
228                'imagename' => $imageName,
229                'imagedata' => $entity->content
230                )
231            );
232        }
233        $entity->id = $clusterId;
234        return $entity;
235    }
236
237    public function readImageData($clusterId): \BO\Zmsentities\Mimepart
238    {
239        $imageName = 'c_' . $clusterId . '_bild';
240        $imageData = new \BO\Zmsentities\Mimepart();
241        $fileData = $this->getReader()->fetchAll(
242            (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQueryReadImageData(),
243            ['imagename' => "$imageName%"]
244        );
245        if ($fileData) {
246            $imageData->content = $fileData[0]['imagecontent'];
247            $imageData->mime = pathinfo($fileData[0]['imagename'])['extension'];
248        }
249        return $imageData;
250    }
251
252    public function deleteImage($clusterId)
253    {
254        $imageName = 'c_' . $clusterId . '_bild';
255        $result = $this->perform(
256            (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::DELETE))->getQueryDeleteImage(),
257            array(
258            'imagename' => "$imageName%"
259            )
260        );
261        return $result;
262    }
263
264    public function deleteEntity($itemId)
265    {
266        $result = false;
267        $cluster = $this->readEntity($itemId);
268        $query =  new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::DELETE);
269        $query->addConditionClusterId($itemId);
270        if ($this->deleteItem($query)) {
271            $result = $this->perform(
272                (new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::DELETE))->getQueryDeleteAssignedScopes(),
273                ['clusterId' => $itemId]
274            );
275        }
276
277        $this->removeCache($cluster);
278
279        return $result;
280    }
281
282    public function writeEntity(\BO\Zmsentities\Cluster $entity)
283    {
284        $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::INSERT);
285        $values = $query->reverseEntityMapping($entity);
286        $query->addValues($values);
287        $this->writeItem($query);
288        $lastInsertId = $this->getWriter()->lastInsertId();
289        if ($entity->toProperty()->scopes->isAvailable()) {
290            $this->writeAssignedScopes($lastInsertId, $entity->scopes);
291        }
292
293        $this->removeCache($entity);
294
295        return $this->readEntity($lastInsertId, 1, true);
296    }
297
298    public function updateEntity($clusterId, \BO\Zmsentities\Cluster $entity)
299    {
300        $query = new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::UPDATE);
301        $query->addConditionClusterId($clusterId);
302        $values = $query->reverseEntityMapping($entity);
303        $query->addValues($values);
304        $this->writeItem($query);
305        if ($entity->toProperty()->scopes->isAvailable()) {
306            $this->writeAssignedScopes($clusterId, $entity->scopes);
307        }
308
309        $this->removeCache($entity);
310
311        return $this->readEntity($clusterId, 1, true);
312    }
313
314    protected function writeAssignedScopes(string|false $clusterId, $scopeList): void
315    {
316        $cluster = $this->readEntity($clusterId);
317        $this->perform(
318            (new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::DELETE))->getQueryDeleteAssignedScopes(),
319            ['clusterId' => $clusterId]
320        );
321        foreach ($scopeList as $scope) {
322            if (0 < $scope['id']) {
323                $this->perform(
324                    (new \BO\Zmsbackend\Cluster\Repository\Cluster(\BO\Zmsbackend\Query\Base::REPLACE))->getQueryWriteAssignedScopes(),
325                    array(
326                    'clusterId' => $clusterId,
327                    'scopeId' => $scope['id']
328                    )
329                );
330            }
331        }
332
333        $this->removeCache($cluster);
334    }
335
336    /**
337     * @return void
338     */
339    public function removeCache(Entity $cluster)
340    {
341        if (!\App::$cache || !isset($cluster->id)) {
342            return;
343        }
344
345        $invalidatedKeys = [];
346
347        // Invalidate cluster entity cache for all resolveReferences levels (0, 1, 2)
348        for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
349            $key = "cluster-{$cluster->id}-{$resolveReferences}";
350            if (\App::$cache->has($key)) {
351                \App::$cache->delete($key);
352                $invalidatedKeys[] = $key;
353            }
354        }
355
356        // Invalidate scopeReadByClusterId cache for all resolveReferences levels (0, 1, 2)
357        for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
358            $key = "scopeReadByClusterId-{$cluster->id}-{$resolveReferences}";
359            if (\App::$cache->has($key)) {
360                \App::$cache->delete($key);
361                $invalidatedKeys[] = $key;
362            }
363        }
364
365        if (!empty($invalidatedKeys) && \App::$log) {
366            \App::$log->info('Cluster cache invalidated', [
367                'cluster_id' => $cluster->id,
368                'invalidated_keys' => $invalidatedKeys
369            ]);
370        }
371    }
372}