Passed
Branch master (e85262)
by Timóteo
06:27
created

QueryParameter::createConditionsFromArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.9666
c 0
b 0
f 0
cc 4
nc 4
nop 4
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TZachi\PhalconRepository\Resolver;
6
7
use InvalidArgumentException;
8
use function array_keys;
9
use function count;
10
use function implode;
11
use function in_array;
12
use function is_array;
13
use function is_int;
14
use function range;
15
use function sprintf;
16
use function strtoupper;
17
18
/**
19
 * Class to map repository arguments to parameters that can be used in phalcon models
20
 */
21
class QueryParameter
22
{
23
    public const TYPE_AND = 'AND';
24
    public const TYPE_OR  = 'OR';
25
26
    public const ORDER_ASC  = 'ASC';
27
    public const ORDER_DESC = 'DESC';
28
29
    protected const OPERATORS = ['=', '<>', '<=', '>=', '<', '>', 'BETWEEN'];
30
31
    protected const CONDITION_TYPES = [self::TYPE_AND, self::TYPE_OR];
32
33
    /**
34
     * @var int
35
     */
36
    protected $bindingIndex;
37
38
    /**
39
     * @param mixed[] $where
40
     * @param int     $bindingStartIndex If specified, parameter binding will start from index
41
     *
42
     * @return mixed[]
43
     *
44
     * @throws InvalidArgumentException When a condition contains an invalid value
45
     */
46 14
    public function where(array $where, int $bindingStartIndex = 0): array
47
    {
48 14
        if ($where === []) {
49 1
            return [];
50
        }
51
52 13
        $this->bindingIndex = $bindingStartIndex;
53
54 13
        [$type, $operator] = $this->extractConditionConfig($where);
55 11
        $conditions        = [];
56 11
        $bindings          = [];
57
58 11
        foreach ($where as $field => $value) {
59 11
            $this->validateValueForOperator($operator, $value);
60
61 5
            if ($value === null) {
62 2
                $conditions[] = '[' . $field . '] IS NULL';
63 2
                continue;
64
            }
65
66 5
            if (is_array($value)) {
67 5
                $conditions[] = $this->createConditionsFromArray($operator, $field, $value, $bindings);
68 3
                continue;
69
            }
70
71 3
            $conditions[]                  = sprintf('[%s] %s ?%d', $field, $operator, $this->bindingIndex);
72 3
            $bindings[$this->bindingIndex] = $value;
73 3
            $this->bindingIndex++;
74
        }
75
76
        return [
77 3
            'conditions' => implode(' ' . $type . ' ', $conditions),
78 3
            'bind' => $bindings,
79
        ];
80
    }
81
82
    /**
83
     * @param string[] $orderBy
84
     *
85
     * @return string[]
86
     *
87
     * @throws InvalidArgumentException When sort direction is invalid
88
     */
89 5
    public function orderBy(array $orderBy): array
90
    {
91 5
        if ($orderBy === []) {
92 1
            return [];
93
        }
94
95 4
        $orderByStatements = [];
96 4
        foreach ($orderBy as $sortField => $sortDirection) {
97 4
            if (is_int($sortField)) {
98 2
                $sortField     = $sortDirection;
99 2
                $sortDirection = 'ASC';
100
            } else {
101 3
                $sortDirection = strtoupper($sortDirection);
102
            }
103
104 4
            if ($sortDirection !== self::ORDER_ASC && $sortDirection !== self::ORDER_DESC) {
105 1
                throw new InvalidArgumentException(
106 1
                    sprintf('Sort direction must be one of the %s::ORDER_ constants', self::class)
107
                );
108
            }
109
110 3
            $orderByStatements[] = '[' . $sortField . '] ' . $sortDirection;
111
        }
112
113 3
        return ['order' => implode(', ', $orderByStatements)];
114
    }
115
116
    /**
117
     * @return int[]
118
     */
119 2
    public function limit(int $limit, int $offset = 0): array
120
    {
121 2
        $parameters = [];
122 2
        if ($limit > 0) {
123 1
            $parameters['limit']  = $limit;
124 1
            $parameters['offset'] = $offset;
125
        }
126
127 2
        return $parameters;
128
    }
129
130
    /**
131
     * @return string[]
132
     */
133 1
    public function column(string $columnName): array
134
    {
135 1
        return ['column' => $columnName];
136
    }
137
138
    /**
139
     * @param mixed[] $where
140
     *
141
     * @return string[]
142
     *
143
     * @throws InvalidArgumentException When type or operator is invalid
144
     */
145 13
    protected function extractConditionConfig(array &$where): array
146
    {
147 13
        $type     = $where['@type'] ?? self::TYPE_AND;
148 13
        $operator = $where['@operator'] ?? '=';
149 13
        unset($where['@type'], $where['@operator']);
150
151 13
        if ($type !== self::TYPE_AND && $type !== self::TYPE_OR) {
152 1
            throw new InvalidArgumentException(
153 1
                sprintf('configuration @type must be one of the %s::TYPE_ constants', self::class)
154
            );
155
        }
156
157 12
        if (!in_array($operator, self::OPERATORS, true)) {
158 1
            throw new InvalidArgumentException(
159 1
                sprintf('configuration @operator must be one of: %s', implode(', ', self::OPERATORS))
160
            );
161
        }
162
163 11
        return [$type, $operator];
164
    }
165
166
    /**
167
     * @param mixed $value
168
     */
169 11
    protected function validateValueForOperator(string $operator, $value): void
170
    {
171 11
        if (is_array($value)) {
172 10
            if (!in_array($operator, ['=', 'BETWEEN'], true)) {
173 5
                throw new InvalidArgumentException('Operator ' . $operator . ' cannot have an array as its value');
174
            }
175
176 5
            return;
177
        }
178 4
        if ($operator === 'BETWEEN') {
179 1
            throw new InvalidArgumentException('Operator BETWEEN needs an array as its value');
180
        }
181 3
    }
182
183
    /**
184
     * @param string|int $field
185
     * @param mixed[]    $value
186
     * @param mixed[]    $bindings
187
     *
188
     * @throws InvalidArgumentException When value is an empty array
189
     */
190 5
    protected function createConditionsFromArray(string $operator, $field, array $value, array &$bindings): string
191
    {
192 5
        if ($value === []) {
193 1
            throw new InvalidArgumentException('Empty array value is not allowed in where condition');
194
        }
195
196 4
        if ($operator === 'BETWEEN') {
197 3
            return sprintf('[%s] BETWEEN %s', $field, $this->createBetweenCondition($value, $bindings));
198
        }
199
200
        // Check if $value is not an indexed array
201 3
        if (array_keys($value) !== range(0, count($value) - 1)) {
202 2
            $parameters = $this->where($value, $this->bindingIndex);
203 2
            $bindings  += $parameters['bind'];
204
205 2
            return '(' . $parameters['conditions'] . ')';
206
        }
207
208 2
        return sprintf('[%s] IN (%s)', $field, $this->createInCondition($value, $bindings, $this->bindingIndex));
209
    }
210
211
    /**
212
     * @param mixed[]  $value
213
     * @param string[] $bindings
214
     *
215
     * @throws InvalidArgumentException When value is not an array with two values
216
     */
217 3
    protected function createBetweenCondition(array $value, array &$bindings): string
218
    {
219 3
        if (count($value) !== 2) {
220 1
            throw new InvalidArgumentException(
221 1
                'Value for BETWEEN operator must be an array with exactly two values'
222
            );
223
        }
224
225 2
        $condition                       = sprintf('?%d AND ?%d', $this->bindingIndex, $this->bindingIndex + 1);
226 2
        $bindings[$this->bindingIndex++] = $value[0];
227 2
        $bindings[$this->bindingIndex++] = $value[1];
228
229 2
        return $condition;
230
    }
231
232
    /**
233
     * @param mixed[]  $values
234
     * @param string[] $bindings
235
     */
236 2
    protected function createInCondition(array $values, array &$bindings, int &$paramsIdx): string
237
    {
238 2
        $condition = '';
239 2
        foreach ($values as $i => $value) {
240 2
            $condition           .= sprintf('%s?%d', $i === 0 ? '' : ', ', $paramsIdx);
241 2
            $bindings[$paramsIdx] = $value;
242
243 2
            $paramsIdx++;
244
        }
245
246 2
        return $condition;
247
    }
248
}
249