Passed
Pull Request — master (#5)
by Timóteo
02:23
created

QueryParameter::createConditionsFromArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 3
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 8
    public function where(array $where, int $bindingStartIndex = 0): array
47
    {
48 8
        if ($where === []) {
49 1
            return [];
50
        }
51
52 7
        $this->bindingIndex = $bindingStartIndex;
53
54 7
        [$type, $operator] = $this->extractConditionConfig($where);
55 5
        $conditions        = [];
56 5
        $bindings          = [];
57
58 5
        foreach ($where as $field => $value) {
59 5
            if ($value === null) {
60 2
                $conditions[] = '[' . $field . '] IS NULL';
61 2
                continue;
62
            }
63
64 5
            if ($operator === 'BETWEEN') {
65 3
                $conditions[] = sprintf('[%s] BETWEEN %s', $field, $this->createBetweenCondition($value, $bindings));
66 2
                continue;
67
            }
68
69 4
            if (is_array($value)) {
70 4
                $conditions[] = $this->createConditionsFromArray($field, $value, $bindings);
71 3
                continue;
72
            }
73
74 3
            $conditions[]                  = sprintf('[%s] %s ?%d', $field, $operator, $this->bindingIndex);
75 3
            $bindings[$this->bindingIndex] = $value;
76 3
            $this->bindingIndex++;
77
        }
78
79
        return [
80 3
            'conditions' => implode(' ' . $type . ' ', $conditions),
81 3
            'bind' => $bindings,
82
        ];
83
    }
84
85
    /**
86
     * @param string[] $orderBy
87
     *
88
     * @return string[]
89
     *
90
     * @throws InvalidArgumentException When sort direction is invalid
91
     */
92 5
    public function orderBy(array $orderBy): array
93
    {
94 5
        if ($orderBy === []) {
95 1
            return [];
96
        }
97
98 4
        $orderByStatements = [];
99 4
        foreach ($orderBy as $sortField => $sortDirection) {
100 4
            if (is_int($sortField)) {
101 2
                $sortField     = $sortDirection;
102 2
                $sortDirection = 'ASC';
103
            } else {
104 3
                $sortDirection = strtoupper($sortDirection);
105
            }
106
107 4
            if ($sortDirection !== self::ORDER_ASC && $sortDirection !== self::ORDER_DESC) {
108 1
                throw new InvalidArgumentException(
109 1
                    sprintf('Sort direction must be one of the %s::ORDER_ constants', self::class)
110
                );
111
            }
112
113 3
            $orderByStatements[] = '[' . $sortField . '] ' . $sortDirection;
114
        }
115
116 3
        return ['order' => implode(', ', $orderByStatements)];
117
    }
118
119
    /**
120
     * @return int[]
121
     */
122 2
    public function limit(int $limit, int $offset = 0): array
123
    {
124 2
        $parameters = [];
125 2
        if ($limit > 0) {
126 1
            $parameters['limit']  = $limit;
127 1
            $parameters['offset'] = $offset;
128
        }
129
130 2
        return $parameters;
131
    }
132
133
    /**
134
     * @return string[]
135
     */
136 1
    public function column(string $columnName): array
137
    {
138 1
        return ['column' => $columnName];
139
    }
140
141
    /**
142
     * @param mixed[] $where
143
     *
144
     * @return string[]
145
     *
146
     * @throws InvalidArgumentException When type or operator is invalid
147
     */
148 7
    protected function extractConditionConfig(array &$where): array
149
    {
150 7
        $type     = $where['@type'] ?? self::TYPE_AND;
151 7
        $operator = $where['@operator'] ?? '=';
152 7
        unset($where['@type'], $where['@operator']);
153
154 7
        if ($type !== self::TYPE_AND && $type !== self::TYPE_OR) {
155 1
            throw new InvalidArgumentException(
156 1
                sprintf('configuration @type must be one of the %s::TYPE_ constants', self::class)
157
            );
158
        }
159
160 6
        if (!in_array($operator, self::OPERATORS, true)) {
161 1
            throw new InvalidArgumentException(
162 1
                sprintf('configuration @operator must be one of: %s', implode(', ', self::OPERATORS))
163
            );
164
        }
165
166 5
        return [$type, $operator];
167
    }
168
169
    /**
170
     * @param string|int $field
171
     * @param mixed[]    $value
172
     * @param mixed[]    $bindings
173
     *
174
     * @throws InvalidArgumentException When value is an empty array
175
     */
176 4
    protected function createConditionsFromArray($field, array $value, array &$bindings): string
177
    {
178 4
        if ($value === []) {
179 1
            throw new InvalidArgumentException('Empty array value is not allowed in where condition');
180
        }
181
182
        // Check if $value is not an indexed array
183 3
        if (array_keys($value) !== range(0, count($value) - 1)) {
184 2
            $parameters = $this->where($value, $this->bindingIndex);
185 2
            $bindings  += $parameters['bind'];
186
187 2
            return '(' . $parameters['conditions'] . ')';
188
        }
189
190 2
        return sprintf('[%s] IN (%s)', $field, $this->createInCondition($value, $bindings, $this->bindingIndex));
191
    }
192
193
    /**
194
     * @param mixed    $value
195
     * @param string[] $bindings
196
     *
197
     * @throws InvalidArgumentException When value is not an array with two values
198
     */
199 3
    protected function createBetweenCondition($value, array &$bindings): string
200
    {
201 3
        if (count($value) !== 2) {
202 1
            throw new InvalidArgumentException(
203 1
                'Value for between operator must be an array with exactly two values'
204
            );
205
        }
206
207 2
        $condition                       = sprintf('?%d AND ?%d', $this->bindingIndex, $this->bindingIndex + 1);
208 2
        $bindings[$this->bindingIndex++] = $value[0];
209 2
        $bindings[$this->bindingIndex++] = $value[1];
210
211 2
        return $condition;
212
    }
213
214
    /**
215
     * @param mixed[]  $values
216
     * @param string[] $bindings
217
     */
218 2
    protected function createInCondition(array $values, array &$bindings, int &$paramsIdx): string
219
    {
220 2
        $condition = '';
221 2
        foreach ($values as $i => $value) {
222 2
            $condition           .= sprintf('%s?%d', $i === 0 ? '' : ', ', $paramsIdx);
223 2
            $bindings[$paramsIdx] = $value;
224
225 2
            $paramsIdx++;
226
        }
227
228 2
        return $condition;
229
    }
230
}
231