DataLoader   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 66
c 2
b 1
f 0
dl 0
loc 211
rs 10
wmc 30

10 Methods

Rating   Name   Duplication   Size   Complexity  
A extractQueryExpansionKeys() 0 14 4
A __construct() 0 3 1
A processDataLoader() 0 37 5
A load() 0 12 5
A extractKeysFromTemplate() 0 3 1
A extractEqualsFormatKeys() 0 23 5
A buildGroupKeyFromQuery() 0 8 2
A getDataLoader() 0 9 2
A buildGroupKey() 0 12 3
A parseQuery() 0 11 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\DataLoader;
6
7
use BEAR\Resource\Annotation\Link;
8
use BEAR\Resource\Exception\RowMustContainKeyInDataLoaderException;
9
use BEAR\Resource\LinkType;
10
use BEAR\Resource\Types;
11
use Ray\Di\InjectorInterface;
12
13
use function array_key_exists;
14
use function assert;
15
use function explode;
16
use function implode;
17
use function is_string;
18
use function parse_str;
19
use function parse_url;
20
use function preg_match_all;
21
use function str_contains;
22
use function str_starts_with;
23
use function uri_template;
24
25
use const PHP_URL_QUERY;
26
27
/**
28
 * Loads data in batch for DataLoader-enabled links
29
 *
30
 * @psalm-import-type Query from Types
31
 * @psalm-import-type QueryList from Types
32
 * @psalm-import-type ObjectList from Types
33
 */
