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
23:10
created

ConnectionBuilder::offsetToCursor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 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
    public function __construct(callable $connectionCallback = null, callable $edgeCallback = null)
36
    {
37
        $this->connectionCallback = $connectionCallback;
38
        $this->edgeCallback = $edgeCallback;
39
    }
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
    public function connectionFromArray(array $data, $args = []): Connection
52
    {
53
        return $this->connectionFromArraySlice(
54
            $data,
55
            $args,
56
            [
57
                'sliceStart' => 0,
58
                '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
    public function connectionFromPromisedArray($dataPromise, $args = [])
73
    {
74
        $this->checkPromise($dataPromise);
75
76
        return $dataPromise->then(function ($data) use ($args) {
77
            return $this->connectionFromArray($data, $args);
78
        });
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
    public function connectionFromArraySlice(array $arraySlice, $args, array $meta): Connection
97
    {
98
        $connectionArguments = $this->getOptionsWithDefaults(
99
            $args instanceof Argument ? $args->getRawArguments() : $args,
100
            [
101
                'after' => '',
102
                'before' => '',
103
                'first' => null,
104
                'last' => null,
105
            ]
106
        );
107
        $arraySliceMetaInfo = $this->getOptionsWithDefaults(
108
            $meta,
109
            [
110
                'sliceStart' => 0,
111
                'arrayLength' => 0,
112
            ]
113
        );
114
115
        $arraySliceLength = \count($arraySlice);
116
        $after = $connectionArguments['after'];
117
        $before = $connectionArguments['before'];
118
        $first = $connectionArguments['first'];
119
        $last = $connectionArguments['last'];
120
        $sliceStart = $arraySliceMetaInfo['sliceStart'];
121
        $arrayLength = $arraySliceMetaInfo['arrayLength'];
122
        $sliceEnd = $sliceStart + $arraySliceLength;
123
        $beforeOffset = $this->getOffsetWithDefault($before, $arrayLength);
124
        $afterOffset = $this->getOffsetWithDefault($after, -1);
125
126
        $startOffset = \max($sliceStart - 1, $afterOffset, -1) + 1;
127
        $endOffset = \min($sliceEnd, $beforeOffset, $arrayLength);
128
129
        if (\is_numeric($first)) {
130
            if ($first < 0) {
131
                throw new \InvalidArgumentException('Argument "first" must be a non-negative integer');
132
            }
133
            $endOffset = \min($endOffset, $startOffset + $first);
134
        }
135
136
        if (\is_numeric($last)) {
137
            if ($last < 0) {
138
                throw new \InvalidArgumentException('Argument "last" must be a non-negative integer');
139
            }
140
141
            $startOffset = \max($startOffset, $endOffset - $last);
142
        }
143
144
        // If supplied slice is too large, trim it down before mapping over it.
145
        $offset = \max($startOffset - $sliceStart, 0);
146
        $length = ($arraySliceLength - ($sliceEnd - $endOffset)) - $offset;
147
148
        $slice = \array_slice(
149
            $arraySlice,
150
            $offset,
151
            $length
152
        );
153
154
        $edges = [];
155
156
        foreach ($slice as $index => $value) {
157
            $cursor = $this->offsetToCursor($startOffset + $index);
158
            if ($this->edgeCallback) {
159
                $edge = ($this->edgeCallback)($cursor, $value, $index);
160
                if (!($edge instanceof EdgeInterface)) {
161
                    throw new \InvalidArgumentException(\sprintf('The $edgeCallback of the ConnectionBuilder must return an instance of EdgeInterface'));
162
                }
163
            } else {
164
                $edge = new Edge($cursor, $value);
165
            }
166
            $edges[] = $edge;
167
        }
168
169
        $firstEdge = $edges[0] ?? null;
170
        $lastEdge = \end($edges);
171
        $lowerBound = $after ? ($afterOffset + 1) : 0;
172
        $upperBound = $before ? $beforeOffset : $arrayLength;
173
174
        $pageInfo = new PageInfo(
175
            $firstEdge instanceof EdgeInterface ? $firstEdge->getCursor() : null,
176
            $lastEdge instanceof EdgeInterface ? $lastEdge->getCursor() : null,
177
            null !== $last ? $startOffset > $lowerBound : false,
178
            null !== $first ? $endOffset < $upperBound : false
179
        );
180
181
        if ($this->connectionCallback) {
182
            $connection = ($this->connectionCallback)($edges, $pageInfo);
183
            if (!($connection instanceof ConnectionInterface)) {
184
                throw new \InvalidArgumentException(\sprintf('The $connectionCallback of the ConnectionBuilder must return an instance of ConnectionInterface'));
185
            }
186
187
            return $connection;
188
        }
189
190
        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
    public function connectionFromPromisedArraySlice($dataPromise, $args, array $meta)
204
    {
205
        $this->checkPromise($dataPromise);
206
207
        return $dataPromise->then(function ($arraySlice) use ($args, $meta) {
208
            return $this->connectionFromArraySlice($arraySlice, $args, $meta);
209
        });
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
    public function cursorForObjectInConnection(array $data, $object): ? string
221
    {
222
        $offset = null;
223
224
        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
            if ($entry == $object) {
229
                $offset = $i;
230
                break;
231
            }
232
        }
233
234
        if (null === $offset) {
235
            return null;
236
        }
237
238
        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
    public function getOffsetWithDefault(? string $cursor, int $defaultOffset): int
252
    {
253
        if (empty($cursor)) {
254
            return $defaultOffset;
255
        }
256
        $offset = $this->cursorToOffset($cursor);
257
258
        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
    public function offsetToCursor($offset): string
269
    {
270
        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
    public function cursorToOffset($cursor): string
281
    {
282
        if (null === $cursor) {
283
            return '';
284
        }
285
286
        return \str_replace(static::PREFIX, '', \base64_decode($cursor, true));
287
    }
288
289
    private function getOptionsWithDefaults(array $options, array $defaults)
290
    {
291
        return $options + $defaults;
292
    }
293
294
    private function checkPromise($value): void
295
    {
296
        if (!\is_callable([$value, 'then'])) {
297
            throw new \InvalidArgumentException('This is not a valid promise.');
298
        }
299
    }
300
}
301