Completed
Push — master ( be1dc2...d56e9e )
by Vitaly
06:38
created

SQLBuilder::buildArgumentCondition()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 16
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 21
rs 9.0534
1
<?php declare(strict_types = 1);
2
/**
3
 * Created by Vitaly Iegorov <[email protected]>.
4
 * on 29.08.16 at 14:58
5
 */
6
namespace samsonframework\orm;
7
8
/**
9
 * SQL statement builder class.
10
 *
11
 * @author Vitaly Egorov <[email protected]>
12
 */
13
class SQLBuilder
14
{
15
    const NUMERIC_COLUMNS_TYPES = ['int', 'float', 'longint', 'smallint', 'tinyint'];
16
    const DATE_COLUMNS_TYPES = ['date', 'datetime', 'timestamp'];
17
18
    /**
19
     * Build update statement.
20
     *
21
     * @param TableMetadata  $tableMetadata Table metadata
22
     * @param array          $columnValues  Collection of columnName => columnValue
23
     * @param Condition|null $condition     Update filtering condition
24
     *
25
     * @return string Update SQL statement
26
     * @throws \InvalidArgumentException
27
     */
28
    public function buildUpdateStatement(TableMetadata $tableMetadata, array $columnValues, Condition $condition = null) : string
29
    {
30
        $sql = [];
31
        foreach ($columnValues as $columnName => $columnValue) {
32
            $columnName = $tableMetadata->getTableColumnName($columnName);
33
            $sql[] = $this->buildFullColumnName($tableMetadata->tableName, $columnName) .
34
                $this->buildArgumentValue(
35
                    $columnValue,
36
                    $tableMetadata->getTableColumnType($columnName),
37
                    ArgumentInterface::EQUAL
38
                );
39
        }
40
41
        return 'UPDATE `' . $tableMetadata->tableName . '` SET ' . implode(', ', $sql)
42
        . ($condition !== null ? $this->buildWhereStatement($tableMetadata, $condition) : '');
43
    }
44
45
    /**
46
     * Build full table column name.
47
     *
48
     * @param string $tableName  Table name
49
     * @param string $columnName Field name
50
     *
51
     * @return string Full table column name
52
     */
53
    protected function buildFullColumnName(string $tableName, string $columnName) : string
54
    {
55
        return '`' . $tableName . '`.' . ($columnName === '*' ? '*' : '`' . $columnName . '`');
56
    }
57
58
    /**
59
     * Build argument value statement.
60
     *
61
     * @param string|array $value      Argument column value
62
     * @param string       $columnType Argument column type
63
     * @param string       $relation   Argument relation
64
     * @param bool         $nullable
65
     * @param string       $defaultValue
66
     *
67
     * @return string Argument relation with value statement
68
     */
69
    protected function buildArgumentValue(
70
        $value,
71
        string $columnType,
72
        string $relation,
73
        bool $nullable = false,
74
        $defaultValue = ''
75
    )
76
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
77
        return is_array($value)
78
            ? $this->buildArrayValue($columnType, $value, $relation)
79
            : $this->buildValue($columnType, $value, $relation, $nullable, $defaultValue);
80
    }
81
82
    /**
83
     * Build array argument value statement.
84
     *
85
     * @param string $columnType Table column type
86
     * @param array  $value      Table column array value
87
     * @param string $relation   Table column relation to value
88
     *
89
     * @return string Array argument relation with value statement
90
     */
91
    protected function buildArrayValue(string $columnType, array $value, string $relation = 'IN') : string
92
    {
93
        $relation = $relation === ArgumentInterface::NOT_EQUAL ? 'NOT IN' : 'IN';
94
95
        return $this->isColumnNumeric($columnType)
96
            ? $this->buildNumericArrayValue($value, $relation)
97
            : $this->buildStringArrayValue($value, $relation);
98
    }
99
100
    /**
101
     * Define if table column type is numeric.
102
     *
103
     * @param string $columnType Table column type
104
     *
105
     * @return bool True if column type is numeric
106
     */
107
    protected function isColumnNumeric(string $columnType) : bool
108
    {
109
        return in_array($columnType, self::NUMERIC_COLUMNS_TYPES, true);
110
    }
111
112
    /**
113
     * Build array with numeric values statement.
114
     *
115
     * @param array  $value    Array with numeric values
116
     * @param string $relation Table column relation to value
117
     *
118
     * @return string Array with numeric values statement
119
     */
120
    protected function buildNumericArrayValue(array $value, string $relation) : string
121
    {
122
        return $relation . ' (' . implode(',', $value) . ')';
123
    }
124
125
    /**
126
     * Build array string value statement.
127
     *
128
     * @param array  $value    Array with string values
129
     * @param string $relation Table column relation to value
130
     *
131
     * @return string Array with string values statement
132
     */
133
    protected function buildStringArrayValue(array $value, string $relation) : string
134
    {
135
        return $relation . ' ("' . implode('","', $value) . '")';
136
    }
