Lines 91.44% 492 / 538
Methods 75.92% 41 / 54
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 readEntity 100.00% 16 / 16 100.00% 1 / 1 8
 readEntitiesByIds 92.85% 26 / 28 0.00% 0 / 1 10.04
 readResolvedReferences 100.00% 4 / 4 100.00% 1 / 1 2
 readByClusterId 100.00% 38 / 38 100.00% 1 / 1 12
 readByProviderId 78.12% 25 / 32 0.00% 0 / 1 12.27
 readByRequestId 100.00% 7 / 7 100.00% 1 / 1 3
 readByDepartmentId 97.14% 34 / 35 0.00% 0 / 1 12
 readListBySource 0.00% 0 / 7 0.00% 0 / 1 6
 testSource 0.00% 0 / 2 0.00% 0 / 1 6
 readCollection 0.00% 0 / 6 0.00% 0 / 1 6
 readList 100.00% 19 / 19 100.00% 1 / 1 10
 readIsOpened 100.00% 5 / 5 100.00% 1 / 1 2
 readIsEnabled 100.00% 10 / 10 100.00% 1 / 1 3
 readWaitingNumberUpdated 100.00% 9 / 9 100.00% 1 / 1 3
 readDisplayNumberUpdated 0.00% 0 / 7 0.00% 0 / 1 2
 readIsGivenNumberInContingent 100.00% 4 / 4 100.00% 1 / 1 2
 readQueueList 100.00% 12 / 12 100.00% 1 / 1 2
 readWithWorkstationCount 100.00% 9 / 9 100.00% 1 / 1 2
 readQueueListWithWaitingTime 66.66% 6 / 9 0.00% 0 / 1 3.33
 readScopesQueueListWithWaitingTime 91.66% 11 / 12 0.00% 0 / 1 3.01
 readListWithScopeAdminEmail 100.00% 13 / 13 100.00% 1 / 1 4
 writeEntity 100.00% 10 / 10 100.00% 1 / 1 1
 updateEntity 100.00% 10 / 10 100.00% 1 / 1 1
 replacePreferences 100.00% 7 / 7 100.00% 1 / 1 4
 updateGhostWorkstationCount 100.00% 8 / 8 100.00% 1 / 1 1
 updateEmergency 100.00% 9 / 9 100.00% 1 / 1 1
 writeImageData 93.33% 14 / 15 0.00% 0 / 1 4.00
 readImageData 100.00% 10 / 10 100.00% 1 / 1 2
 deleteImage 100.00% 4 / 4 100.00% 1 / 1 1
 deleteEntity 100.00% 11 / 11 100.00% 1 / 1 3
 deletePreferences 100.00% 6 / 6 100.00% 1 / 1 3
 readDepartmentIdByScopeId 100.00% 2 / 2 100.00% 1 / 1 1
 readClusterIdsByScopeId 100.00% 3 / 3 100.00% 1 / 1 1
 removeCacheByContext 95.00% 38 / 40 0.00% 0 / 1 22
 removeCache 0.00% 0 / 2 0.00% 0 / 1 6
 [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] hashStringValue 50.00% 5 / 10 0.00% 0 / 1 8.12
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        bool $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        bool $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, bool $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, int $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, bool $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, int $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, bool $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, int $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, int $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}

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    }
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    }