Lines 75.68% 249 / 329
Functions and Methods 51.85% 14 / 27
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
Request 75.68% 249 / 329 51.85% 14 / 27 292.35 0.00% 0 / 1
 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
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(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}