Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.61% covered (warning)
75.61%
248 / 328
51.85% covered (warning)
51.85%
14 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
75.61% covered (warning)
75.61%
248 / 328
51.85% covered (warning)
51.85%
14 / 27
294.01
0.00% covered (danger)
0.00%
0 / 1
 readEntity
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
6.01
 readEntityById
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 readSourceMapByIds
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 readCollection
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 attachRootParentIds
60.00% covered (warning)
60.00%
15 / 25
0.00% covered (danger)
0.00%
0 / 1
16.40
 readEntityWithoutRootParentResolution
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 resolveRootParentId
40.00% covered (danger)
40.00%
6 / 15
0.00% covered (danger)
0.00%
0 / 1
17.58
 getRequestLookupKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readRequestByProcessId
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 readAllRequestsForProcessIds
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
7
 readRequestsByIds
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 readRequestByArchiveId
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 readRequestIdsByArchiveId
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
9.08
 readListByProvider
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 readListByScopeAndDepartment
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
5.07
 readDepartmentProviders
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
5.12
 readAllRequestsForProviders
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 keepRequestsNotAlreadyOnScope
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
6.44
 readListBySource
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
 readListByCluster
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 writeEntity
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 writeListBySource
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 writeImportEntity
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 writeDeleteEntity
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 writeDeleteListBySource
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 testSource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 removeCache
11.76% covered (danger)
11.76%
2 / 17
0.00% covered (danger)
0.00%
0 / 1
64.64
1<?php
2
3namespace BO\Zmsbackend\Request\Service;
4
5use BO\Zmsbackend\Application as App;
6use BO\Zmsentities\Request as Entity;
7use BO\Zmsentities\Scope;
8use BO\Zmsentities\Collection\RequestList as Collection;
9
10/**
11 *
12 * @SuppressWarnings(TooManyPublicMethods)
13 */
14class Request extends \BO\Zmsbackend\Base
15{
16    public function readEntity($source, $requestId, $resolveReferences = 0, $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    public function readEntityById($requestId, $resolveReferences = 0, $disableCache = false)
47    {
48        $cacheKey = "request-byid-$requestId-$resolveReferences";
49
50        if (!$disableCache && App::$cache && App::$cache->has($cacheKey)) {
51            return App::$cache->get($cacheKey);
52        }
53
54        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
55        $query
56            ->setResolveLevel($resolveReferences)
57            ->addEntityMapping()
58            ->addResolvedReferences($resolveReferences)
59            ->addConditionRequestId($requestId);
60        $request = $this->fetchOne($query, new Entity());
61
62        if ($request->hasId()) {
63            $this->attachRootParentIds(new Collection([$request]));
64        }
65
66        if (App::$cache && $request->hasId()) {
67            App::$cache->set($cacheKey, $request);
68        }
69
70        return $request;
71    }
72
73    /**
74     * @return array<string, string>
75     */
76    public function readSourceMapByIds(array $requestIds): array
77    {
78        if ($requestIds === []) {
79            return [];
80        }
81
82        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::SELECT);
83        $query
84            ->setResolveLevel(0)
85            ->addEntityMapping()
86            ->addConditionRequestIdList($requestIds);
87
88        $map = [];
89        foreach ($this->readCollection($query) as $request) {
90            $map[(string) $request->getId()] = (string) $request->getSource();
91        }
92
93        return $map;
94    }
95
96    /**
97     * @SuppressWarnings(Param)
98     *
99     */
100    protected function readCollection($query)
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, $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, $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, $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, $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, $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, $resolveReferences = 0, $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, $resolveReferences = 0)
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        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::REPLACE);
510        $query->addValues([
511            'source' => $source,
512            'id' => $request['id'],
513            'name' => $request['name'],
514            'group' => (isset($request['group'])) ? $request['group'] : 'Sonstiges',
515            'link' => ('dldb' == $source)
516                ? 'https://service.berlin.de/dienstleistung/' . $request['id'] . '/'
517                : ((isset($request['link'])) ? $request['link'] : ''),
518            'data' => json_encode($request)
519        ]);
520        $this->writeItem($query);
521        return $this->readEntity($source, $request['id'], 0, true);
522    }
523
524    public function writeDeleteEntity($requestId, $source)
525    {
526        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::DELETE);
527        $query->addConditionRequestId($requestId);
528        $query->addConditionRequestSource($source);
529
530        return $this->deleteItem($query);
531    }
532
533    public function writeDeleteListBySource($source)
534    {
535        $query = new \BO\Zmsbackend\Request\Repository\Request(\BO\Zmsbackend\Query\Base::DELETE);
536        $query->addConditionRequestSource($source);
537        return $this->deleteItem($query);
538    }
539
540    protected function testSource($source)
541    {
542        if (! (new \BO\Zmsbackend\Source\Service\Source())->readEntity($source)) {
543            throw new \BO\Zmsbackend\Source\Exception\UnknownDataSource();
544        }
545    }
546
547    public function removeCache(Entity $request)
548    {
549        if (!App::$cache) {
550            return;
551        }
552
553        $source = $request->getSource();
554
555        if (isset($request->id)) {
556            $requestId = $request->getId();
557            if (App::$cache->has("request-$source-$requestId-0")) {
558                App::$cache->delete("request-$source-$requestId-0");
559            }
560
561            if (App::$cache->has("request-$source-$requestId-1")) {
562                App::$cache->delete("request-$source-$requestId-1");
563            }
564
565            if (App::$cache->has("request-$source-$requestId-2")) {
566                App::$cache->delete("request-$source-$requestId-2");
567            }
568        }
569
570        if (App::$cache->has("requestReadListBySource-$source-0")) {
571            App::$cache->delete("requestReadListBySource-$source-0");
572        }
573
574        if (App::$cache->has("requestReadListBySource-$source-1")) {
575            App::$cache->delete("requestReadListBySource-$source-1");
576        }
577
578        if (App::$cache->has("requestReadListBySource-$source-2")) {
579            App::$cache->delete("requestReadListBySource-$source-2");
580        }
581    }
582}