QueryParameter::where()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

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