AbstractProcessor::reverse()
last analyzed

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
ccs 0
cts 1
cp 0
c 0
b 0
f 0
1
<?php
2
3
namespace mav3rick177\RapidPagination\Base;
4
5
use mav3rick177\RapidPagination\Base\Contracts\Formatter;
6
use mav3rick177\RapidPagination\Base\Exceptions\InvalidArgumentException;
7
use mav3rick177\RapidPagination\Base\Query\UnionAll;
8
9
/**
10
 * Class AbstractProcessor
11
 */
12
abstract class AbstractProcessor
13
{
14
    /**
15
     * @var null|callable
16
     */
17
    protected static $defaultFormatter;
18
19
    /**
20
     * @var null|callable
21
     */
22
    protected $formatter;
23
    
24
25
    /**
26
     * Override static default formatter.
27
     *
28
     * @param callable|Formatter|string $formatter
29
     */
30 1
    public static function setDefaultFormatter($formatter)
31
    {
32 1
        static::$defaultFormatter = static::validateFormatter($formatter);
33
    }
34
35
    /**
36
     * Restore static default formatter.
37
     */
38 1
    public static function restoreDefaultFormatter()
39
    {
40 1
        static::$defaultFormatter = null;
41
    }
42
43
    /**
44
     * Use custom formatter.
45
     *
46
     * @param  callable|Formatter|string $formatter
47
     * @return $this
48
     */
49 2
    public function useFormatter($formatter)
50
    {
51 2
        $this->formatter = static::validateFormatter($formatter);
52 1
        return $this;
53
    }
54
55
    /**
56
     * Restore default formatter.
57
     *
58
     * @return $this
59
     */
60 1
    public function restoreFormatter()
61
    {
62 1
        $this->formatter = null;
63 1
        return $this;
64
    }
65
66
    /**
67
     * Get result.
68
     *
69
     * @param  Query $query
70
     * @param  mixed $rows
71
     * @return mixed
72
     */
73 2
    public function process(Query $query, $rows)
74
    {
75
        $meta = [
76 2
            'hasPrevious' => false,
77
            'previousCursor' => null,
78
            'hasNext' => false,
79
            'nextCursor' => null,
80
            'previousUrl' => null,
81
            'nextUrl' => null,
82
        ];
83
84 2
        if ($this->shouldLtrim($query, $rows)) {
85
            $type = $query->direction()->forward() ? 'previous' : 'next';
86
            $meta['has' . ucfirst($type)] = true;
87
            $meta[$type . 'Cursor'] = $this->makeCursor(
88
                $query,
89
                $this->offset($rows, (int)$query->exclusive())
90
            );
91
            $rows = $this->slice($rows, 1);
92
        }
93 2
        if ($this->shouldRtrim($query, $rows)) {
94
            $type = $query->direction()->backward() ? 'previous' : 'next';
95
            $meta['has' . ucfirst($type)] = true;
96
            $meta[$type . 'Cursor'] = $this->makeCursor(
97
                $query,
98
                $this->offset($rows, $query->limit() - $query->exclusive())
99
            );
100
            $rows = $this->slice($rows, 0, $query->limit());
101
        }
102
103
        // If we are not using UNION ALL, boolean values are not defined.
104 2
        if (!$query->selectOrUnionAll() instanceof UnionAll) {
105 2
            $meta[$query->direction()->forward() ? 'hasPrevious' : 'hasNext'] = null;
106
        }
107
108 2
        return $this->invokeFormatter($this->shouldReverse($query) ? $this->reverse($rows) : $rows, $meta, $query);
109
    }
110
111
    /**
112
     * Invoke formatter.
113
     *
114
     * @param  mixed $rows
115
     * @param  array $meta
116
     * @param  Query $query
117
     * @return mixed
118
     */
119 2
    protected function invokeFormatter($rows, array $meta, Query $query)
120
    {
121 2
        $formatter = $this->formatter ?: static::$defaultFormatter ?: [$this, 'defaultFormat'];
122 2
        return $formatter($rows, $meta, $query);
123
    }
124
125
    /**
126
     * Validate formatter and return in normalized form.
127
     *
128
     * @param  mixed    $formatter
129
     * @return callable
130
     */
131 3
    protected static function validateFormatter($formatter)
132
    {
133 3
        if (is_subclass_of($formatter, Formatter::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \mav3rick177\RapidPagina...tracts\Formatter::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
134
            return [is_string($formatter) ? new $formatter() : $formatter, 'format'];
135
        }
136 3
        if (is_callable($formatter)) {
137 2
            return $formatter;
138
        }
139 1
        throw new InvalidArgumentException('Formatter must be an instanceof ' . Formatter::class . ' or callable.');
140
    }
141
142
    /**
143
     * Format result with default format.
144
     *
145
     * @param  mixed            $rows
146
     * @param  array            $meta
147
     * @param  Query            $query
148
     * @return PaginationResult
149
     */
150
    protected function defaultFormat($rows, array $meta, Query $query)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
151
    {
152
        return new PaginationResult($rows, $meta);
153
    }
154
155
    /**
156
     * Determine if the rows should be replaced in reverse order.
157
     *
158
     * @param  Query $query
159
     * @return bool
160
     */
161 2
    protected function shouldReverse(Query $query)
162
    {
163 2
        return $query->direction()->backward();
164
    }
165
166
    /**
167
     * Determine if the first row should be dropped.
168
     *
169
     * @param  Query $query
170
     * @param  mixed $rows
171
     * @return bool
172
     */
173 2
    protected function shouldLtrim(Query $query, $rows)
174
    {
175 2
        $first = $this->offset($rows, 0);
176
177 2
        $selectOrUnionAll = $query->selectOrUnionAll();
178
179
        // If we are not using UNION ALL or the elements are empty...
180 2
        if (!$selectOrUnionAll instanceof UnionAll || !$first) {
181 2
            return false;
182
        }
183
184
        foreach ($selectOrUnionAll->supportQuery()->orders() as $order) {
185
186
            // Retrieve values
187
            $field = $this->field($first, $order->column());
188
            $cursor = $query->cursor()->get($order->column());
189
190
            // Compare the first row and the cursor
191
            if (!$diff = $this->compareField($field, $cursor)) {
192
                continue;
193
            }
194
195
            //
196
            // Drop the first row if ...
197
            //
198
            //
199
            //  - the support query is descending  &&  $field < $cursor
200
            //
201
            //               -------------------->  Main query, ascending
202
            //           [4, <5>, 6, 7, 8, 9, 10]
203
            //         <-----                       Support query, descending
204
            //
205
            //                               ---->  Support query, descending
206
            //           [10, 9, 8, 7, 6, <5>, 4]
207
            //         <---------------------       Main query, ascending
208
            //
209
            //
210
            //  - the support query is ascending   &&  $field > $cursor
211
            //
212
            //                              ----->  Support query, ascending
213
            //           [4, 5, 6, 7, 8, <9>, 10]
214
            //         <--------------------        Main query, descending
215
            //
216
            //               -------------------->  Main query, descending
217
            //           [10, <9>, 8, 7, 6, 5, 4]
218
            //         <----                        Support query, ascending
219
            //
220
            return $diff === ($order->descending() ? -1 : 1);
221
        }
222
223
        // If the first row and the cursor are identical, we should drop the first row only if exclusive.
224
        return $query->exclusive();
225
    }
226
227
    /**
228
     * Determine if the last row should be dropped.
229
     *
230
     * @param Query $query
231
     * @param $rows
232
     * @return bool
233
     */
234 2
    protected function shouldRtrim(Query $query, $rows)
235
    {
236 2
        return $query->limit() < $this->count($rows);
237
    }
238
239
    /**
240
     * Make a cursor from the specific row.
241
     *
242
     * @param  Query          $query
243
     * @param  mixed          $row
244
     * @return int[]|string[]
245
     */
246
    protected function makeCursor(Query $query, $row)
247
    {
248
        $fields = [];
249
        foreach ($query->orders() as $order) {
250
            $fields[$order->column()] = $this->field($row, $order->column());
251
        }
252
        return $fields;
253
    }
254
255
    /**
256
     * Return comparable value from a row.
257
     *
258
     * @param  mixed      $row
259
     * @param  string     $column
260
     * @return int|string
261
     */
262
    abstract protected function field($row, $column);
263
264
    /**
265
     * Compare the values.
266
     *
267
     *  "$field < $cursor" should return -1.
268
     *  "$field > $cursor" should return 1.
269
     *  "$field == $cursor" should return 0.
270
     *
271
     * @param  int|string $field
272
     * @param  int|string $cursor
273
     * @return int
274
     */
275
    protected function compareField($field, $cursor)
276
    {
277
        return ($field > $cursor) - ($field < $cursor);
278
    }
279
280
    /**
281
     * Return the n-th element of collection.
282
     * Must return null if not exists.
283
     *
284
     * @param  mixed $rows
285
     * @param  int   $offset
286
     * @return mixed
287
     */
288
    abstract protected function offset($rows, $offset);
289
290
    /**
291
     * Slice rows, like PHP function array_slice().
292
     *
293
     * @param  mixed    $rows
294
     * @param  int      $offset
295
     * @param  null|int $length
296
     * @return mixed
297
     */
298
    abstract protected function slice($rows, $offset, $length = null);
299
300
    /**
301
     * Count rows, like PHP function count().
302
     *
303
     * @param  mixed $rows
304
     * @return int
305
     */
306
    abstract protected function count($rows);
307
308
    /**
309
     * Reverse rows, like PHP function array_reverse().
310
     *
311
     * @param $rows
312
     * @return mixed
313
     */
314
    abstract protected function reverse($rows);
315
}
316