Test Failed
Push — master ( a0576a...ebf6f0 )
by Juuso
09:48 queued 11s
created

DataLoader::loadMany()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace leinonen\DataLoader;
6
7
use React\Promise\Promise;
8
use function React\Promise\all;
9
use function React\Promise\reject;
10
use React\EventLoop\LoopInterface;
11
use function React\Promise\resolve;
12
use React\Promise\ExtendedPromiseInterface;
13
14
final class DataLoader implements DataLoaderInterface
15
{
16
    /**
17
     * @var callable
18
     */
19
    private $batchLoadFunction;
20
21
    private array $promiseQueue = [];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
22
23
    private CacheMapInterface $promiseCache;
24
25
    private LoopInterface $eventLoop;
26
27
    private DataLoaderOptions $options;
28
29
    /**
30
     * Initiates a new DataLoader.
31
     *
32
     * @param callable $batchLoadFunction The function which will be called for the batch loading.
33
     * It must accept an array of keys and returns a Promise which resolves to an array of values.
34
     * @param LoopInterface $loop
35
     * @param CacheMapInterface $cacheMap
36
     * @param null|DataLoaderOptions $options
37
     */
38
    public function __construct(
39
        callable $batchLoadFunction,
40
        LoopInterface $loop,
41
        CacheMapInterface $cacheMap,
42
        DataLoaderOptions $options = null
43
    ) {
44
        $this->batchLoadFunction = $batchLoadFunction;
45
        $this->eventLoop = $loop;
46
        $this->promiseCache = $cacheMap;
47
        $this->options = $options ?? new DataLoaderOptions();
48
    }
49
50 32
    /**
51
     * {@inheritdoc}
52
     */
53
    public function load($key): ExtendedPromiseInterface
54
    {
55
        if ($key === null) {
56 32
            throw new \InvalidArgumentException(self::class . '::load must be called with a value, but got null');
57 32
        }
58 32
59 32
        if ($this->options->shouldCache() && $this->promiseCache->get($key)) {
60 32
            return $this->promiseCache->get($key);
61
        }
62
63
        $promise = new Promise(
64
            function (callable $resolve, callable $reject) use ($key) {
65 32
                $this->promiseQueue[] = [
66
                    'key' => $key,
67 32
                    'resolve' => $resolve,
68 2
                    'reject' => $reject,
69
                ];
70
71 30
                if (\count($this->promiseQueue) === 1) {
72 9
                    $this->scheduleDispatch();
73
                }
74
            }
75 29
        );
76
77 29
        if ($this->options->shouldCache()) {
78 29
            $this->promiseCache->set($key, $promise);
79 29
        }
80 29
81
        return $promise;
82
    }
83 29
84 29
    /**
85
     * {@inheritdoc}
86 29
     */
87
    public function loadMany(array $keys): ExtendedPromiseInterface
88
    {
89 29
        return all(
90 28
            \array_map(
91
                fn($key) => $this->load($key),
92
                $keys
93 29
            )
94
        );
95
    }
96
97
    /**
98
     * {@inheritdoc}
99 2
     */
100
    public function clear($key): void
101 2
    {
102 2
        $this->promiseCache->delete($key);
103
    }
104 2
105 2
    /**
106 2
     * {@inheritdoc}
107
     */
108
    public function clearAll(): void
109
    {
110
        $this->promiseCache->clear();
111
    }
112
113
    /**
114 11
     * {@inheritdoc}
115
     */
116 11
    public function prime($key, $value): void
117 11
    {
118
        if (! $this->promiseCache->get($key)) {
119
            // Cache a rejected promise if the value is an Exception, in order to match
120
            // the behavior of load($key).
121
            $promise = $value instanceof \Exception ? reject($value) : resolve($value);
122 1
123
            $this->promiseCache->set($key, $promise);
124 1
        }
125 1
    }
126
127
    /**
128
     * Schedules the dispatch to happen on the next tick of the EventLoop
129
     * If batching is disabled, schedule the dispatch immediately.
130 4
     *
131
     * @return void
132 4
     */
133
    private function scheduleDispatch(): void
134
    {
135 4
        if ($this->options->shouldBatch()) {
136
            $this->eventLoop->futureTick(
137 4
                fn() => $this->dispatchQueue()
138
            );
139 4
140
            return;
141
        }
142
143
        $this->dispatchQueue();
144
    }
145
146
    /**
147 29
     * Resets and dispatches the DataLoaders queue.
148
     *
149 29
     * @return void
150 28
     */
151
    private function dispatchQueue(): void
152 27
    {
153 28
        $queue = $this->promiseQueue;
154
        $this->promiseQueue = [];
155
156 28
        $maxBatchSize = $this->options->getMaxBatchSize();
157
        $shouldBeDispatchedInMultipleBatches = $maxBatchSize !== null
158
            && $maxBatchSize > 0
159 1
            && $maxBatchSize < count($queue);
160 1
161
        $shouldBeDispatchedInMultipleBatches
162
            ? $this->dispatchQueueInMultipleBatches($queue, $maxBatchSize)
163
            : $this->dispatchQueueBatch($queue);
164
    }
165
166
    /**
167 28
     * Dispatches a batch of a queue. The given batch can also be the whole queue.
168
     *
169 28
     * @param array $batch
170 28
     */
171
    private function dispatchQueueBatch($batch)
172 28
    {
173
        $keys = \array_column($batch, 'key');
174 28
        $batchLoadFunction = $this->batchLoadFunction;
175 1
176
        /** @var Promise $batchPromise */
177 27
        $batchPromise = $batchLoadFunction($keys);
178
179 28
        try {
180
            $this->validateBatchPromise($batchPromise);
181
        } catch (DataLoaderException $exception) {
182
            return $this->handleFailedDispatch($batch, $exception);
183
        }
184
185
        $batchPromise
186 28
            ->then(
187
                function ($values) use ($batch, $keys) {
188 28
                    $this->validateBatchPromiseOutput($values, $keys);
189 28
                    $this->handleSuccessfulDispatch($batch, $values);
190
                }
191
            )
192 28
            ->then(null, fn($error)  => $this->handleFailedDispatch($batch, $error));
193
    }
194
195 28
    /**
196 4
     * Dispatches the given queue in multiple batches.
197 4
     *
198
     * @param array $queue
199
     * @param int $maxBatchSize
200 24
     *
201
     * @return void
202 23
     */
203 21
    private function dispatchQueueInMultipleBatches(array $queue, $maxBatchSize): void
204 24
    {
205
        $numberOfBatchesToDispatch = \count($queue) / $maxBatchSize;
206 3
207 24
        for ($i = 0; $i < $numberOfBatchesToDispatch; $i++) {
208 24
            $this->dispatchQueueBatch(
209
                \array_slice($queue, $i * $maxBatchSize, $maxBatchSize)
210
            );
211
        }
212
    }
213
214
    /**
215
     * Handles the batch by resolving the promises and rejecting ones that return Exceptions.
216
     *
217
     * @param array $batch
218 1
     * @param array $values
219
     */
220 1
    private function handleSuccessfulDispatch(array $batch, array $values): void
221
    {
222 1
        foreach ($batch as $index => $queueItem) {
223 1
            $value = $values[$index];
224 1
            $value instanceof \Exception
225
                ? $queueItem['reject']($value)
226
                : $queueItem['resolve']($value);
227 1
        }
228
    }
229
230
    /**
231
     * Handles the failed batch dispatch.
232
     *
233
     * @param array $batch
234
     * @param \Exception $error
235 21
     */
236
    private function handleFailedDispatch(array $batch, \Exception $error)
237 21
    {
238 21
        foreach ($batch as $index => $queueItem) {
239 21
            // We don't want to cache individual loads if the entire batch dispatch fails.
240 4
            $this->clear($queueItem['key']);
241
            $queueItem['reject']($error);
242 19
        }
243
    }
244
245 21
    /**
246
     * Validates the batch promise's output.
247
     *
248
     * @param array $values Values from resolved promise.
249
     * @param array $keys Keys which the DataLoaders load was called with
250
     *
251
     * @throws DataLoaderException
252
     */
253 7
    private function validateBatchPromiseOutput($values, $keys): void
254
    {
255 7
        if (! \is_array($values)) {
256
            throw new DataLoaderException(
257 7
                self::class . ' must be constructed with a function which accepts ' .
258 7
                'an array of keys and returns a Promise which resolves to an array of values ' .
259
                \sprintf('not return a Promise: %s.', \gettype($values))
260 7
            );
261
        }
262
263
        if (\count($values) !== \count($keys)) {
264
            throw new DataLoaderException(
265
                self::class . ' must be constructed with a function which accepts ' .
266
                'an array of keys and returns a Promise which resolves to an array of values, but ' .
267
                'the function did not return a Promise of an array of the same length as the array of keys.' .
268
                \sprintf("\n Keys: %s\n Values: %s\n", \count($keys), \count($values))
269
            );
270 23
        }
271
    }
272 23
273 1
    /**
274
     * Validates the batch promise returned from the batch load function.
275
     *
276 1
     * @param $batchPromise
277
     *
278
     * @throws DataLoaderException
279
     */
280 22
    private function validateBatchPromise($batchPromise): void
281 1
    {
282
        if (! $batchPromise || ! \is_callable([$batchPromise, 'then'])) {
283
            throw new DataLoaderException(
284
                self::class . ' must be constructed with a function which accepts ' .
285 1
                'an array of keys and returns a Promise which resolves to an array of values ' .
286
                \sprintf('the function returned %s.', \gettype($batchPromise))
287
            );
288 21
        }
289
    }
290
}
291