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
06:04
created

ConnectionBuilder::createEdges()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

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