Completed
Pull Request — master (#4)
by Timóteo
05:46
created

Repository::createValueList()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 3
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TZachi\PhalconRepository;
6
7
use InvalidArgumentException;
8
use Phalcon\Mvc\Model;
9
use Phalcon\Mvc\Model\Criteria;
10
use Phalcon\Mvc\Model\Resultset\Simple as SimpleResultset;
11
use function array_keys;
12
use function count;
13
use function implode;
14
use function in_array;
15
use function is_array;
16
use function is_int;
17
use function range;
18
use function sprintf;
19
use function strtoupper;
20
21
class Repository
22
{
23
    public const TYPE_AND = 'AND';
24
    public const TYPE_OR  = 'OR';
25
26
    protected const OPERATORS = ['=', '<>', '<=', '>=', '<', '>', 'BETWEEN'];
27
28
    protected const CONDITION_TYPES = [self::TYPE_AND, self::TYPE_OR];
29
30
    /**
31
     * @var ModelWrapper
32
     */
33
    protected $modelWrapper;
34
35
    /**
36
     * @var string
37
     */
38
    protected $idField;
39
40 33
    public function __construct(ModelWrapper $modelWrapper, string $idField = 'id')
41
    {
42 33
        $this->modelWrapper = $modelWrapper;
43 33
        $this->idField      = $idField;
44 33
    }
45
46
    /**
47
     * Returns the first record matching the specified conditions
48
     *
49
     * @param mixed[]  $where   Where condition
50
     * @param string[] $orderBy One or more order by statements
51
     */
52 6
    public function findFirstWhere(array $where, array $orderBy = []): ?Model
53
    {
54 6
        $parameters = $this->whereToParameters($where) + $this->orderByToParameters($orderBy);
55
56 6
        $model = $this->modelWrapper->findFirst($parameters);
57 6
        if ($model === false) {
58 2
            return null;
59
        }
60
61 4
        return $model;
62
    }
63
64
    /**
65
     * Returns the first record that matches a single condition
66
     *
67
     * @param mixed    $value
68
     * @param string[] $orderBy One or more order by statements
69
     */
70 5
    public function findFirstBy(string $field, $value, array $orderBy = []): ?Model
71
    {
72 5
        return $this->findFirstWhere([$field => $value], $orderBy);
73
    }
74
75
    /**
76
     * Returns the first record that matches a primary key
77
     *
78
     * @param mixed $id
79
     */
80 3
    public function findFirst($id): ?Model
81
    {
82 3
        return $this->findFirstBy($this->idField, $id);
83
    }
84
85
    /**
86
     * Finds all records matching the specified conditions
87
     *
88
     * @param mixed[]  $where
89
     * @param string[] $orderBy
90
     */
91 6
    public function findWhere(array $where, array $orderBy = [], int $limit = 0, int $offset = 0): SimpleResultset
92
    {
93 6
        $parameters = $this->whereToParameters($where) + $this->orderByToParameters($orderBy);
94 6
        if ($limit > 0) {
95 3
            $parameters['offset'] = $offset;
96 3
            $parameters['limit']  = $limit;
97
        }
98
99 6
        return $this->modelWrapper->find($parameters);
100
    }
101
102
    /**
103
     * Finds all records in a table
104
     *
105
     * @param string[] $orderBy
106
     */
107 1
    public function findAll(array $orderBy = [], int $limit = 0, int $offset = 0): SimpleResultset
108
    {
109 1
        return $this->findWhere([], $orderBy, $limit, $offset);
110
    }
111
112
    /**
113
     * Finds all records that match a single condition
114
     *
115
     * @param mixed    $value
116
     * @param string[] $orderBy
117
     */
118 5
    public function findBy(
119
        string $field,
120
        $value,
121
        array $orderBy = [],
122
        int $limit = 0,
123
        int $offset = 0
124
    ): SimpleResultset {
125 5
        return $this->findWhere([$field => $value], $orderBy, $limit, $offset);
126
    }
127
128
    /**
129
     * Returns a query builder (Criteria) to create custom queries
130
     */
131 1
    public function query(): Criteria
132
    {
133 1
        return $this->modelWrapper->query();
134
    }
135
136
    /**
137
     * Returns the number of rows that match a certain condition
138
     *
139
     * @param mixed[] $where
140
     */
141 2
    public function count(?string $column = null, array $where = []): int
142
    {
143 2
        $parameters = $this->whereToParameters($where);
144 2
        if ($column !== null) {
145 1
            $parameters['column'] = $column;
146
        }
147
148 2
        return $this->modelWrapper->count($parameters);
149
    }
150
151
    /**
152
     * Returns the sum on a column of rows or null if the conditions don't match any rows
153
     *
154
     * @param mixed[] $where
155
     */
156 1
    public function sum(string $column, array $where = []): ?float
157
    {
158 1
        $parameters = ['column' => $column] + $this->whereToParameters($where);
159
160 1
        $sum = $this->modelWrapper->sum($parameters);
161 1
        if ($sum === null) {
162 1
            return null;
163
        }
164
165 1
        return (float) $sum;
166
    }
167
168
    /**
169
     * Returns the average on a column of rows or null if the conditions don't match any rows
170
     *
171
     * @param mixed[] $where
172
     */
173 1
    public function average(string $column, array $where = []): ?float
174
    {
175 1
        $parameters = ['column' => $column] + $this->whereToParameters($where);
176
177 1
        $average = $this->modelWrapper->average($parameters);
178 1
        if ($average === null) {
179 1
            return null;
180
        }
181
182 1
        return (float) $average;
183
    }
184
185
    /**
186
     * Returns the minimum on a column of rows or null if the conditions don't match any rows
187
     *
188
     * @param mixed[] $where
189
     */
190 1
    public function minimum(string $column, array $where = []): ?string
191
    {
192 1
        $parameters = ['column' => $column] + $this->whereToParameters($where);
193
194 1
        return $this->modelWrapper->minimum($parameters);
195
    }
196
197
    /**
198
     * Returns the maximum on a column of rows or null if the conditions don't match any rows
199
     *
200
     * @param mixed[] $where
201
     */
202 1
    public function maximum(string $column, array $where = []): ?string
203
    {
204 1
        $parameters = ['column' => $column] + $this->whereToParameters($where);
205
206 1
        return $this->modelWrapper->maximum($parameters);
207
    }
208
209
    /**
210
     * @param mixed[] $where
211
     *
212
     * @return mixed[]
213
     *
214
     * @throws InvalidArgumentException When where contains invalid values.
215
     */
216 25
    public function whereToParameters(array $where, int &$paramsIdx = 0): array
217
    {
218 25
        if ($where === []) {
219 7
            return [];
220
        }
221
222 22
        $config     = $this->extractConfigFromWhere($where);
223 21
        $conditions = [];
224 21
        $bindings   = [];
225
226 21
        foreach ($where as $field => $value) {
227 21
            if ($value === null) {
228 2
                $conditions[] = '[' . $field . '] IS NULL';
229 2
                continue;
230
            }
231
232 21
            if (is_array($value)) {
233 5
                $conditions[] = sprintf(
234 5
                    $this->createConditionsFromArray($config['operator'], $value, $bindings, $paramsIdx),
235 3
                    $field
236
                );
237
238 3
                continue;
239
            }
240
241 19
            $conditions[]         = sprintf('[%s] %s ?%d', $field, $config['operator'], $paramsIdx);
242 19
            $bindings[$paramsIdx] = $value;
243 19
            $paramsIdx++;
244
        }
245
246
        return [
247 19
            'conditions' => implode(' ' . $config['type'] . ' ', $conditions),
248 19
            'bind' => $bindings,
249
        ];
250
    }
251
252
    /**
253
     * @param mixed[] $where
254
     *
255
     * @return string[]
256
     */
257 22
    protected function extractConfigFromWhere(array &$where): array
258
    {
259
        $config = [
260 22
            'type' => $where['@type'] ?? self::TYPE_AND,
261 22
            'operator' => $where['@operator'] ?? '=',
262
        ];
263
264 22
        if (!in_array($config['operator'], self::OPERATORS, true)) {
265 1
            throw new InvalidArgumentException('Operator ' . $config['operator'] . ' is not a valid operator');
266
        }
267
268 21
        foreach (array_keys($config) as $key) {
269 21
            unset($where['@' . $key]);
270
        }
271
272 21
        return $config;
273
    }
274
275
    /**
276
     * @param mixed[] $value
277
     * @param mixed[] $bindings
278
     */
279 5
    protected function createConditionsFromArray(
280
        string $operator,
281
        array $value,
282
        array &$bindings,
283
        int &$paramsIdx
284
    ): string {
285 5
        if ($value === []) {
286 1
            throw new InvalidArgumentException('Empty array value is not allowed in where conditions');
287
        }
288
289
        // Check if $value is not an indexed array
290 4
        if (array_keys($value) !== range(0, count($value) - 1)) {
291 2
            $parameters = $this->whereToParameters($value, $paramsIdx);
292 2
            $bindings  += $parameters['bind'];
293
294 2
            return '(' . $parameters['conditions'] . ')';
295
        }
296
297 4
        if ($operator === 'BETWEEN') {
298 3
            return '[%s] BETWEEN ' . $this->createBetweenRange($value, $bindings, $paramsIdx);
299
        }
300
301 2
        return '[%s] IN (' . $this->createValueList($value, $bindings, $paramsIdx) . ')';
302
    }
303
304
    /**
305
     * @param mixed[]  $values
306
     * @param string[] $bindings
307
     */
308 3
    protected function createBetweenRange(array $values, array &$bindings, int &$paramsIdx): string
309
    {
310 3
        if (count($values) !== 2) {
311 1
            throw new InvalidArgumentException(
312 1
                'Value for between operator must be an array with exactly two values'
313
            );
314
        }
315
316 2
        $range                    = sprintf('?%d AND ?%d', $paramsIdx, $paramsIdx + 1);
317 2
        $bindings[$paramsIdx]     = $values[0];
318 2
        $bindings[$paramsIdx + 1] = $values[1];
319
320 2
        $paramsIdx += 2;
321
322 2
        return $range;
323
    }
324
325
    /**
326
     * @param mixed[]  $values
327
     * @param string[] $bindings
328
     */
329 2
    protected function createValueList(array $values, array &$bindings, int &$paramsIdx): string
330
    {
331 2
        $list = '';
332 2
        foreach ($values as $i => $value) {
333 2
            $list                .= sprintf('%s?%d', $i === 0 ? '' : ', ', $paramsIdx);
334 2
            $bindings[$paramsIdx] = $value;
335
336 2
            $paramsIdx++;
337
        }
338
339 2
        return $list;
340
    }
341
342
    /**
343
     * @param string[] $orderBy
344
     *
345
     * @return string[]
346
     */
347 16
    public function orderByToParameters(array $orderBy): array
348
    {
349 16
        if ($orderBy === []) {
350 7
            return [];
351
        }
352
353 9
        $orderByStatements = [];
354 9
        foreach ($orderBy as $sortField => $origSortOrder) {
355 9
            $sortOrder = 'ASC';
356 9
            if (strtoupper($origSortOrder) === 'DESC') {
357 4
                $sortOrder = 'DESC';
358
            }
359
360 9
            if (is_int($sortField)) {
361 7
                $sortField = $origSortOrder;
362
            }
363
364 9
            $orderByStatements[] = '[' . $sortField . '] ' . $sortOrder;
365
        }
366
367 9
        return ['order' => implode(', ', $orderByStatements)];
368
    }
369
}
370