Query::orderBy()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 2
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
        $items = $this->paginate()->items();
139
        return new ResultSet(is_array($items) ? $items : iterator_to_array($items));
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    protected function _performCount(): int
146
    {
147
        return $this->all()->count();
148
    }
149
150
    protected function _executeOrder(?OrderByExpression $order): void
151
    {
152
        $this->_paginator->clearOrderBy();
153
154
        if ($order === null) {
155
            return;
156
        }
157
158
        $generator = $this->getValueBinder();
159
        $order->iterateParts(function ($condition, $key) use ($generator) {
160
            if (!is_int($key)) {
161
                /**
162
                 * @var string $key       The column
163
                 * @var string $condition The order
164
                 */
165
                $this->_paginator->orderBy($key, $condition);
166
            }
167
168
            if ($condition instanceof OrderClauseExpression) {
169
                $generator->resetCount();
170
171
                if (!preg_match('/ (?<direction>ASC|DESC)$/', $condition->sql($generator), $matches)) {
172
                    throw new BadKeywordException('OrderClauseExpression does not have direction');
173
                }
174
175
                /** @var string $direction */
176
                $direction = $matches['direction'];
177
178
                /** @var ExpressionInterface|string $field */
179
                $field = $condition->getField();
180
181
                if ($field instanceof ExpressionInterface) {
182
                    $generator->resetCount();
183
                    $this->_paginator->orderBy($field->sql($generator), $direction);
184
                } else {
185
                    $this->_paginator->orderBy($field, $direction);
186
                }
187
            }
188
189
            if ($condition instanceof QueryExpression) {
190
                $generator->resetCount();
191
                $this->_paginator->orderBy($condition->sql($generator));
192
            }
193
194
            return $condition;
195
        });
196
    }
197
198
    /**
199
     * @param null|int|QueryExpression $limit
200
     */
201
    protected function _executeLimit($limit): void
202
    {
203
        if (is_int($limit)) {
204
            $this->_paginator->limit($limit);
205
            return;
206
        }
207
208
        if ($limit instanceof QueryExpression) {
209
            $generator = $this->getValueBinder();
210
            $generator->resetCount();
211
            $sql = $limit->sql($generator);
212
213
            if (!ctype_digit($sql) || $sql <= 0) {
214
                throw new LimitParameterException('Limit must be positive integer');
215
            }
216
217
            $this->_paginator->limit((int)$sql);
218
            return;
219
        }
220
221
        // @codeCoverageIgnoreStart
222
        throw new \LogicException('Unreachable here');
223
        // @codeCoverageIgnoreEnd
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function __call(string $method, array $arguments)
230
    {
231
        static $options = [
232
            'forward',
233
            'backward',
234
            'exclusive',
235
            'inclusive',
236
            'seekable',
237
            'unseekable',
238
            'fromArray',
239
        ];
240
241
        if (in_array($method, $options, true)) {
242
            $this->_paginator->$method(...$arguments);
243
            return $this;
244
        }
245
246
        throw new \BadMethodCallException('Method ' . __CLASS__ . '::' . $method . ' does not exist');
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function __debugInfo(): array
253
    {
254
        try {
255
            $info = $this->_paginator->build($this->_cursor)->__debugInfo();
256
        } catch (InsufficientConstraintsException $e) {
257
            $info = [
258
                'sql' => 'SQL could not be generated for this query as it is incomplete: ' . $e->getMessage(),
259
                'params' => [],
260
                'defaultTypes' => $this->getDefaultTypes(),
261
                'decorators' => count($this->_resultDecorators),
262
                'executed' => (bool)$this->_statement,
263
                'hydrate' => $this->_hydrate,
264
                'formatters' => count($this->_formatters),
265
                'mapReducers' => count($this->_mapReduce),
266
                'contain' => $this->_eagerLoader ? $this->_eagerLoader->getContain() : [],
267
                'matching' => $this->_eagerLoader ? $this->_eagerLoader->getMatching() : [],
268
                'extraOptions' => $this->_options,
269
                'repository' => $this->_repository,
270
            ];
271
        }
272
273
        return [
274
            '(help)' => 'This is a Lampager Query object to get the paginated results.',
275
            'paginator' => $this->_paginator,
276
        ] + $info;
277
    }
278
}
279