DataLoader::parseQuery()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
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) || ! str_contains($queryString, '=')) {
0 ignored issues
show
introduced by
The condition is_string($queryString) is always true.
Loading history...
156
            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...
157
        }
158
159
        parse_str($queryString, $params);
160
        $keys = [];
161
        /** @psalm-suppress MixedAssignment */
162
        foreach ($params as $name => $value) {
163
            if (! str_starts_with((string) $value, '{')) {
164
                continue;
165
            }
166
167
            $keys[] = (string) $name;
168
        }
169
170
        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...
171
    }
172
173
    /**
174
     * Parse query string from URI
175
     *
176
     * @return array<string, string>
177
     */
178
    private function parseQuery(string $uri): array
179
    {
180
        $queryString = parse_url($uri, PHP_URL_QUERY);
181
        if ($queryString === null || $queryString === false) {
182
            return [];
183
        }
184
185
        parse_str($queryString, $query);
186
187
        /** @var array<string, string> $query */
188
        return $query;
189
    }
190
191
    /**
192
     * Build a group key from row data
193
     *
194
     * @param array<string, mixed> $row
195
     * @param list<string>         $keys
196
     */
197
    private function buildGroupKey(array $row, array $keys): string
198
    {
199
        $parts = [];
200
        foreach ($keys as $key) {
201
            if (! array_key_exists($key, $row)) {
202
                throw new RowMustContainKeyInDataLoaderException($key);
203
            }
204
205
            $parts[] = (string) $row[$key];
206
        }
207
208
        return implode("\0", $parts);
209
    }
210
211
    /**
212
     * Build a group key from query parameters
213
     *
214
     * @param array<string, string> $query
215
     * @param list<string>          $keys
216
     */
217
    private function buildGroupKeyFromQuery(array $query, array $keys): string
218
    {
219
        $parts = [];
220
        foreach ($keys as $key) {
221
            $parts[] = $query[$key] ?? '';
222
        }
223
224
        return implode("\0", $parts);
225
    }
226
227
    /**
228
     * Get or create a DataLoader instance
229
     *
230
     * @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...
231
     */
232
    private function getDataLoader(string $class): DataLoaderInterface
233
    {
234
        if (! isset($this->cache[$class])) {
235
            $instance = $this->injector->getInstance($class);
236
            assert($instance instanceof DataLoaderInterface);
237
            $this->cache[$class] = $instance;
238
        }
239
240
        return $this->cache[$class];
241
    }
242
}
243