Lines 80.94% 361 / 446
Methods 69.56% 32 / 46
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 readEntity 94.44% 17 / 18 0.00% 0 / 1 6.01
 readEntityById 0.00% 0 / 15 0.00% 0 / 1 56
 readSourceMapByIds 90.90% 10 / 11 0.00% 0 / 1 3.01
 readCollection 100.00% 6 / 6 100.00% 1 / 1 2
 attachRootParentIds 60.00% 15 / 25 0.00% 0 / 1 16.40
 readEntityWithoutRootParentResolution 0.00% 0 / 11 0.00% 0 / 1 6
 resolveRootParentId 40.00% 6 / 15 0.00% 0 / 1 17.58
 getRequestLookupKey 100.00% 1 / 1 100.00% 1 / 1 1
 readRequestByProcessId 100.00% 8 / 8 100.00% 1 / 1 2
 readAllRequestsForProcessIds 95.83% 23 / 24 0.00% 0 / 1 7
 readRequestsByIds 100.00% 5 / 5 100.00% 1 / 1 1
 readRequestByArchiveId 100.00% 8 / 8 100.00% 1 / 1 2
 readRequestIdsByArchiveId 90.00% 9 / 10 0.00% 0 / 1 9.08
 readListByProvider 100.00% 12 / 12 100.00% 1 / 1 2
 readListByScopeAndDepartment 85.71% 24 / 28 0.00% 0 / 1 5.07
 readDepartmentProviders 58.82% 10 / 17 0.00% 0 / 1 5.12
 readAllRequestsForProviders 100.00% 14 / 14 100.00% 1 / 1 4
 keepRequestsNotAlreadyOnScope 76.92% 10 / 13 0.00% 0 / 1 6.44
 readListBySource 84.61% 11 / 13 0.00% 0 / 1 6.13
 readListByCluster 100.00% 14 / 14 100.00% 1 / 1 5
 writeEntity 100.00% 16 / 16 100.00% 1 / 1 2
 writeListBySource 100.00% 5 / 5 100.00% 1 / 1 2
 writeImportEntity 100.00% 14 / 14 100.00% 1 / 1 4
 writeDeleteEntity 100.00% 4 / 4 100.00% 1 / 1 1
 writeDeleteListBySource 100.00% 3 / 3 100.00% 1 / 1 1
 testSource 100.00% 2 / 2 100.00% 1 / 1 2
 removeCache 11.76% 2 / 17 0.00% 0 / 1 64.64
 [BO\Zmsbackend\Base] __construct 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] init 100.00% 2 / 2 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] getWriter 100.00% 4 / 4 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] getReader 100.00% 5 / 5 100.00% 1 / 1 3
 [BO\Zmsbackend\Base] fetchPreparedStatement 100.00% 9 / 9 100.00% 1 / 1 3
 [BO\Zmsbackend\Base] startExecute 100.00% 5 / 5 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] pdoExceptionHandler 100.00% 13 / 13 100.00% 1 / 1 4
 [BO\Zmsbackend\Base] fetchStatement 100.00% 3 / 3 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchOne 100.00% 7 / 7 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] fetchRow 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchValue 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchAll 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchHandle 100.00% 5 / 5 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchList 100.00% 8 / 8 100.00% 1 / 1 2
 [BO\Zmsbackend\Base] writeItem 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] deleteItem 100.00% 1 / 1 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] perform 100.00% 7 / 7 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] fetchAffected 100.00% 8 / 8 100.00% 1 / 1 1
 [BO\Zmsbackend\Base] readResolvedReferences n/a 0 / 0 n/a 0 / 0 1
 [BO\Zmsbackend\Base] hashStringValue 50.00% 5 / 10 0.00% 0 / 1 8.12
