Passed
Pull Request — master (#48)
by Chito
12:37
created

Query::paginate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Lampager\Cake\ORM;
6
7
use Cake\Database\Expression\OrderByExpression;
8
use Cake\Database\Expression\OrderClauseExpression;
9
use Cake\Database\Expression\QueryExpression;
10
use Cake\Database\ExpressionInterface;
11
use Cake\Datasource\Paging\PaginatedInterface;
12
use Cake\Datasource\ResultSetInterface;
13
use Cake\ORM\Query\SelectQuery;
14
use Cake\ORM\ResultSet;
15
use Cake\ORM\Table;
16
use Lampager\Cake\Paginator;
17
use Lampager\Contracts\Cursor;
18
use Lampager\Exceptions\Query\BadKeywordException;
19
use Lampager\Exceptions\Query\InsufficientConstraintsException;
20
use Lampager\Exceptions\Query\LimitParameterException;
21
22
/**
23
 * @method $this forward(bool $forward = true)       Define that the current pagination is going forward.
24
 * @method $this backward(bool $backward = true)     Define that the current pagination is going backward.
25
 * @method $this exclusive(bool $exclusive = true)   Define that the cursor value is not included in the previous/next result.
26
 * @method $this inclusive(bool $inclusive = true)   Define that the cursor value is included in the previous/next result.
27
 * @method $this seekable(bool $seekable = true)     Define that the query can detect both "has_previous" and "has_next".
28
 * @method $this unseekable(bool $unseekable = true) Define that the query can detect only either "has_previous" or "has_next".
29
 * @method $this fromArray(mixed[] $options)         Define options from an associative array.
30
 */
31
class Query extends SelectQuery
32
{
33
    /** @var Paginator */
34
    protected $_paginator;
35
36
    /** @var Cursor|int[]|string[] */
37
    protected $_cursor = [];
38
39
    /**
40
     * Construct query.
41
     *
42
     * @param Table $table The table this query is starting on
43
     */
44
    public function __construct(Table $table)
45
    {
46
        parent::__construct($table);
47
48
        $this->_paginator = Paginator::create($this);
49
    }
50
51
    /**
52
     * Create query based on the existing query. This factory copies the internal
53
     * state of the given query to a new instance.
54
     */
55
    public static function fromQuery(SelectQuery $query)
56
    {
57
        $obj = new static($query->getRepository());
58
59
        foreach (get_object_vars($query) as $k => $v) {
60
            $obj->$k = $v;
61
        }
62
63
        $obj->_executeOrder($obj->clause('order'));
64
        $obj->_executeLimit($obj->clause('limit'));
65
66
        return $obj;
67
    }
68
69
    /**
70
     * Set the cursor.
71
     *
72
     * @param Cursor|int[]|string[] $cursor
73
     */
74
    public function cursor($cursor = [])
75
    {
76
        $this->_cursor = $cursor;
77
78
        return $this;
79
    }
80
81
    /**
82
     * Execute query and paginate them.
83
     */
84
    public function paginate(): PaginatedInterface
85
    {
86
        return $this->_paginator->paginate($this->_cursor);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function orderBy(ExpressionInterface|\Closure|array|string $fields, bool $overwrite = false)
93
    {
94
        parent::orderBy($fields, $overwrite);
95
        $this->_executeOrder($this->clause('order'));
96
97
        return $this;
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function orderByAsc(ExpressionInterface|\Closure|string $field, bool $overwrite = false)
104
    {
105
        parent::orderByAsc($field, $overwrite);
106
        $this->_executeOrder($this->clause('order'));
107
108
        return $this;
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function orderByDesc(ExpressionInterface|\Closure|string $field, bool $overwrite = false)
115
    {
116
        parent::orderByDesc($field, $overwrite);
117
        $this->_executeOrder($this->clause('order'));
118
119
        return $this;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function limit($num)
126
    {
127
        parent::limit($num);
128
        $this->_executeLimit($this->clause('limit'));
129
130
        return $this;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function all(): ResultSetInterface
137
    {
138
        return new ResultSet(iterator_to_array($this->paginate()->items()));
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    protected function _performCount(): int
145
    {
146
        return $this->all()->count();
147
    }
148
149
    protected function _executeOrder(?OrderByExpression $order): void
150
    {
151
        $this->_paginator->clearOrderBy();
152
153
        if ($order === null) {
154
            return;
155
        }
156
157
        $generator = $this->getValueBinder();
158
        $order->iterateParts(function ($condition, $key) use ($generator) {
159
            if (!is_int($key)) {
160
                /**
161
                 * @var string $key       The column
162
                 * @var string $condition The order
163
                 */
164
                $this->_paginator->orderBy($key, $condition);
165
            }
166
167
            if ($condition instanceof OrderClauseExpression) {
168
                $generator->resetCount();
169
170
                if (!preg_match('/ (?<direction>ASC|DESC)$/', $condition->sql($generator), $matches)) {
171
                    throw new BadKeywordException('OrderClauseExpression does not have direction');
172
                }
173
174
                /** @var string $direction */
175
                $direction = $matches['direction'];
176
177
                /** @var ExpressionInterface|string $field */
178
                $field = $condition->getField();
179
180
                if ($field instanceof ExpressionInterface) {
181
                    $generator->resetCount();
182
                    $this->_paginator->orderBy($field->sql($generator), $direction);
183
                } else {
184
                    $this->_paginator->orderBy($field, $direction);
185
                }
186
            }
187
188
            if ($condition instanceof QueryExpression) {
189
                $generator->resetCount();
190
                $this->_paginator->orderBy($condition->sql($generator));
191
            }
192
193
            return $condition;
194
        });
195
    }
196
197
    /**
198
     * @param null|int|QueryExpression $limit
199
     */
200
    protected function _executeLimit($limit): void
201
    {
202
        if (is_int($limit)) {
203
            $this->_paginator->limit($limit);
204
            return;
205
        }
206
207
        if ($limit instanceof QueryExpression) {
208
            $generator = $this->getValueBinder();
209
            $generator->resetCount();
210
            $sql = $limit->sql($generator);
211
212
            if (!ctype_digit($sql) || $sql <= 0) {
213
                throw new LimitParameterException('Limit must be positive integer');
214
            }
215
216
            $this->_paginator->limit((int)$sql);
217
            return;
218
        }
219
220
        // @codeCoverageIgnoreStart
221
        throw new \LogicException('Unreachable here');
222
        // @codeCoverageIgnoreEnd
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    public function __call(string $method, array $arguments)
229
    {
230
        static $options = [
231
            'forward',
232
            'backward',
233
            'exclusive',
234
            'inclusive',
235
            'seekable',
236
            'unseekable',
237
            'fromArray',
238
        ];
239
240
        if (in_array($method, $options, true)) {
241
            $this->_paginator->$method(...$arguments);
242
            return $this;
243
        }
244
245
        throw new \BadMethodCallException('Method ' . __CLASS__ . '::' . $method . ' does not exist');
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function __debugInfo(): array
252
    {
253
        try {
254
            $info = $this->_paginator->build($this->_cursor)->__debugInfo();
255
        } catch (InsufficientConstraintsException $e) {
256
            $info = [
257
                'sql' => 'SQL could not be generated for this query as it is incomplete: ' . $e->getMessage(),
258
                'params' => [],
259
                'defaultTypes' => $this->getDefaultTypes(),
260
                'decorators' => count($this->_resultDecorators),
261
                'executed' => (bool)$this->_statement,
262
                'hydrate' => $this->_hydrate,
263
                'formatters' => count($this->_formatters),
264
                'mapReducers' => count($this->_mapReduce),
265
                'contain' => $this->_eagerLoader ? $this->_eagerLoader->getContain() : [],
266
                'matching' => $this->_eagerLoader ? $this->_eagerLoader->getMatching() : [],
267
                'extraOptions' => $this->_options,
268
                'repository' => $this->_repository,
269
            ];
270
        }
271
272
        return [
273
            '(help)' => 'This is a Lampager Query object to get the paginated results.',
274
            'paginator' => $this->_paginator,
275
        ] + $info;
276
    }
277
}
278