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 (#685)
by
unknown
08:10 queued 04:15
created

ConnectionBuilder::cursorToOffset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 10
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Relay\Connection;
6
7
use Overblog\GraphQLBundle\Definition\ArgumentInterface;
8
use Overblog\GraphQLBundle\Relay\Connection\Cursor\Base64CursorEncoder;
9
use Overblog\GraphQLBundle\Relay\Connection\Cursor\CursorEncoderInterface;
10
use Overblog\GraphQLBundle\Relay\Connection\Output\Connection;
11
use Overblog\GraphQLBundle\Relay\Connection\Output\Edge;
12
use Overblog\GraphQLBundle\Relay\Connection\Output\PageInfo;
13
14
/**
15
 * Class ConnectionBuilder.
16
 *
17
 * https://github.com/graphql/graphql-relay-js/blob/master/src/connection/arrayconnection.js
18
 */
19
class ConnectionBuilder
20
{
21
    public const PREFIX = 'arrayconnection:';
22
23
    /**
24
     * @var CursorEncoderInterface
25
     */
26
    protected $cursorEncoder;
27
28
    /**
29
     * If set, used to generate the connection object.
30
     *
31
     * @var callable
32
     */
33
    protected $connectionCallback;
34
35
    /**
36
     * If set, used to generate the edge object.
37
     *
38
     * @var callable
39
     */
40
    protected $edgeCallback;
41
42 80
    public function __construct(?CursorEncoderInterface $cursorEncoder = null, callable $connectionCallback = null, callable $edgeCallback = null)
43
    {
44 80
        $this->cursorEncoder = $cursorEncoder ?? new Base64CursorEncoder();
45 80
        $this->connectionCallback = $connectionCallback;
46 80
        $this->edgeCallback = $edgeCallback;
47 80
    }
48
49
    /**
50
     * A simple function that accepts an array and connection arguments, and returns
51
     * a connection object for use in GraphQL. It uses array offsets as pagination,
52
     * so pagination will only work if the array is static.
53
     *
54
     * @param array                   $data
55
     * @param array|ArgumentInterface $args
56
     *
57
     * @return ConnectionInterface
58
     */
59 69
    public function connectionFromArray(array $data, $args = []): ConnectionInterface
60
    {
61 69
        return $this->connectionFromArraySlice(
62
            $data,
63
            $args,
64
            [
65 69
                'sliceStart' => 0,
66 69
                'arrayLength' => \count($data),
67
            ]
68
        );
69
    }
70
71
    /**
72
     * A version of `connectionFromArray` that takes a promised array, and returns a
73
     * promised connection.
74
     *
75
     * @param mixed                   $dataPromise a promise
76
     * @param array|ArgumentInterface $args
77
     *
78
     * @return mixed a promise
79
     */
80 14
    public function connectionFromPromisedArray($dataPromise, $args = [])
81
    {
82 14
        $this->checkPromise($dataPromise);
83
84
        return $dataPromise->then(function ($data) use ($args) {
85 4
            return $this->connectionFromArray($data, $args);
86 4
        });
87
    }
88
89
    /**
90
     * Given a slice (subset) of an array, returns a connection object for use in
91
     * GraphQL.
92
     *
93
     * This function is similar to `connectionFromArray`, but is intended for use
94
     * cases where you know the cardinality of the connection, consider it too large
95
     * to materialize the entire array, and instead wish pass in a slice of the
96
     * total result large enough to cover the range specified in `args`.
97
     *
98
     * @param array                   $arraySlice
99
     * @param array|ArgumentInterface $args
100
     * @param array                   $meta
101
     *
102
     * @return ConnectionInterface
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);
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
     * @param array                   $meta
186
     *
187
     * @return mixed a promise
188
     */
189 12
    public function connectionFromPromisedArraySlice($dataPromise, $args, array $meta)
190
    {
191 12
        $this->checkPromise($dataPromise);
192
193
        return $dataPromise->then(function ($arraySlice) use ($args, $meta) {
194 2
            return $this->connectionFromArraySlice($arraySlice, $args, $meta);
195 2
        });
196
    }
197
198
    /**
199
     * Return the cursor associated with an object in an array.
200
     *
201
     * @param array $data
202
     * @param mixed $object
203
     *
204
     * @return string|null
205
     */
206 4
    public function cursorForObjectInConnection(array $data, $object): ? string
207
    {
208 4
        $offset = null;
209
210 4
        foreach ($data as $i => $entry) {
211
            // When using the comparison operator (==), object variables are compared in a simple manner,
212
            // namely: Two object instances are equal if they have the same attributes and values,
213
            // and are instances of the same class.
214 4
            if ($entry == $object) {
215 2
                $offset = $i;
216 2
                break;
217
            }
218
        }
219
220 4
        if (null === $offset) {
221 2
            return null;
222
        }
223
224 2
        return $this->offsetToCursor($offset);
225
    }
226
227
    /**
228
     * Given an optional cursor and a default offset, returns the offset
229
     * to use; if the cursor contains a valid offset, that will be used,
230
     * otherwise it will be the default.
231
     *
232
     * @param string|null $cursor
233
     * @param int         $defaultOffset
234
     *
235
     * @return int
236
     */
237 95
    public function getOffsetWithDefault(?string $cursor, int $defaultOffset): int
238
    {
239 95
        if (empty($cursor)) {
240 77
            return $defaultOffset;
241
        }
242 46
        $offset = $this->cursorToOffset($cursor);
243
244 46
        return !\is_numeric($offset) ? $defaultOffset : (int) $offset;
245
    }
246
247
    /**
248
     * Creates the cursor string from an offset.
249
     *
250
     * @param $offset
251
     *
252
     * @return string
253
     */
254 87
    public function offsetToCursor($offset): string
255
    {
256 87
        return $this->cursorEncoder->encode(self::PREFIX.$offset);
257
    }
258
259
    /**
260
     * Redefines the offset from the cursor string.
261
     *
262
     * @param $cursor
263
     *
264
     * @return string
265
     */
266 50
    public function cursorToOffset($cursor): string
267
    {
268
        // Returning an empty string is required to not break the Paginator
269
        // class. Ideally, we should throw an exception or not call this
270
        // method if $cursor is empty
271 50
        if (null === $cursor) {
272 3
            return '';
273
        }
274
275 47
        return \str_replace(static::PREFIX, '', $this->cursorEncoder->decode($cursor));
276
    }
277
278 89
    private function createEdges(iterable $slice, int $startOffset): array
279
    {
280 89
        $edges = [];
281
282 89
        foreach ($slice as $index => $value) {
283 84
            $cursor = $this->offsetToCursor($startOffset + $index);
284 84
            if ($this->edgeCallback) {
285 2
                $edge = ($this->edgeCallback)($cursor, $value, $index);
286 2
                if (!($edge instanceof EdgeInterface)) {
287 2
                    throw new \InvalidArgumentException(\sprintf('The $edgeCallback of the ConnectionBuilder must return an instance of EdgeInterface'));
288
                }
289
            } else {
290 82
                $edge = new Edge($cursor, $value);
291
            }
292 84
            $edges[] = $edge;
293
        }
294
295 89
        return $edges;
296
    }
297
298 89
    private function createConnection($edges, PageInfoInterface $pageInfo): ConnectionInterface
299
    {
300 89
        if ($this->connectionCallback) {
301 2
            $connection = ($this->connectionCallback)($edges, $pageInfo);
302 2
            if (!($connection instanceof ConnectionInterface)) {
303
                throw new \InvalidArgumentException(\sprintf('The $connectionCallback of the ConnectionBuilder must return an instance of ConnectionInterface'));
304
            }
305
306 2
            return $connection;
307
        }
308
309 87
        return new Connection($edges, $pageInfo);
310
    }
311
312 93
    private function getOptionsWithDefaults(array $options, array $defaults)
313
    {
314 93
        return $options + $defaults;
315
    }
316
317 26
    private function checkPromise($value): void
318
    {
319 26
        if (!\is_callable([$value, 'then'])) {
320 20
            throw new \InvalidArgumentException('This is not a valid promise.');
321
        }
322 6
    }
323
}
324