137
138
    /**
139
     * Build not array argument value statement.
140
     *
141
     * @param string $columnType Table column type
142
     * @param mixed  $value      Table column value
143
     * @param string $relation   Table column relation to value
144
     *
145
     * @return string Not array argument relation with value statement
146
     */
147
    protected function buildValue(
148
        string $columnType,
149
        $value,
150
        string $relation,
151
        bool $nullable = false,
152
        $defaultValue = ''
153
    ) : string
154
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
155
        // Append space to relation if present
156
        $relation = strlen($relation) ? $relation . ' ' : '';
157
        
158
        if ($this->isColumnNumeric($columnType)) {
159
            return $relation . $this->buildNumericValue($value);
160
        } elseif ($this->isColumnDate($columnType)) {
161
            return $relation . $this->buildDateValue($value);
162
        } elseif (is_string($value)) {
163
            return $relation . $this->buildStringValue($value);
164
        } elseif ($value === null) {
165
            if ($nullable) {
166
                return $relation . $this->buildNullValue();
167
            } else {
168
                return $relation . $defaultValue;
169
            }
170
        }
171
172
        throw new \InvalidArgumentException('Cannot build column value ' . $value);
173
    }
174
175
    /**
176
     * Build not array numeric value statement.
177
     *
178
     * @param mixed $value Numeric value
179
     *
180
     * @return string Not array numeric value statement
181
     */
182
    protected function buildNumericValue($value) : string
183
    {
184
        return $value !== null ? (string)$value : '0';
185
    }
186
187
    /**
188
     * Define if table column type is date.
189
     *
190
     * @param string $columnType Table column type
191
     *
192
     * @return bool True if column type is date
193
     */
194
    protected function isColumnDate(string $columnType) : bool
195
    {
196
        return in_array($columnType, self::DATE_COLUMNS_TYPES, true);
197
    }
198
199
    /**
200
     * Build not array date value statement.
201
     *
202
     * @param mixed $value Date value
203
     *
204
     * @return string Not array date value statement
205
     */
206
    protected function buildDateValue($value) : string
207
    {
208
        return '"' . $value . '"';
209
    }
210
211
    /**
212
     * Build not array string value statement.
213
     *
214
     * @param string $value String value
215
     *
216
     * @return string Not array string value statement
217
     */
218
    protected function buildStringValue(string $value) : string
219
    {
220
        return '"' . $value . '"';
221
    }
222
223
    /**
224
     * Build not array NULL value statement.
225
     *
226
     * @return string Not array string value statement
227
     */
228
    protected function buildNullValue() : string
229
    {
230
        return 'NULL';
231
    }
232
233
    /**
234
     * Build where statement.
235
     *
236
     * @param TableMetadata $metadata
237
     * @param Condition     $condition
238
     *
239
     * @return string Limitation statement
240
     * @throws \InvalidArgumentException
241
     *
242
     */
243
    public function buildWhereStatement(TableMetadata $metadata, Condition $condition) : string
244
    {
245
        $conditions = [];
246
247
        foreach ($condition as $argument) {
248
            if ($argument instanceof ConditionInterface) {
249
                $conditions[] = $this->buildWhereStatement($metadata, $argument);
250
            } else {
251
                $conditions[] = $this->buildArgumentCondition($argument, $metadata);
0 ignored issues
show
Security Bug introduced by
It seems like $argument defined by $argument on line 247 can also be of type false; however, samsonframework\orm\SQLB...uildArgumentCondition() does only seem to accept object<samsonframework\orm\Argument>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
252
            }
253
        }
254
255
        return '(' . implode(') ' . $condition->relation . ' (', $conditions) . ')';
256
    }
257
258
    /**
259
     * Build argument condition.
260
     *
261
     * @param Argument      $argument Condition argument
262
     * @param TableMetadata $metadata Table metadata
263
     *
264
     * @return string Argument condition statement
265
     * @throws \InvalidArgumentException If argument column does not exist
266
     */
267
    protected function buildArgumentCondition(Argument $argument, TableMetadata $metadata)
268
    {
269
        switch ($argument->relation) {
270
            case ArgumentInterface::OWN:
271
                return $this->buildOwnCondition($argument->field);
272
            case ArgumentInterface::ISNULL:
273
            case ArgumentInterface::NOTNULL:
274
                $columnName = $metadata->getTableColumnName($argument->field);
275
                return $this->buildNullCondition($columnName, $argument->relation);
276
            default:
277
                $columnName = $metadata->getTableColumnName($argument->field);
278
                return $this->buildCondition(
279
                    $columnName,
280
                    $this->buildArgumentValue(
281
                        $argument->value,
282
                        $metadata->getTableColumnType($columnName),
283
                        $argument->relation
284
                    )
285
                );
286
        }
287
    }
288
289
    /**
290
     * Build own  condition statement.
291
     *
292
     * @param string $ownCondition Condition statement
293
     *
294
     * @return string Own condition statement
295
     */
296
    protected function buildOwnCondition(string $ownCondition) : string