34
final class DataLoader
35
{
36
    /** @var array<class-string<DataLoaderInterface>, DataLoaderInterface> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<DataL...>, DataLoaderInterface> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<DataLoaderInterface>, DataLoaderInterface>.
Loading history...
37
    private array $cache = [];
38
39
    public function __construct(
40
        private readonly InjectorInterface $injector,
41
    ) {
42
    }
43
44
    /**
45
     * Load data for DataLoader-enabled links
46
     *
47
     * @param ObjectList $annotations
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\DataLoader\ObjectList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
48
     * @param QueryList  $bodyList
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\DataLoader\QueryList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
49
     *
50
     * @param-out QueryList $bodyList
51
     *
52
     * @psalm-suppress ReferenceConstraintViolation
53
     */
54
    public function load(array $annotations, LinkType $link, array &$bodyList): void
55
    {
56
        foreach ($annotations as $annotation) {
57
            if (! $annotation instanceof Link || $annotation->crawl !== $link->key) {
58
                continue;
59
            }
60
61
            if ($annotation->dataLoader === null) {
62
                continue;
63
            }
64
65
            $this->processDataLoader($annotation, $bodyList);
66
        }
67
    }
68
69
    /**
70
     * @param QueryList $bodyList
71
     *
72
     * @param-out QueryList $bodyList
73
     *
74
     * @psalm-suppress ReferenceConstraintViolation
75
     */
76
    private function processDataLoader(Link $annotation, array &$bodyList): void
77
    {
78
        // Extract keys from URI template (parameters with {placeholder} values)
79
        $keys = $this->extractKeysFromTemplate($annotation->href);
80
81
        // Generate URIs and extract queries
82
        /** @var list<array{index: int, query: array<string, string>}> $uriData */
83
        $uriData = [];
84
        foreach ($bodyList as $index => $body) {
85
            $uri = uri_template($annotation->href, $body);
86
            $query = $this->parseQuery($uri);
87
            $uriData[] = ['index' => $index, 'query' => $query];
88
        }
89
90
        // Collect queries for DataLoader
91
        $queries = [];
92
        foreach ($uriData as $data) {
93
            $queries[] = $data['query'];
94
        }
95
96
        // Call DataLoader
97
        assert($annotation->dataLoader !== null);
98
        $loader = $this->getDataLoader($annotation->dataLoader);
99
        $rows = $loader($queries);
100
101
        // Group rows by keys
102
        /** @var array<string, list<array<string, mixed>>> $grouped */
103
        $grouped = [];
104
        foreach ($rows as $row) {
105
            $groupKey = $this->buildGroupKey($row, $keys);
106
            $grouped[$groupKey][] = $row;
107
        }
108
109
        // Distribute results to bodyList
110
        foreach ($uriData as $data) {
111
            $groupKey = $this->buildGroupKeyFromQuery($data['query'], $keys);
112
            $bodyList[$data['index']][$annotation->rel] = $grouped[$groupKey] ?? [];
113
        }
114
    }
115
116
    /**
117
     * Extract key parameter names from URI template
118
     *
119
     * @return list<string> Parameter names that will be used for matching
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\DataLoader\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
120
     */
121
    private function extractKeysFromTemplate(string $template): array
122
    {
123
        return [...$this->extractQueryExpansionKeys($template), ...$this->extractEqualsFormatKeys($template)];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($this->extr...sFormatKeys($template)) returns the type array<integer,array|string[]> which is incompatible with the documented return type BEAR\Resource\DataLoader\list.
Loading history...
124
    }
125
126
    /**
127
     * Extract keys from {?var1,var2} or {&var} format
128
     *
129
     * @return list<string>
130
     */
131
    private function extractQueryExpansionKeys(string $template): array
132
    {
133
        if (preg_match_all('/\{[?&]([^}]+)\}/', $template, $matches) === 0) {
134
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type BEAR\Resource\DataLoader\list.
Loading history...
135
        }
136
137
        $keys = [];
138
        foreach ($matches[1] as $varList) {
139
            foreach (explode(',', $varList) as $var) {
140
                $keys[] = $var;
141
            }
142
        }
143
144
        return $keys;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $keys returns the type array|string[] which is incompatible with the documented return type BEAR\Resource\DataLoader\list.
Loading history...
145
    }
146
147
    /**
148
     * Extract keys from param={var} format
149
     *
150
     * @return list<string>
151
     */
152
    private function extractEqualsFormatKeys(string $template): array
153
    {
154
        $queryString = parse_url($template, PHP_URL_QUERY);
155
        if (! is_string($queryString)) {
0 ignored issues
show
introduced by
The condition is_string($queryString) is always true.
Loading history...
156
            return [];
157
        }
158
159
        if (! str_contains($queryString, '=')) {
160
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type BEAR\Resource\DataLoader\list.
Loading history...
161
        }
162
163
        parse_str($queryString, $params);
164
        $keys = [];
165
        /** @psalm-suppress MixedAssignment */
166
        foreach ($params as $name => $value) {
167
            if (! str_starts_with((string) $value, '{')) {
168
                continue;
169
            }
170
171
            $keys[] = (string) $name;
172
        }
173
174
        return $keys;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $keys returns the type array|string[] which is incompatible with the documented return type BEAR\Resource\DataLoader\list.
Loading history...
175
    }
176
177
    /**
178
     * Parse query string from URI
179
     *
180
     * @return array<string, string>
181
     */
182
    private function parseQuery(string $uri): array
183
    {
184
        $queryString = parse_url($uri, PHP_URL_QUERY);
185
        if (! is_string($queryString)) {
0 ignored issues
show
introduced by
The condition is_string($queryString) is always true.
Loading history...
186
            return [];
187
        }
188
189
        parse_str($queryString, $query);
190
191
        /** @var array<string, string> $query */
192
        return $query;
193
    }
194
195
    /**
196
     * Build a group key from row data
197
     *
198
     * @param array<string, mixed> $row
199
     * @param list<string>         $keys
200
     */
201
    private function buildGroupKey(array $row, array $keys): string
202
    {
203
        $parts = [];
204
        foreach ($keys as $key) {
205
            if (! array_key_exists($key, $row)) {
206
                throw new RowMustContainKeyInDataLoaderException($key);
207
            }
208
209
            $parts[] = (string) $row[$key];
210
        }
211
212
        return implode("\0", $parts);
213
    }
214
215
    /**
216
     * Build a group key from query parameters
217
     *
218
     * @param array<string, string> $query
219
     * @param list<string>          $keys
220
     */
221
    private function buildGroupKeyFromQuery(array $query, array $keys): string
222
    {
223
        $parts = [];
224
        foreach ($keys as $key) {
225
            $parts[] = $query[$key] ?? '';
226
        }
227
228
        return implode("\0", $parts);
229
    }
230
231
    /**
232
     * Get or create a DataLoader instance
233
     *
234
     * @param class-string<DataLoaderInterface> $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<DataLoaderInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<DataLoaderInterface>.
Loading history...
235
     */
236
    private function getDataLoader(string $class): DataLoaderInterface
237
    {
238
        if (! isset($this->cache[$class])) {
239
            $instance = $this->injector->getInstance($class);
240
            assert($instance instanceof DataLoaderInterface);
241
            $this->cache[$class] = $instance;
242
        }
243
244
        return $this->cache[$class];
245
    }
246
}
247