14class Request extends \BO\Zmsbackend\Base
15{
16    public function readEntity(string $source, string $requestId, int|null $resolveReferences = 0, bool $disableCache = false)
17    {
18        $cacheKey = "request-$source-$requestId-$resolveReferences";
19
20        if (!$disableCache && App::$cache && App::$cache->has($cacheKey)) {
21            return App::$cache->get($cacheKey);
22        }
23
24        $this->testSource($source);
25        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
26        $query
27            ->setResolveLevel($resolveReferences)
28            ->addEntityMapping()
29            ->addResolvedReferences($resolveReferences)
30            ->addConditionRequestSource($source)
31            ->addConditionRequestId($requestId);
32        $request = $this->fetchOne($query, new Entity());
33        if (! $request->hasId()) {
34            throw new \BO\Zmsbackend\Request\Exception\RequestNotFound("Could not find request with ID $source/$requestId");
35        }
36
37        $this->attachRootParentIds(new Collection([$request]));
38
39        if (App::$cache) {
40            App::$cache->set($cacheKey, $request);
41        }
42
43        return $request;
44    }
45
46    /** @psalm-api */
47    public function readEntityById($requestId, int $resolveReferences = 0, bool $disableCache = false)
48    {
49        $cacheKey = "request-byid-$requestId-$resolveReferences";
50
51        if (!$disableCache && App::$cache && App::$cache->has($cacheKey)) {
52            return App::$cache->get($cacheKey);
53        }
54
55        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
56        $query
57            ->setResolveLevel($resolveReferences)
58            ->addEntityMapping()
59            ->addResolvedReferences($resolveReferences)
60            ->addConditionRequestId($requestId);
61        $request = $this->fetchOne($query, new Entity());
62
63        if ($request->hasId()) {
64            $this->attachRootParentIds(new Collection([$request]));
65        }
66
67        if (App::$cache && $request->hasId()) {
68            App::$cache->set($cacheKey, $request);
69        }
70
71        return $request;
72    }
73
74    /**
75     * @return array<string, string>
76     */
77    public function readSourceMapByIds(array $requestIds): array
78    {
79        if ($requestIds === []) {
80            return [];
81        }
82
83        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
84        $query
85            ->setResolveLevel(0)
86            ->addEntityMapping()
87            ->addConditionRequestIdList($requestIds);
88
89        $map = [];
90        foreach ($this->readCollection($query) as $request) {
91            $map[(string) $request->getId()] = (string) $request->getSource();
92        }
93
94        return $map;
95    }
96
97    /**
98     * @SuppressWarnings(Param)
99     */
100    protected function readCollection(\BO\Zmsbackend\Request\Repository\Request $query): Collection
101    {
102        $requestList = new Collection();
103        $statement = $this->fetchStatement($query);
104        while ($requestData = $statement->fetch(\PDO::FETCH_ASSOC)) {
105            $request = new Entity($query->postProcessJoins($requestData));
106            $requestList->addEntity($request);
107        }
108
109        return $this->attachRootParentIds($requestList);
110    }
111
112    protected function attachRootParentIds(Collection $requestList): Collection
113    {
114        if (!$requestList->count()) {
115            return $requestList;
116        }
117
118        $lookup = [];
119        foreach ($requestList as $request) {
120            $lookup[$this->getRequestLookupKey($request)] = $request;
121        }
122
123        $changed = true;
124        while ($changed) {
125            $changed = false;
126            foreach ($lookup as $request) {
127                $parentId = $request->getParentId();
128                if ($parentId === null || $parentId === '') {
129                    continue;
130                }
131
132                $parentKey = $request->getSource() . ':' . $parentId;
133                if (isset($lookup[$parentKey])) {
134                    continue;
135                }
136
137                try {
138                    $lookup[$parentKey] = $this->readEntityWithoutRootParentResolution(
139                        $request->getSource(),
140                        $parentId
141                    );
142                    $changed = true;
143                } catch (\Throwable $exception) {
144                    continue;
145                }
146            }
147        }
148
149        foreach ($requestList as $request) {
150            $request['root_parent_id'] = $this->resolveRootParentId($request, $lookup);
151        }
152
153        return $requestList;
154    }
155
156    protected function readEntityWithoutRootParentResolution($source, $requestId): Entity
157    {
158        $this->testSource($source);
159        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
160        $query
161            ->setResolveLevel(0)
162            ->addEntityMapping()
163            ->addConditionRequestSource($source)
164            ->addConditionRequestId($requestId);
165
166        $request = $this->fetchOne($query, new Entity());
167        if (!$request->hasId()) {
168            throw new \BO\Zmsbackend\Request\Exception\RequestNotFound("Could not find request with ID $source/$requestId");
169        }
170
171        return $request;
172    }
173
174    protected function resolveRootParentId(Entity $request, array $lookup): string
175    {
176        $current = $request;
177        $visited = [];
178
179        while ($current->getParentId() !== null && $current->getParentId() !== '') {
180            $parentKey = $current->getSource() . ':' . $current->getParentId();
181            if (isset($visited[$parentKey])) {
182                break;
183            }
184            $visited[$parentKey] = true;
185
186            $parent = $lookup[$parentKey] ?? null;
187            if (!$parent instanceof Entity) {
188                break;
189            }
190            $current = $parent;
191        }
192
193        $parentId = $current->getParentId();
194        if ($parentId !== null && $parentId !== '') {
195            return (string) $parentId;
196        }
197
198        return (string) $current->getId();
199    }
200
201    protected function getRequestLookupKey(Entity $request): string
202    {
203        return $request->getSource() . ':' . $request->getId();
204    }
205
206    public function readRequestByProcessId($processId, int $resolveReferences = 0)
207    {
208        $collection = new Collection();
209        if ($processId) {
210            $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
211            $query->setResolveLevel($resolveReferences);
212            $query->addConditionProcessId($processId);
213            $query->addEntityMapping();
214            $collection = $this->readCollection($query);
215        }
216        return $collection;
217    }
218
219    public function readAllRequestsForProcessIds(array $processIds, int $resolveReferences = 0): array
220    {
221        $processIds = array_values(array_unique(array_filter(
222            array_map('intval', $processIds),
223            static fn (int $id): bool => $id > 0
224        )));
225        if ($processIds === []) {
226            return [];
227        }
228
229        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
230        $query->setResolveLevel($resolveReferences);
231        $query->addConditionProcessIds($processIds);
232        $query->addEntityMappingWithProcessId();
233
234        $requestsByProcessId = [];
235        foreach ($processIds as $processId) {
236            $requestsByProcessId[$processId] = new Collection();
237        }
238
239        $statement = $this->fetchStatement($query);
240        while ($requestData = $statement->fetch(\PDO::FETCH_ASSOC)) {
241            $processed = $query->postProcessJoins($requestData);
242            $processId = (int) ($processed['processId'] ?? 0);
243            unset($processed['processId']);
244            if ($processId <= 0 || !isset($requestsByProcessId[$processId])) {
245                continue;
246            }
247            $requestsByProcessId[$processId]->addEntity(new Entity($processed));
248        }
249
250        foreach ($requestsByProcessId as $processId => $requestList) {
251            $requestsByProcessId[$processId] = $this->attachRootParentIds($requestList);
252        }
253
254        return $requestsByProcessId;
255    }
256
257    public function readRequestsByIds($ids, int $resolveReferences = 0)
258    {
259        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
260        $query->setResolveLevel($resolveReferences);
261        $query->addConditionIds($ids);
262        $query->addEntityMapping();
263
264        return $this->readCollection($query);
265    }
266
267    public function readRequestByArchiveId($archiveId, int $resolveReferences = 0)
268    {
269        $collection = new Collection();
270        if ($archiveId) {
271            $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
272            $query->setResolveLevel($resolveReferences);
273            $query->addConditionArchiveId($archiveId);
274            $query->addEntityMapping();
275            $collection = $this->readCollection($query);
276        }
277        return $collection;
278    }
279
280    public function readRequestIdsByArchiveId(int|string|null $archiveId): array
281    {
282        if ($archiveId === null || $archiveId === '' || $archiveId === 0 || $archiveId === '0') {
283            return [];
284        }
285
286        $ids = [];
287        foreach (
288            $this->fetchAll(
289                \BO\Zmsbackend\Request\Repository\Request::QUERY_BY_ARCHIVEID,
290                ['archive_id' => $archiveId]
291            ) as $row
292        ) {
293            if (array_key_exists('id', $row) && $row['id'] !== null && $row['id'] !== '') {
294                $ids[] = (int) $row['id'];
295            }
296        }
297
298        return $ids;
299    }
300
301    public function readListByProvider($source, $providerId, int $resolveReferences = 0)
302    {
303        $this->testSource($source);
304        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
305        $requestRelationQuery = new \BO\Zmsbackend\RequestRelation\Service\RequestRelation();
306        $query->setResolveLevel($resolveReferences);
307        $query->addConditionProvider($providerId, $source);
308        $query->addConditionRequestSource($source);
309        $query->addEntityMapping();
310        $requestList = $this->readCollection($query);
311        foreach ($requestList as $request) {
312            $requestRelation = $requestRelationQuery->readEntity($request->getId(), $providerId);
313            $request['timeSlotCount'] = $requestRelation->getSlotCount();
314        }
315        return $requestList;
316    }
317
318    public function readListByScopeAndDepartment(int $scopeId, int $resolveReferences = 0): array
319    {
320        $scopeService = new \BO\Zmsbackend\Scope\Service\Scope();
321        $scope = $scopeService->readEntity($scopeId, $resolveReferences ? $resolveReferences : 1);
322        if (!$scope || !$scope->hasId()) {
323            throw new \BO\Zmsbackend\Scope\Exception\ScopeNotFound();
324        }
325
326        $scopeRequestList = $this->readListByProvider(
327            $scope->provider['source'],
328            $scope->getProviderId(),
329            $resolveReferences
330        );
331
332        $departmentId = (int) $scopeService->readDepartmentIdByScopeId($scopeId);
333        if ($departmentId < 1) {
334            return [
335                'scopeRequests' => $scopeRequestList,
336                'additionalDepartmentRequests' => new Collection(),
337            ];
338        }
339
340        $providers = $this->readDepartmentProviders(
341            $scopeService->readByDepartmentId($departmentId, 1),
342            $departmentId
343        );
344        $departmentRequestList = $this->readAllRequestsForProviders($providers, $resolveReferences);
345        $additionalDepartmentRequestList = $this->keepRequestsNotAlreadyOnScope(
346            $scopeRequestList,
347            $departmentRequestList
348        );
349
350        return [
351            'scopeRequests' => $scopeRequestList,
352            'additionalDepartmentRequests' => $additionalDepartmentRequestList->sortByName(),
353        ];
354    }
355
356    protected function readDepartmentProviders(
357        \BO\Zmsentities\Collection\ScopeList $departmentScopes,
358        int $departmentId
359    ): array {
360        $providers = [];
361        foreach ($departmentScopes as $departmentScope) {
362            if (!$departmentScope instanceof Scope) {
363                continue;
364            }
365            try {
366                $provider = $departmentScope->getProvider();
367                $key = $provider->source . ':' . $provider->id;
368                $providers[$key] = [
369                    'source' => (string) $provider->source,
370                    'id' => $provider->id,
371                ];
372            } catch (\BO\Zmsentities\Exception\ScopeMissingProvider $exception) {
373                App::$log->warning('Skipping department scope without provider for request list', [
374                    'scopeId' => $departmentScope->id ?? null,
375                    'departmentId' => $departmentId,
376                    'error' => $exception->getMessage(),
377                ]);
378            }
379        }
380
381        return $providers;
382    }
383
384    protected function readAllRequestsForProviders(array $providers, int $resolveReferences): Collection
385    {
386        $requestList = new Collection();
387        $requestKeys = [];
388        foreach ($providers as $provider) {
389            $providerRequests = $this->readListByProvider(
390                $provider['source'],
391                $provider['id'],
392                $resolveReferences
393            );
394            foreach ($providerRequests as $request) {
395                $requestKey = $this->getRequestLookupKey($request);
396                if (!isset($requestKeys[$requestKey])) {
397                    $requestKeys[$requestKey] = true;
398                    $requestList->addEntity(clone $request);
399                }
400            }
401        }
402
403        return $requestList;
404    }
405
406    protected function keepRequestsNotAlreadyOnScope(
407        Collection $scopeRequestList,
408        Collection $departmentRequestList
409    ): Collection {
410        $additionalDepartmentRequestList = new Collection();
411        $scopeRequestKeys = [];
412        foreach ($scopeRequestList as $request) {
413            if (!$request instanceof Entity) {
414                continue;
415            }
416            $scopeRequestKeys[$this->getRequestLookupKey($request)] = true;
417        }
418        foreach ($departmentRequestList as $request) {
419            if (!$request instanceof Entity) {
420                continue;
421            }
422            $requestKey = $this->getRequestLookupKey($request);
423            if (!isset($scopeRequestKeys[$requestKey])) {
424                $additionalDepartmentRequestList->addEntity(clone $request);
425            }
426        }
427
428        return $additionalDepartmentRequestList;
429    }
430
431    public function readListBySource($source, int $resolveReferences = 0, bool $disableCache = false)
432    {
433        $cacheKey = "requestReadListBySource-$source-$resolveReferences";
434
435        if (!$disableCache && App::$cache && App::$cache->has($cacheKey)) {
436            return App::$cache->get($cacheKey);
437        }
438
439        $this->testSource($source);
440        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
441        $query->setResolveLevel($resolveReferences);
442        $query->addConditionRequestSource($source);
443        $query->addEntityMapping();
444        $requestList = $this->readCollection($query);
445        $requestList = ($requestList->count()) ? $requestList->sortByCustomKey('id') : $requestList;
446
447        if (App::$cache) {
448            App::$cache->set($cacheKey, $requestList);
449        }
450
451        return $requestList;
452    }
453
454    public function readListByCluster(\BO\Zmsentities\Cluster $cluster, int $resolveReferences = 0): Collection
455    {
456        $intersectList = array();
457        if ($cluster->scopes->count()) {
458            foreach ($cluster->scopes as $scope) {
459                $requestsByProvider = $this
460                    ->readListByProvider(
461                        $scope->provider['source'],
462                        $scope->getProviderId(),
463                        $resolveReferences - 1
464                    )->getArrayCopy();
465                if (count($requestsByProvider)) {
466                    $intersectList = (count($intersectList)) ?
467                        array_values(array_intersect($intersectList, $requestsByProvider)) : $requestsByProvider;
468                }
469            }
470        }
471        $requestList = new Collection($intersectList);
472        return $requestList;
473    }
474
475    public function writeEntity(Entity $entity)
476    {
477        $additionalData = ($entity->getAdditionalData()) ? json_encode($entity->getAdditionalData()) : '{}';
478        $this->writeDeleteEntity($entity->getId(), $entity->getSource());
479        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::INSERT);
480        $query->addValues([
481            'source' => $entity->getSource(),
482            'id' => $entity->getId(),
483            'name' => $entity->getName(),
484            'parent_id' => $entity->getParentId(),
485            'group' => $entity->getGroup(),
486            'link' =>  $entity->getLink(),
487            'data' => $additionalData,
488            'variant_id' => $entity->getVariantId()
489        ]);
490        $this->writeItem($query);
491
492        $this->removeCache($entity);
493
494        return $this->readEntity($entity->getSource(), $entity->getId());
495    }
496
497    public function writeListBySource(\BO\Zmsentities\Source $source)
498    {
499        $this->writeDeleteListBySource($source->getSource());
500        foreach ($source->getRequestList() as $request) {
501            $this->writeEntity($request);
502            $this->removeCache($request);
503        }
504        return $this->readListBySource($source->getSource());
505    }
506
507    public function writeImportEntity($request, $source = 'dldb')
508    {
509        (new \BO\Zmsentities\Request($request))->testValid();
510
511        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::REPLACE);
512        $query->addValues([
513            'source' => $source,
514            'id' => $request['id'],
515            'name' => $request['name'],
516            'group' => (isset($request['group'])) ? $request['group'] : 'Sonstiges',
517            'link' => ('dldb' == $source)
518                ? 'https://service.berlin.de/dienstleistung/' . $request['id'] . '/'
519                : ((isset($request['link'])) ? $request['link'] : ''),
520            'data' => json_encode($request)
521        ]);
522        $this->writeItem($query);
523        return $this->readEntity($source, $request['id'], 0, true);
524    }
525
526    public function writeDeleteEntity($requestId, $source): bool
527    {
528        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::DELETE);
529        $query->addConditionRequestId($requestId);
530        $query->addConditionRequestSource($source);
531
532        return $this->deleteItem($query);
533    }
534
535    public function writeDeleteListBySource(string $source): bool
536    {
537        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::DELETE);
538        $query->addConditionRequestSource($source);
539        return $this->deleteItem($query);
540    }
541
542    /**
543     * @return void
544     */
545    protected function testSource($source)
546    {
547        if (! (new \BO\Zmsbackend\Source\Service\Source())->readEntity($source)) {
548            throw new \BO\Zmsbackend\Source\Exception\UnknownDataSource();
549        }
550    }
551
552    /**
553     * @return void
554     */
555    public function removeCache(Entity $request)
556    {
557        if (!App::$cache) {
558            return;
559        }
560
561        $source = $request->getSource();
562
563        if (isset($request->id)) {
564            $requestId = $request->getId();
565            if (App::$cache->has("request-$source-$requestId-0")) {
566                App::$cache->delete("request-$source-$requestId-0");
567            }
568
569            if (App::$cache->has("request-$source-$requestId-1")) {
570                App::$cache->delete("request-$source-$requestId-1");
571            }
572
573            if (App::$cache->has("request-$source-$requestId-2")) {
574                App::$cache->delete("request-$source-$requestId-2");
575            }
576        }
577
578        if (App::$cache->has("requestReadListBySource-$source-0")) {
579            App::$cache->delete("requestReadListBySource-$source-0");
580        }
581
582        if (App::$cache->has("requestReadListBySource-$source-1")) {
583            App::$cache->delete("requestReadListBySource-$source-1");
584        }
585
586        if (App::$cache->has("requestReadListBySource-$source-2")) {
587            App::$cache->delete("requestReadListBySource-$source-2");
588        }
589    }
590}

Inherited from BO\Zmsbackend\Base

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