297
    {
298
        return $this->buildCondition($ownCondition);
299
    }
300
301
    /**
302
     * Build generic condition statement.
303
     *
304
     * @param string $columnName Table column name
305
     * @param string $relation   Table column value relation
306
     * @param string $value      Table column value
307
     *
308
     * @return string Generic condition statement
309
     */
310
    protected function buildCondition(string $columnName, string $relation = '', string $value = '') : string
311
    {
312
        return trim($columnName . ' ' . $relation . ' ' . $value);
313
    }
314
315
    /**
316
     * Build is null/not null condition statement.
317
     *
318
     * @param string $columnName Table column name
319
     * @param string $nullRelation Table column null relation
320
     *
321
     * @return string Is null/not null condition statement
322
     */
323
    protected function buildNullCondition(string $columnName, string $nullRelation) : string
324
    {
325
        return $this->buildCondition($columnName, $nullRelation);
326
    }
327
328
    /**
329
     * Build insert statement.
330
     *
331
     * @param TableMetadata $tableMetadata Table metadata
332
     * @param array         $columnValues  Collection of columnName => columnValue
333
     *
334
     * @return string Insert SQL statement
335
     * @throws \InvalidArgumentException
336
     */
337
    public function buildInsertStatement(TableMetadata $tableMetadata, array $columnValues) : string
338
    {
339
        $valuesSQL = [];
340
        $columnNames = [];
341
        foreach ($columnValues as $columnName => $columnValue) {
342
            $columnNames[] = $columnName = $tableMetadata->getTableColumnName($columnName);
343
            $valuesSQL[] = $this->buildArgumentValue(
344
                $columnValue,
345
                $tableMetadata->columnTypes[$columnName],
346
                '',
347
                $tableMetadata->columnNullable[$columnName],
348
                $tableMetadata->columnDefaults[$columnName]
349
            );
350
        }
351
352
        $sql = 'INSERT INTO `' . $tableMetadata->tableName . '` (`' . implode('`, `', $columnNames) . '`) ';
353
        $sql .= 'VALUES (' . implode(', ', $valuesSQL) . ')';
354
355
        return $sql;
356
    }
357
358
    /**
359
     * Build selected fields SELECT statement part.
360
     *
361
     * @param array $tableColumns Tables and column names collection
362
     *
363
     * @return string SELECT statement
364
     */
365
    public function buildSelectStatement(array $tableColumns) : string
366
    {
367
        return 'SELECT ' . implode(', ', $this->buildFullColumnNames($tableColumns));
368
    }
369
370
    /**
371
     * Build full table column names collection.
372
     *
373
     * @param array $tableColumns Tables and column names collection
374
     *
375
     * @return array Collection of full column names for query
376
     */
377
    protected function buildFullColumnNames(array $tableColumns) : array
378
    {
379
        $grouping = [];
380
        foreach ($tableColumns as $tableName => $columnNames) {
381
            /** @var array $columnNames */
382
            foreach ($columnNames = is_array($columnNames) ? $columnNames : [$columnNames] as $columnName) {
383
                $grouping[] = $this->buildFullColumnName($tableName, $columnName);
384
            }
385
        }
386
387
        return $grouping;
388
    }
389
390
    /**
391
     * Build FROM statement part.
392
     *
393
     * @param array $tableNames Tables and column names collection
394
     *
395
     * @return string FROM statement
396
     */
397
    public function buildFromStatement(array $tableNames = []) : string
398
    {
399
        return 'FROM `' . implode('`, `', $tableNames) . '`';
400
    }
401
402
    /**
403
     * Build grouping statement.
404
     *
405
     * @param array $tableColumns Tables and column names collection
406
     *
407
     * @return string Grouping statement
408
     */
409
    public function buildGroupStatement(array $tableColumns) : string
410
    {
411
        return 'GROUP BY ' . implode(', ', $this->buildFullColumnNames($tableColumns));
412
    }
413
414
    /**
415
     * Build ordering statement.
416
     *
417
     * @param array $tableColumns Tables and column names collection
418
     * @param array $orders       Collection of columns sorting order
419
     *
420
     * @return string Ordering statement
421
     * @throws \InvalidArgumentException
422
     */
423
    public function buildOrderStatement(array $tableColumns, array $orders) : string
424
    {
425
        $ordering = [];
426
        $i = 0;
427
        foreach ($this->buildFullColumnNames($tableColumns) as $columnName) {
428
            $ordering[] = $columnName . ' ' . ($orders[$i++] ?? 'ASC');
429
        }
430
431
        return 'ORDER BY ' . implode(', ', $ordering);
432
    }
433
434
    /**
435
     * Build limitation statement.
436
     *
437
     * @param int $rows   Rows amount for limitation
438
     * @param int $offset Rows offset
439
     *
440
     * @return string Limitation statement
441
     */
442
    public function buildLimitStatement(int $rows, int $offset = 0) : string
443
    {
444
        return 'LIMIT ' . $offset . ', ' . $rows;
445
    }
446
}
447