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

Passed
Pull Request — 0.14 (#841)
by Jérémiah
02:54
created

ConnectionBuilder::createConnection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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