Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Completed
Pull Request — master (#726)
by Vincent
17:18 queued 10:52
created

ConnectionBuilder::connectionFromArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Relay\Connection;
6
7
use InvalidArgumentException;
8
use Overblog\GraphQLBundle\Definition\ArgumentInterface;
9
use Overblog\GraphQLBundle\Relay\Connection\Cursor\Base64CursorEncoder;
10
use Overblog\GraphQLBundle\Relay\Connection\Cursor\CursorEncoderInterface;
11
use Overblog\GraphQLBundle\Relay\Connection\Output\Connection;
12
use Overblog\GraphQLBundle\Relay\Connection\Output\Edge;
13
use Overblog\GraphQLBundle\Relay\Connection\Output\PageInfo;
14
use function array_slice;
15
use function count;
16
use function end;
17
use function is_callable;
18
use function is_numeric;
19
use function max;
20
use function min;
21
use function sprintf;
22
use function str_replace;
23
24
/**
25
 * Class ConnectionBuilder.
26
 *
27
 * https://github.com/graphql/graphql-relay-js/blob/master/src/connection/arrayconnection.js
28
 */
29
class ConnectionBuilder
30
{
31
    public const PREFIX = 'arrayconnection:';
32
33
    protected CursorEncoderInterface $cursorEncoder;
34
35
    /**
36
     * If set, used to generate the connection object.
37
     *
38
     * @var callable|null
39
     */
40
    protected $connectionCallback;
41
42
    /**
43
     * If set, used to generate the edge object.
44
     *
45
     * @var ?callable
46
     */
47
    protected $edgeCallback;
48
49 80
    public function __construct(?CursorEncoderInterface $cursorEncoder = null, callable $connectionCallback = null, callable $edgeCallback = null)
50
    {
51 80
        $this->cursorEncoder = $cursorEncoder ?? new Base64CursorEncoder();
52 80
        $this->connectionCallback = $connectionCallback;
53 80
        $this->edgeCallback = $edgeCallback;
54 80
    }
55
56
    /**
57
     * A simple function that accepts an array and connection arguments, and returns
58
     * a connection object for use in GraphQL. It uses array offsets as pagination,
59
     * so pagination will only work if the array is static.
60
     *
61
     * @param array|ArgumentInterface $args
62
     */
63 69
    public function connectionFromArray(array $data, $args = []): ConnectionInterface
64
    {
65 69
        return $this->connectionFromArraySlice(
66
            $data,
67
            $args,
68
            [
69 69
                'sliceStart' => 0,
70 69
                'arrayLength' => count($data),
71
            ]
72
        );
73
    }
74
75
    /**
76
     * A version of `connectionFromArray` that takes a promised array, and returns a
77
     * promised connection.
78
     *
79
     * @param mixed                   $dataPromise a promise
80
     * @param array|ArgumentInterface $args
81
     *
82
     * @return mixed a promise
83
     */
84 14
    public function connectionFromPromisedArray($dataPromise, $args = [])
85
    {
86 14
        $this->checkPromise($dataPromise);
87
88 4
        return $dataPromise->then(function ($data) use ($args) {
89 4
            return $this->connectionFromArray($data, $args);
90 4
        });
91
    }
92
93
    /**
94
     * Given a slice (subset) of an array, returns a connection object for use in
95
     * GraphQL.
96
     *
97
     * This function is similar to `connectionFromArray`, but is intended for use
98
     * cases where you know the cardinality of the connection, consider it too large
99
     * to materialize the entire array, and instead wish pass in a slice of the
100
     * total result large enough to cover the range specified in `args`.
101
     *
102
     * @param array|ArgumentInterface $args
103
     */
104 93
    public function connectionFromArraySlice(array $arraySlice, $args, array $meta): ConnectionInterface
105
    {
106 93
        $connectionArguments = $this->getOptionsWithDefaults(
107 93
            $args instanceof ArgumentInterface ? $args->getArrayCopy() : $args,
108
            [
109 93
                'after' => '',
110
                'before' => '',
111
                'first' => null,
112
                'last' => null,
113
            ]
114
        );
115 93
        $arraySliceMetaInfo = $this->getOptionsWithDefaults(
116
            $meta,
117
            [
118 93
                'sliceStart' => 0,
119
                'arrayLength' => 0,
120
            ]
121
        );
122
123 93
        $arraySliceLength = count($arraySlice);
124 93
        $after = $connectionArguments['after'];
125 93
        $before = $connectionArguments['before'];
126 93
        $first = $connectionArguments['first'];
127 93
        $last = $connectionArguments['last'];
128 93
        $sliceStart = $arraySliceMetaInfo['sliceStart'];
129 93
        $arrayLength = $arraySliceMetaInfo['arrayLength'];
130 93
        $sliceEnd = $sliceStart + $arraySliceLength;
131 93
        $beforeOffset = $this->getOffsetWithDefault($before, $arrayLength);
132 93
        $afterOffset = $this->getOffsetWithDefault($after, -1);
133
134 93
        $startOffset = max($sliceStart - 1, $afterOffset, -1) + 1;
135 93
        $endOffset = min($sliceEnd, $beforeOffset, $arrayLength);
136
137 93
        if (is_numeric($first)) {
138 55
            if ($first < 0) {
139 2
                throw new InvalidArgumentException('Argument "first" must be a non-negative integer');
140
            }
141 53
            $endOffset = min($endOffset, $startOffset + $first); // @phpstan-ignore-line
142
        }
143
144 91
        if (is_numeric($last)) {
145 24
            if ($last < 0) {
146 2
                throw new InvalidArgumentException('Argument "last" must be a non-negative integer');
147
            }
148
149 22
            $startOffset = max($startOffset, $endOffset - $last);
150
        }
151
152
        // If supplied slice is too large, trim it down before mapping over it.
153 89
        $offset = max($startOffset - $sliceStart, 0);
154 89
        $length = ($arraySliceLength - ($sliceEnd - $endOffset)) - $offset;
155
156 89
        $slice = array_slice(
157 89
            $arraySlice,
158 89
            $offset,
159 89
            $length
160
        );
161
162 89
        $edges = $this->createEdges($slice, $startOffset);
163
164 89
        $firstEdge = $edges[0] ?? null;
165 89
        $lastEdge = end($edges);
166 89
        $lowerBound = $after ? ($afterOffset + 1) : 0;
167 89
        $upperBound = $before ? $beforeOffset : $arrayLength;
168
169 89
        $pageInfo = new PageInfo(
170 89
            $firstEdge instanceof EdgeInterface ? $firstEdge->getCursor() : null,
171 89
            $lastEdge instanceof EdgeInterface ? $lastEdge->getCursor() : null,
172 89
            null !== $last ? $startOffset > $lowerBound : false,
173 89
            null !== $first ? $endOffset < $upperBound : false
174
        );
175
176 89
        return $this->createConnection($edges, $pageInfo);
177
    }
178
179
    /**
180
     * A version of `connectionFromArraySlice` that takes a promised array slice,
181
     * and returns a promised connection.
182
     *
183
     * @param mixed                   $dataPromise a promise
184
     * @param array|ArgumentInterface $args
185
     *
186
     * @return mixed a promise
187
     */
188 12
    public function connectionFromPromisedArraySlice($dataPromise, $args, array $meta)
189
    {
190 12
        $this->checkPromise($dataPromise);
191
192 2
        return $dataPromise->then(function ($arraySlice) use ($args, $meta) {
193 2
            return $this->connectionFromArraySlice($arraySlice, $args, $meta);
194 2
        });
195
    }
196
197
    /**
198
     * Return the cursor associated with an object in an array.
199
     *
200
     * @param mixed $object
201
     */
202 4
    public function cursorForObjectInConnection(array $data, $object): ? string
203
    {
204 4
        $offset = null;
205
206 4
        foreach ($data as $i => $entry) {
207
            // When using the comparison operator (==), object variables are compared in a simple manner,
208
            // namely: Two object instances are equal if they have the same attributes and values,
209
            // and are instances of the same class.
210 4
            if ($entry == $object) {
211 2
                $offset = $i;
212 2
                break;
213
            }
214
        }
215
216 4
        if (null === $offset) {
217 2
            return null;
218
        }
219
220 2
        return $this->offsetToCursor($offset);
221
    }
222
223
    /**
224
     * Given an optional cursor and a default offset, returns the offset
225
     * to use; if the cursor contains a valid offset, that will be used,
226
     * otherwise it will be the default.
227
     */
228 95
    public function getOffsetWithDefault(?string $cursor, int $defaultOffset): int
229
    {
230 95
        if (empty($cursor)) {
231 77
            return $defaultOffset;
232
        }
233 46
        $offset = $this->cursorToOffset($cursor);
234
235 46
        return !is_numeric($offset) ? $defaultOffset : (int) $offset;
236
    }
237
238
    /**
239
     * Creates the cursor string from an offset.
240
     *
241
     * @param int|string $offset
242
     */
243 87
    public function offsetToCursor($offset): string
244
    {
245 87
        return $this->cursorEncoder->encode(self::PREFIX.$offset);
246
    }
247
248
    /**
249
     * Redefines the offset from the cursor string.
250
     */
251 50
    public function cursorToOffset(?string $cursor): string
252
    {
253
        // Returning an empty string is required to not break the Paginator
254
        // class. Ideally, we should throw an exception or not call this
255
        // method if $cursor is empty
256 50
        if (null === $cursor) {
257 3
            return '';
258
        }
259
260 47
        return str_replace(static::PREFIX, '', $this->cursorEncoder->decode($cursor));
261
    }
262
263 89
    private function createEdges(iterable $slice, int $startOffset): array
264
    {
265 89
        $edges = [];
266
267 89
        foreach ($slice as $index => $value) {
268 84
            $cursor = $this->offsetToCursor($startOffset + $index);
269 84
            if ($this->edgeCallback) {
270 2
                $edge = ($this->edgeCallback)($cursor, $value, $index);
271 2
                if (!($edge instanceof EdgeInterface)) {
272 2
                    throw new InvalidArgumentException(sprintf('The $edgeCallback of the ConnectionBuilder must return an instance of EdgeInterface'));
273
                }
274
            } else {
275 82
                $edge = new Edge($cursor, $value);
276
            }
277 84
            $edges[] = $edge;
278
        }
279
280 89
        return $edges;
281
    }
282
283
    /**
284
     * @param mixed $edges
285
     */
286 89
    private function createConnection($edges, PageInfoInterface $pageInfo): ConnectionInterface
287
    {
288 89
        if ($this->connectionCallback) {
289 2
            $connection = ($this->connectionCallback)($edges, $pageInfo);
290 2
            if (!($connection instanceof ConnectionInterface)) {
291
                throw new InvalidArgumentException(sprintf('The $connectionCallback of the ConnectionBuilder must return an instance of ConnectionInterface'));
292
            }
293
294 2
            return $connection;
295
        }
296
297 87
        return new Connection($edges, $pageInfo);
298
    }
299
300 93
    private function getOptionsWithDefaults(array $options, array $defaults): array
301
    {
302 93
        return $options + $defaults;
303
    }
304
305
    /**
306
     * @param string|object $value
307
     */
308 26
    private function checkPromise($value): void
309
    {
310 26
        if (!is_callable([$value, 'then'])) {
311 20
            throw new InvalidArgumentException('This is not a valid promise.');
312
        }
313 6
    }
314
}
315