Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.26% covered (success)
90.26%
380 / 421
65.71% covered (warning)
65.71%
23 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
Scope
90.26% covered (success)
90.26%
380 / 421
65.71% covered (warning)
65.71%
23 / 35
165.69
0.00% covered (danger)
0.00%
0 / 1
 readEntity
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 readEntitiesByIds
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
10.04
 readResolvedReferences
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 readByClusterId
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
12
 readByProviderId
78.12% covered (warning)
78.12%
25 / 32
0.00% covered (danger)
0.00%
0 / 1
12.27
 readByRequestId
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 readByDepartmentId
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 readListBySource
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 testSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 readCollection
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 readList
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
10
 readIsOpened
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 readIsEnabled
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 readWaitingNumberUpdated
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 readDisplayNumberUpdated
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 readIsGivenNumberInContingent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 readQueueList
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 readWithWorkstationCount
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 readQueueListWithWaitingTime
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 readScopesQueueListWithWaitingTime
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 readListWithScopeAdminEmail
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 writeEntity
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 updateEntity
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 replacePreferences
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 updateGhostWorkstationCount
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 updateEmergency
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 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%
4 / 4
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
3
 deletePreferences
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 readDepartmentIdByScopeId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 readClusterIdsByScopeId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 removeCacheByContext
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
22
 removeCache
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BO\Zmsbackend\Scope\Service;
4
5use BO\Zmsentities\Scope as Entity;
6use BO\Zmsentities\Collection\ScopeList as Collection;
7
8/**
9 *
10 * @SuppressWarnings(Public)
11 * @SuppressWarnings(Coupling)
12 * @SuppressWarnings(Complexity)
13 * @SuppressWarnings(TooManyMethods)
14 *
15 */
16class Scope extends \BO\Zmsbackend\Base
17{
18    public static $cache = [ ];
19
20    /**
21     * @param false|int|string $scopeId
22     */
23    public function readEntity(string|int|false $scopeId, int $resolveReferences = 0, bool $disableCache = false): ?Entity
24    {
25        $cacheKey = "scope-$scopeId-$resolveReferences";
26
27        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
28            $scope = \App::$cache->get($cacheKey);
29        }
30
31        if (empty($scope)) {
32            $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
33            $query->addEntityMapping()
34                ->addResolvedReferences($resolveReferences)
35                ->addConditionScopeId($scopeId);
36            $scope = $this->fetchOne($query, new Entity());
37            if (! $scope->hasId()) {
38                return null;
39            }
40
41            if (\App::$cache) {
42                \App::$cache->set($cacheKey, $scope);
43                if (\App::$log) {
44                    \App::$log->info('Scope cache set', ['cache_key' => $cacheKey]);
45                }
46            }
47        }
48
49        return $this->readResolvedReferences($scope, $resolveReferences, $disableCache);
50    }
51
52    public function readEntitiesByIds(array $scopeIds, int $resolveReferences = 0, bool $disableCache = false): array
53    {
54        $scopeIds = array_values(array_unique(array_filter(array_map('intval', $scopeIds))));
55        if (!$scopeIds) {
56            return [];
57        }
58
59        $result  = [];
60        $missing = [];
61        foreach ($scopeIds as $scopeId) {
62            $cacheKey = "{$scopeId}-{$resolveReferences}";
63            if (!$disableCache && array_key_exists($cacheKey, self::$cache)) {
64                $result[$scopeId] = self::$cache[$cacheKey];
65            } else {
66                $missing[] = $scopeId;
67            }
68        }
69
70        if ($missing) {
71            $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
72            $query
73                ->addEntityMapping()
74                ->addResolvedReferences($resolveReferences)
75                ->addConditionScopeIds($missing);
76
77            $fetched = $this->fetchList($query, new Entity());
78            foreach ($fetched as $entity) {
79                if (!$entity->hasId()) {
80                    continue;
81                }
82                $entity = $this->readResolvedReferences($entity, $resolveReferences);
83                $result[$entity->id] = $entity;
84                self::$cache["{$entity->id}-{$resolveReferences}"] = $entity;
85            }
86        }
87
88        $ordered = [];
89        foreach ($scopeIds as $id) {
90            if (isset($result[$id])) {
91                $ordered[$id] = $result[$id];
92            }
93        }
94
95        return $ordered;
96    }
97
98    /**
99     * @return \BO\Zmsentities\Schema\Entity
100     */
101    #[\Override]
102    public function readResolvedReferences(
103        \BO\Zmsentities\Schema\Entity $entity,
104        int $resolveReferences,
105        $disableCache = false
106    ) {
107        if (0 < $resolveReferences) {
108            $entity['dayoff'] = (new \BO\Zmsbackend\Dayoff\Service\DayOff())->readByScopeId($entity->id, $disableCache);
109            $entity['closure'] = (new \BO\Zmsbackend\Availability\Service\Closure())->readByScopeId($entity->id, $disableCache);
110        }
111        return $entity;
112    }
113
114    public function readByClusterId(
115        $clusterId,
116        int $resolveReferences = 0,
117        $disableCache = false
118    ): Collection {
119        $cacheKey = "scopeReadByClusterId-$clusterId-$resolveReferences";
120
121        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
122            $result = \App::$cache->get($cacheKey);
123        }
124
125        if (empty($result)) {
126            if ($resolveReferences > 0) {
127                $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
128                $query->addEntityMapping()
129                    ->addResolvedReferences($resolveReferences - 1)
130                    ->addConditionClusterId($clusterId);
131                $result = $this->fetchList($query, new Entity());
132            } else {
133                $result = $this->getReader()->perform(
134                    (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQuerySimpleClusterMatch(),
135                    [$clusterId]
136                );
137            }
138
139            if (\App::$cache && !($result instanceof \PDOStatement)) {
140                \App::$cache->set($cacheKey, $result);
141                if (\App::$log) {
142                    \App::$log->info('Scope cache set', ['cache_key' => $cacheKey]);
143                }
144            }
145        }
146
147        $scopeList = new Collection();
148        if (!$result) {
149            return $scopeList;
150        }
151
152        foreach ($result as $entity) {
153            if (0 == $resolveReferences) {
154                $entity = new Entity(
155                    array(
156                        'id' => $entity['id'],
157                        'contact' => ['name' => $entity['contact__name']],
158                        'shortName' => $entity['shortName'] ?? '',
159                        '$ref' => '/scope/' . $entity['id'] . '/'
160                    )
161                );
162                $scopeList->addEntity($entity);
163            } else {
164                $scopeList->addEntity($this->readResolvedReferences(
165                    $entity,
166                    $resolveReferences - 1,
167                    $disableCache
168                ));
169            }
170        }
171
172        return $scopeList;
173    }
174
175    public function readByProviderId($providerId, int $resolveReferences = 0, $disableCache = false): Collection
176    {
177        $cacheKey = "scopeReadByProviderId-$providerId-$resolveReferences";
178
179        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
180            $result = \App::$cache->get($cacheKey);
181        }
182
183        if (empty($result)) {
184            $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
185            $query->addEntityMapping()
186                ->addResolvedReferences($resolveReferences)
187                ->addConditionProviderId($providerId);
188            $result = $this->fetchList($query, new Entity());
189
190            if (\App::$cache) {
191                \App::$cache->set($cacheKey, $result);
192                if (\App::$log) {
193                    \App::$log->info('Scope cache set', ['cache_key' => $cacheKey]);
194                }
195            }
196        }
197
198        $scopeList = new Collection();
199        if (count($result)) {
200            foreach ($result as $entity) {
201                if (0 == $resolveReferences) {
202                    $entity = new Entity(
203                        array(
204                            'id' => $entity->id,
205                            '$ref' => '/scope/' . $entity->id . '/'
206                        )
207                    );
208                    $scopeList->addEntity($entity);
209                } else {
210                    if ($entity instanceof Entity) {
211                        $entity = $this->readResolvedReferences(
212                            $entity,
213                            $resolveReferences - 1,
214                            $disableCache
215                        );
216                        $scopeList->addEntity($entity);
217                    }
218                }
219            }
220        }
221
222        return $scopeList;
223    }
224
225    public function readByRequestId($requestId, $source, $resolveReferences = 0): Collection
226    {
227        $scopeList = new Collection();
228        $providerList = (new \BO\Zmsbackend\Provider\Service\Provider())->readListBySource($source, 0, true, $requestId);
229
230        foreach ($providerList as $provider) {
231            $scopeListByProvider = $this->readByProviderId($provider->getId(), $resolveReferences);
232            if ($scopeListByProvider->count()) {
233                $scopeList->addList($scopeListByProvider);
234            }
235        }
236        return $scopeList->withUniqueScopes();
237    }
238
239    public function readByDepartmentId(int $departmentId, int $resolveReferences = 0, $disableCache = false): Collection
240    {
241        $cacheKey = "scopeReadByDepartmentId-$departmentId-$resolveReferences";
242
243        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
244            $result = \App::$cache->get($cacheKey);
245        }
246
247        $scopeList = new Collection();
248
249        if (empty($result)) {
250            if ($resolveReferences > 0) {
251                $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
252                $query->addEntityMapping()
253                    ->addResolvedReferences($resolveReferences)
254                    ->addConditionDepartmentId($departmentId);
255                $result = $this->fetchList($query, new Entity());
256
257                if (\App::$cache) {
258                    \App::$cache->set($cacheKey, $result);
259                    if (\App::$log) {
260                        \App::$log->info('Scope cache set', ['cache_key' => $cacheKey]);
261                    }
262                }
263            } else {
264                $result = $this->getReader()->perform(
265                    (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQuerySimpleDepartmentMatch(),
266                    [$departmentId]
267                );
268            }
269        }
270
271        if ($result) {
272            foreach ($result as $entity) {
273                if (0 == $resolveReferences) {
274                    $entity = new Entity(
275                        array(
276                            'id' => $entity['id'],
277                            'contact' => ['name' => $entity['contact__name']],
278                            'shortName' => $entity['shortName'] ?? '',
279                            '$ref' => '/scope/' . $entity['id'] . '/'
280                        )
281                    );
282                    $scopeList->addEntity($entity);
283                } else {
284                    if ($entity instanceof Entity) {
285                        $entity = $this->readResolvedReferences($entity, $resolveReferences, $disableCache);
286                        $scopeList->addEntity($entity);
287                    }
288                }
289            }
290        }
291
292        return $scopeList;
293    }
294    /** @psalm-api */
295    public function readListBySource($source, $resolveReferences = 0)
296    {
297        $this->testSource($source);
298        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
299        $query->setResolveLevel($resolveReferences);
300        $query->addConditionRequestSource($source);
301        $query->addEntityMapping();
302        $requestList = $this->readCollection($query);
303        return ($requestList->count()) ? $requestList->sortByCustomKey('id') : $requestList;
304    }
305
306    /**
307     * @return void
308     */
309    protected function testSource($source)
310    {
311        if (! (new \BO\Zmsbackend\Source\Service\Source())->readEntity($source)) {
312            throw new \BO\Zmsbackend\Source\Exception\UnknownDataSource();
313        }
314    }
315
316    protected function readCollection(\BO\Zmsbackend\Request\Repository\Request $query): Collection
317    {
318        $requestList = new Collection();
319        $statement = $this->fetchStatement($query);
320        while ($requestData = $statement->fetch(\PDO::FETCH_ASSOC)) {
321            $request = new Entity($query->postProcessJoins($requestData));
322            $requestList->addEntity($request);
323        }
324        return $requestList;
325    }
326
327    public function readList(int $resolveReferences = 0, $disableCache = false): Collection
328    {
329        $cacheKey = "scopeReadList-$resolveReferences";
330
331        if (!$disableCache && \App::$cache && \App::$cache->has($cacheKey)) {
332            $result = \App::$cache->get($cacheKey);
333        }
334
335        if (empty($result)) {
336            $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
337            $query->addEntityMapping()
338                ->addResolvedReferences($resolveReferences);
339            $result = $this->fetchList($query, new Entity());
340
341            if (\App::$cache) {
342                \App::$cache->set($cacheKey, $result);
343                if (\App::$log) {
344                    \App::$log->info('Scope cache set', ['cache_key' => $cacheKey]);
345                }
346            }
347        }
348
349        $scopeList = new Collection();
350        if (count($result)) {
351            foreach ($result as $entity) {
352                if ($entity instanceof Entity) {
353                    $entity = $this->readResolvedReferences($entity, $resolveReferences);
354                    $scopeList->addEntity($entity);
355                }
356            }
357        }
358        return $scopeList;
359    }
360
361    public function readIsOpened($scopeId, \DateTimeInterface $now): bool
362    {
363        $isOpened = false;
364        $availabilityList = (new \BO\Zmsbackend\Availability\Service\Availability())->readOpeningHoursListByDate($scopeId, $now, 2);
365        if ($availabilityList->isOpened($now)) {
366            $isOpened = true;
367        }
368        return $isOpened;
369    }
370
371    public function readIsEnabled(string $scopeId, \DateTimeInterface $now): bool
372    {
373        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
374        $query->addEntityMapping()
375            ->setResolveLevel(0)
376            ->addConditionScopeId($scopeId);
377        $scope = $this->fetchOne($query, new Entity());
378        return (
379            $this->readIsOpened($scopeId, $now) &&
380            $this->readIsGivenNumberInContingent($scopeId) &&
381            ! $scope->getStatus('ticketprinter', 'deactivated')
382        );
383    }
384
385    public function readWaitingNumberUpdated($scopeId, \DateTimeInterface $dateTime, bool $respectContingent = true)
386    {
387        if (! $this->readIsGivenNumberInContingent($scopeId) && $respectContingent) {
388            throw new \BO\Zmsbackend\Scope\Exception\GivenNumberCountExceeded();
389        }
390        $this->perform(
391            (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQueryLastWaitingNumber(),
392            ['scope_id' => $scopeId]
393        );
394        $entity = $this->readEntity($scopeId, 0, true)->updateStatusQueue($dateTime);
395        $scope = $this->updateEntity($scopeId, $entity);
396        return $scope->getStatus('queue', 'lastGivenNumber');
397    }
398
399    public function readDisplayNumberUpdated($scopeId)
400    {
401        $this->perform(
402            (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQueryLastDisplayNumber(),
403            ['scope_id' => $scopeId]
404        );
405        $entity = $this->readEntity($scopeId, 0, true)->incrementDisplayNumber();
406        $scope = $this->updateEntity($scopeId, $entity);
407        return $scope->getStatus('queue', 'lastDisplayNumber');
408    }
409
410    public function readIsGivenNumberInContingent($scopeId): bool
411    {
412        $isInContingent = $this->getReader()
413            ->fetchValue((new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))
414            ->getQueryGivenNumbersInContingent(), ['scope_id' => $scopeId]);
415        return ($isInContingent) ? true : false;
416    }
417
418    public function readQueueList(array $scopeIds, $dateTime, int $resolveReferences = 0, $withEntities = []): \BO\Zmsentities\Collection\QueueList
419    {
420        if ($resolveReferences > 0) {
421            $queueList = (new \BO\Zmsbackend\Process\Service\Process())
422                ->readProcessListByScopesAndTime(
423                    $scopeIds,
424                    $dateTime,
425                    $resolveReferences - 1,
426                    $withEntities
427                )
428                ->toQueueList($dateTime);
429        } else {
430            $queueList = (new \BO\Zmsbackend\Queue\Service\Queue())
431                ->readListByScopeAndTime($scopeIds, $dateTime, $resolveReferences);
432        }
433
434        return $queueList->withSortedArrival();
435    }
436
437    /**
438     * @param string[] $withEntities
439     *
440     */
441    public function readWithWorkstationCount(string $scopeId, \DateTimeInterface $dateTime, int $resolveReferences = 0, array $withEntities = [])
442    {
443        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT, '', false, null, $withEntities);
444        $query
445            ->addEntityMapping()
446            ->addConditionScopeId($scopeId)
447            ->addResolvedReferences($resolveReferences)
448            ->addSelectWorkstationCount($dateTime);
449        $scope = $this->fetchOne($query, new Entity());
450        $scope = $this->readResolvedReferences($scope, $resolveReferences);
451        return ($scope->hasId()) ? $scope : null;
452    }
453
454    /**
455     * @param string[] $withEntities
456     *
457     */
458    public function readQueueListWithWaitingTime(\BO\Zmsentities\Useraccount\AccessInterface $scope, \DateTimeInterface|false $dateTime, int $resolveReferences = 0, array $withEntities = [])
459    {
460        $timeAverage = (int) $scope->getPreference('queue', 'processingTimeAverage');
461        if ($timeAverage <= 0) {
462            $scope = (new \BO\Zmsbackend\Scope\Service\Scope())->readEntity($scope->id);
463            $timeAverage = (int) $scope->getPreference('queue', 'processingTimeAverage');
464        }
465        if ($timeAverage <= 0) {
466            $timeAverage = 12;
467        }
468        $queueList = $this->readQueueList([$scope->id], $dateTime, $resolveReferences, $withEntities);
469        $workstationCount = $scope->getCalculatedWorkstationCount();
470        return $queueList->withEstimatedWaitingTime($timeAverage, $workstationCount, $dateTime);
471    }
472
473    public function readScopesQueueListWithWaitingTime(Collection $scopes, \DateTimeInterface $dateTime, $resolveReferences = 0, $withEntities = [])
474    {
475        $timeSum = 0;
476        $workstationCount = 0;
477        $scopeIds = [];
478        foreach ($scopes as $scope) {
479            $timeSum += $scope->getPreference('queue', 'processingTimeAverage');
480            $workstationCount += $scope->getCalculatedWorkstationCount();
481            $scopeIds[] = $scope->id;
482        }
483
484        $timeAverage = (int) round($timeSum / $scopes->count());
485        if ($timeAverage <= 0) {
486            $timeAverage = 12;
487        }
488        $queueList = $this->readQueueList($scopeIds, $dateTime, $resolveReferences, $withEntities);
489
490        return $queueList->withEstimatedWaitingTime($timeAverage, $workstationCount, $dateTime);
491    }
492
493    public function readListWithScopeAdminEmail(int $resolveReferences = 0): Collection
494    {
495        $scopeList = new Collection();
496        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
497        $query
498            ->addEntityMapping()
499            ->addConditionWithAdminEmail()
500            ->addResolvedReferences($resolveReferences);
501        $result = $this->fetchList($query, new Entity());
502        if (count($result)) {
503            foreach ($result as $entity) {
504                if ($entity instanceof Entity) {
505                    $entity = $this->readResolvedReferences($entity, $resolveReferences);
506                    $scopeList->addEntity($entity);
507                }
508            }
509        }
510        return $scopeList;
511    }
512
513    public function writeEntity(\BO\Zmsentities\Scope $entity, $parentId): Entity|null
514    {
515        self::$cache = [];
516        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::INSERT);
517        $values = $query->reverseEntityMapping($entity, $parentId);
518        $query->addValues($values);
519        $this->writeItem($query);
520        $lastInsertId = $this->getWriter()
521            ->lastInsertId();
522        $this->replacePreferences($entity);
523
524        $this->removeCacheByContext($entity, $parentId);
525
526        return $this->readEntity($lastInsertId);
527    }
528
529    public function updateEntity($scopeId, \BO\Zmsentities\Scope $entity, $resolveReferences = 0): Entity|null
530    {
531        self::$cache = [];
532
533        $departmentId = $this->readDepartmentIdByScopeId($scopeId);
534
535        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::UPDATE);
536        $query->addConditionScopeId($scopeId);
537        $values = $query->reverseEntityMapping($entity);
538        $query->addValues($values);
539        $this->writeItem($query);
540        $this->replacePreferences($entity);
541
542        $this->removeCacheByContext($entity, $departmentId);
543
544        return $this->readEntity($scopeId, $resolveReferences, true);
545    }
546
547    public function replacePreferences(\BO\Zmsentities\Scope $entity): void
548    {
549        if (isset($entity['preferences'])) {
550            $preferenceQuery = new \BO\Zmsbackend\Preferences\Service\Preferences();
551            $entityName = 'scope';
552            $entityId = $entity['id'];
553            foreach ($entity['preferences'] as $groupName => $groupValues) {
554                foreach ($groupValues as $name => $value) {
555                    $preferenceQuery->replaceProperty($entityName, $entityId, $groupName, $name, $value);
556                }
557            }
558        }
559    }
560
561    public function updateGhostWorkstationCount(\BO\Zmsentities\Scope $entity, \DateTimeInterface $dateTime): Entity
562    {
563        $departmentId = $this->readDepartmentIdByScopeId($entity->id);
564
565        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::UPDATE);
566        $query->addConditionScopeId($entity->id);
567        $values = $query->setGhostWorkstationCountEntityMapping($entity, $dateTime);
568        $query->addValues($values);
569        $this->writeItem($query);
570
571        $this->removeCacheByContext($entity, $departmentId);
572
573        return $entity;
574    }
575
576    public function updateEmergency($scopeId, \BO\Zmsentities\Scope $entity): Entity|null
577    {
578        self::$cache = [];
579        $departmentId = $this->readDepartmentIdByScopeId($scopeId);
580
581        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::UPDATE);
582        $query->addConditionScopeId($scopeId);
583        $values = $query->setEmergencyEntityMapping($entity);
584        $query->addValues($values);
585        $this->writeItem($query);
586
587        $this->removeCacheByContext($entity, $departmentId);
588
589        return $this->readEntity($scopeId, 0, true);
590    }
591
592    public function writeImageData($scopeId, \BO\Zmsentities\Mimepart $entity): \BO\Zmsentities\Mimepart
593    {
594        if ($entity->mime && $entity->content) {
595            $this->deleteImage($scopeId);
596            $extension = $entity->getExtension();
597            if ($extension == 'jpeg') {
598                $extension = 'jpg'; //compatibility ZMS1
599            }
600            $imageName = 's_' . $scopeId . '_bild.' . $extension;
601            $this->getWriter()->perform(
602                (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::REPLACE))->getQueryWriteImageData(),
603                array(
604                    'imagename' => $imageName,
605                    'imagedata' => $entity->content
606                )
607            );
608        }
609        $entity->id = $scopeId;
610        return $entity;
611    }
612
613    public function readImageData($scopeId): \BO\Zmsentities\Mimepart
614    {
615        $imageName = 's_' . $scopeId . '_bild';
616        $imageData = new \BO\Zmsentities\Mimepart();
617        $fileData = $this->getReader()->fetchAll(
618            (new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT))->getQueryReadImageData(),
619            ['imagename' => "$imageName%"]
620        );
621        if ($fileData) {
622            $imageData->content = $fileData[0]['imagecontent'];
623            $imageData->mime = pathinfo($fileData[0]['imagename'])['extension'];
624        }
625        return $imageData;
626    }
627
628    public function deleteImage($scopeId)
629    {
630        $imageName = 's_' . $scopeId . '_bild';
631        return $this->perform((new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::DELETE))->getQueryDeleteImage(), array(
632            'imagename' => "$imageName%"
633        ));
634    }
635
636    public function deleteEntity($scopeId): Entity|null
637    {
638        $processListCount = (new \BO\Zmsbackend\Process\Service\Process())->readProcessListCountByScope($scopeId);
639        if (0 < $processListCount) {
640            throw new \BO\Zmsbackend\Scope\Exception\ScopeHasProcesses();
641        }
642        self::$cache = [];
643
644        $departmentId = $this->readDepartmentIdByScopeId($scopeId);
645
646        $entity = $this->readEntity($scopeId);
647        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::DELETE);
648        $query->addConditionScopeId($scopeId);
649        $this->deletePreferences($entity);
650
651        $this->removeCacheByContext($entity, $departmentId);
652
653        return ($this->deleteItem($query)) ? $entity : null;
654    }
655
656    public function deletePreferences(\BO\Zmsentities\Scope $entity): void
657    {
658        $preferenceQuery = new \BO\Zmsbackend\Preferences\Service\Preferences();
659        $entityName = 'scope';
660        $entityId = $entity['id'];
661        foreach ($entity['preferences'] as $groupName => $groupValues) {
662            foreach (array_keys($groupValues) as $name) {
663                $preferenceQuery->deleteProperty($entityName, $entityId, $groupName, $name);
664            }
665        }
666    }
667
668    public function readDepartmentIdByScopeId(int $scopeId)
669    {
670        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
671        return $this->getReader()->fetchValue($query->getQueryDepartmentIdByScopeId(), [$scopeId]);
672    }
673
674    public function readClusterIdsByScopeId($scopeId): array
675    {
676        $query = new \BO\Zmsbackend\Query\Scope(\BO\Zmsbackend\Query\Base::SELECT);
677        $result = $this->getReader()->fetchAll($query->getQueryClusterIdsByScopeId(), [$scopeId]);
678        return array_column($result, 'clusterID');
679    }
680
681    /**
682     * @return void
683     */
684    public function removeCacheByContext(Entity $scope, $departmentId = null)
685    {
686        if (!\App::$cache) {
687            return;
688        }
689
690        $invalidatedKeys = [];
691
692        // Invalidate scope entity cache for all resolveReferences levels (0, 1, 2)
693        if (isset($scope->id)) {
694            for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
695                $key = "scope-{$scope->id}-{$resolveReferences}";
696                if (\App::$cache->has($key)) {
697                    \App::$cache->delete($key);
698                    $invalidatedKeys[] = $key;
699                }
700            }
701        }
702
703        // Invalidate scope list cache for all resolveReferences levels (0, 1, 2)
704        for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
705            $key = "scopeReadList-{$resolveReferences}";
706            if (\App::$cache->has($key)) {
707                \App::$cache->delete($key);
708                $invalidatedKeys[] = $key;
709            }
710        }
711
712        // Invalidate scopeReadByDepartmentId cache for all resolveReferences levels (0, 1, 2)
713        if ($departmentId) {
714            for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
715                $key = "scopeReadByDepartmentId-{$departmentId}-{$resolveReferences}";
716                if (\App::$cache->has($key)) {
717                    \App::$cache->delete($key);
718                    $invalidatedKeys[] = $key;
719                }
720            }
721        }
722
723        // Invalidate scopeReadByClusterId cache for all clusters containing this scope
724        if (isset($scope->id)) {
725            $clusterIds = $this->readClusterIdsByScopeId($scope->id);
726            foreach ($clusterIds as $clusterId) {
727                for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
728                    $key = "scopeReadByClusterId-{$clusterId}-{$resolveReferences}";
729                    if (\App::$cache->has($key)) {
730                        \App::$cache->delete($key);
731                        $invalidatedKeys[] = $key;
732                    }
733                }
734            }
735        }
736
737        // Invalidate scopeReadByProviderId cache for all resolveReferences levels (0, 1, 2)
738        if (isset($scope->provider) && isset($scope->provider['id']) && $scope->provider['id']) {
739            $providerId = $scope->provider['id'];
740            for ($resolveReferences = 0; $resolveReferences <= 2; $resolveReferences++) {
741                $key = "scopeReadByProviderId-{$providerId}-{$resolveReferences}";
742                if (\App::$cache->has($key)) {
743                    \App::$cache->delete($key);
744                    $invalidatedKeys[] = $key;
745                }
746            }
747        }
748
749        if (!empty($invalidatedKeys) && \App::$log) {
750            \App::$log->info('Scope cache invalidated', [
751                'scope_id' => isset($scope->id) ? $scope->id : 'unknown',
752                'invalidated_keys' => $invalidatedKeys
753            ]);
754        }
755    }
756
757    /**
758     * @psalm-api
759     */
760    public function removeCache($scope): void
761    {
762        $departmentId = isset($scope->id) ? $this->readDepartmentIdByScopeId($scope->id) : null;
763        $this->removeCacheByContext($scope, $departmentId);
764    }
765}