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 (#413)
by Vincent
14:40
created

ConnectionBuilder::getOptionsWithDefaults()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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