TDbCommandBuilder::getSelectFieldList()   D
last analyzed

Complexity

Conditions 40
Paths 6

Size

Total Lines 105
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1225.1621

Importance

Changes 0
Metric Value
cc 40
eloc 65
nc 6
nop 1
dl 0
loc 105
ccs 6
cts 63
cp 0.0952
crap 1225.1621
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TDbCommandBuilder class file.
5
 *
6
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Data\Common;
12
13
use PDO;
14
use Traversable;
15
use Prado\Data\TDbCommand;
16
17
/**
18
 * TDbCommandBuilder provides basic methods to create query commands for tables
19
 * giving by {@see setTableInfo TableInfo} the property.
20
 *
21
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
22
 * @since 3.1
23
 */
24
class TDbCommandBuilder extends \Prado\TComponent
25
{
26
	private $_connection;
27
	private $_tableInfo;
28
29
	/**
30
	 * @param null|\Prado\Data\TDbConnection $connection database connection.
31
	 * @param null|TDbTableInfo $tableInfo table information.
32
	 */
33 52
	public function __construct($connection = null, $tableInfo = null)
34
	{
35 52
		$this->setDbConnection($connection);
36 52
		$this->setTableInfo($tableInfo);
37 52
		parent::__construct();
38
	}
39
40
	/**
41
	 * @return \Prado\Data\TDbConnection database connection.
42 47
	 */
43
	public function getDbConnection()
44 47
	{
45
		return $this->_connection;
46
	}
47
48
	/**
49
	 * @param \Prado\Data\TDbConnection $value database connection.
50 52
	 */
51
	public function setDbConnection($value)
52 52
	{
53 52
		$this->_connection = $value;
54
	}
55
56
	/**
57
	 * @param TDbTableInfo $value table information.
58 88
	 */
59
	public function setTableInfo($value)
60 88
	{
61 88
		$this->_tableInfo = $value;
62
	}
63
64
	/**
65
	 * @return TDbTableInfo table information.
66 47
	 */
67
	public function getTableInfo()
68 47
	{
69
		return $this->_tableInfo;
70
	}
71
72
	/**
73
	 * Iterate through all the columns and returns the last insert id of the
74
	 * first column that has a sequence or serial.
75
	 * @return mixed last insert id, null if none is found.
76 7
	 */
77
	public function getLastInsertID()
78 7
	{
79 7
		foreach ($this->getTableInfo()->getColumns() as $column) {
80 7
			if ($column->hasSequence()) {
81
				return $this->getDbConnection()->getLastInsertID($column->getSequenceName());
82
			}
83 6
		}
84
	}
85
86
	/**
87
	 * Alters the sql to apply $limit and $offset. Default implementation is applicable
88
	 * for PostgreSQL, MySQL and SQLite.
89
	 * @param string $sql SQL query string.
90
	 * @param int $limit maximum number of rows, -1 to ignore limit.
91
	 * @param int $offset row offset, -1 to ignore offset.
92
	 * @return string SQL with limit and offset.
93 20
	 */
94
	public function applyLimitOffset($sql, $limit = -1, $offset = -1)
95 20
	{
96 20
		$limit = $limit !== null ? (int) $limit : -1;
97 20
		$offset = $offset !== null ? (int) $offset : -1;
98 20
		$limitStr = $limit >= 0 ? ' LIMIT ' . $limit : '';
99 20
		$offsetStr = $offset >= 0 ? ' OFFSET ' . $offset : '';
100
		return $sql . $limitStr . $offsetStr;
101
	}
102
103
	/**
104
	 * @param string $sql SQL string without existing ordering.
105
	 * @param array $ordering pairs of column names as key and direction as value.
106
	 * @return string modified SQL applied with ORDER BY.
107 2
	 */
108
	public function applyOrdering($sql, $ordering)
109 2
	{
110 2
		$orders = [];
111 2
		foreach ($ordering as $name => $direction) {
112 2
			$direction = strtolower($direction) == 'desc' ? 'DESC' : 'ASC';
113
			if (false !== strpos($name, '(') && false !== strpos($name, ')')) {
114
				// key is a function (bad practice, but we need to handle it)
115
				$key = $name;
116
			} else {
117 2
				// key is a column
118
				$key = $this->getTableInfo()->getColumn($name)->getColumnName();
119 2
			}
120
			$orders[] = $key . ' ' . $direction;
121 2
		}
122 2
		if (count($orders) > 0) {
123
			$sql .= ' ORDER BY ' . implode(', ', $orders);
124 2
		}
125
		return $sql;
126
	}
127
128
	/**
129
	 * Computes the SQL condition for search a set of column using regular expression
130
	 * (or LIKE, depending on database implementation) to match a string of
131
	 * keywords (default matches all keywords).
132
	 * @param array $fields list of column id for potential search condition.
133
	 * @param string $keywords string of keywords
134
	 * @return string SQL search condition matching on a set of columns.
135
	 */
136
	public function getSearchExpression($fields, $keywords)
137
	{
138
		if (strlen(trim($keywords)) == 0) {
139
			return '';
140
		}
141
		$words = preg_split('/\s/u', $keywords);
142
		$conditions = [];
143
		foreach ($fields as $field) {
144
			$column = $this->getTableInfo()->getColumn($field)->getColumnName();
145
			$conditions[] = $this->getSearchCondition($column, $words);
146
		}
147
		return '(' . implode(' OR ', $conditions) . ')';
148
	}
149
150
	/**
151
	 * @param string $column column name.
152
	 * @param array $words keywords
153
	 * @return string search condition for all words in one column.
154
	 */
155
	protected function getSearchCondition($column, $words)
156
	{
157
		$conditions = [];
158
		foreach ($words as $word) {
159
			$conditions[] = $column . ' LIKE ' . $this->getDbConnection()->quoteString('%' . $word . '%');
160
		}
161
		return '(' . implode(' AND ', $conditions) . ')';
162
	}
163
164
	/**
165
	 *
166
	 * Different behavior depends on type of passed data
167
	 * string
168
	 * 	usage without modification
169
	 *
170
	 * null
171
	 * 	will be expanded to full list of quoted table column names (quoting depends on database)
172
	 *
173
	 * array
174
	 * - Column names will be quoted if used as key or value of array
175
	 * ```php
176
	 * array('col1', 'col2', 'col2')
177
	 * // SELECT `col1`, `col2`, `col3` FROM...
178
	 * ```
179
	 *
180
	 * - Column aliasing
181
	 * ```php
182
	 * array('mycol1' => 'col1', 'mycol2' => 'COUNT(*)')
183
	 * // SELECT `col1` AS mycol1, COUNT(*) AS mycol2 FROM...
184
	 * ```
185
	 *
186
	 * - NULL and scalar values (strings will be quoted depending on database)
187
	 * ```php
188
	 * array('col1' => 'my custom string', 'col2' => 1.0, 'col3' => 'NULL')
189
	 * // SELECT "my custom string" AS `col1`, 1.0 AS `col2`, NULL AS `col3` FROM...
190
	 * ```
191
	 *
192
	 * - If the *-wildcard char is used as key or value, add the full list of quoted table column names
193
	 * ```php
194
	 * array('col1' => 'NULL', '*')
195
	 * // SELECT `col1`, `col2`, `col3`, NULL AS `col1` FROM...
196
	 * ```
197
	 * @param mixed $data
198
	 * @return array of generated fields - use implode(', ', $selectfieldlist) to collapse field list for usage
199
	 * @since 3.1.7
200
	 * @todo add support for table aliasing
201
	 * @todo add support for quoting of column aliasing
202 40
	 */
203
	public function getSelectFieldList($data = '*')
204 40
	{
205 40
		if (is_scalar($data)) {
206 40
			$tmp = explode(',', $data);
207 40
			$result = [];
208 40
			foreach ($tmp as $v) {
209
				$result[] = trim($v);
210 40
			}
211
			return $result;
212
		}
213
214
		$bHasWildcard = false;
215
		$result = [];
216
		if (is_array($data) || $data instanceof Traversable) {
217
			$columns = $this->getTableInfo()->getColumns();
218
			foreach ($data as $key => $value) {
219
				if ($key === '*' || $value === '*') {
220
					$bHasWildcard = true;
221
					continue;
222
				}
223
224
				if (strToUpper($key) === 'NULL') {
225
					$result[] = 'NULL';
226
					continue;
227
				}
228
229
				if (strpos($key, '(') !== false && strpos($key, ')') !== false) {
230
					$result[] = $key;
231
					continue;
232
				}
233
234
				if (stripos($key, 'AS') !== false) {
235
					$result[] = $key;
236
					continue;
237
				}
238
239
				if (stripos($value, 'AS') !== false) {
240
					$result[] = $value;
241
					continue;
242
				}
243
244
				$v = isset($columns[$value]);
245
				$k = isset($columns[$key]);
246
				if (is_int($key) && $v) {
247
					$key = $value;
248
					$k = $v;
249
				}
250
251
				if (strToUpper($value) === 'NULL') {
252
					if ($k) {
253
						$result[] = 'NULL AS ' . $columns[$key]->getColumnName();
254
					} else {
255
						$result[] = 'NULL' . (is_string($key) ? (' AS ' . (string) $key) : '');
256
					}
257
					continue;
258
				}
259
260
				if (strpos($value, '(') !== false && strpos($value, ')') !== false) {
261
					if ($k) {
262
						$result[] = $value . ' AS ' . $columns[$key]->getColumnName();
263
					} else {
264
						$result[] = $value . (is_string($key) ? (' AS ' . (string) $key) : '');
265
					}
266
					continue;
267
				}
268
269
				if ($v && $key == $value) {
270
					$result[] = $columns[$value]->getColumnName();
271
					continue;
272
				}
273
274
				if ($k && $value == null) {
275
					$result[] = $columns[$key]->getColumnName();
276
					continue;
277
				}
278
279
				if (is_string($key) && $v) {
280
					$result[] = $columns[$value]->getColumnName() . ' AS ' . $key;
281
					continue;
282
				}
283
284
				if (is_numeric($value) && $k) {
285
					$result[] = $value . ' AS ' . $columns[$key]->getColumnName();
286
					continue;
287
				}
288
289
				if (is_string($value) && $k) {
290
					$result[] = $this->getDbConnection()->quoteString($value) . ' AS ' . $columns[$key]->getColumnName();
291
					continue;
292
				}
293
294
				if (!$v && !$k && is_int($key)) {
295
					$result[] = is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string) $value);
296
					continue;
297
				}
298
299
				$result[] = (is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string) $value)) . ' AS ' . $key;
300
			}
301
		}
302
303
		if ($data === null || count($result) == 0 || $bHasWildcard) {
304
			$result = $result = array_merge($this->getTableInfo()->getColumnNames(), $result);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
305
		}
306
307
		return $result;
308
	}
309
310
	/**
311
	 * Appends the $where condition to the string "SELECT * FROM tableName WHERE ".
312
	 * The tableName is obtained from the {@see setTableInfo TableInfo} property.
313
	 * @param string $where query condition
314
	 * @param array $parameters condition parameters.
315
	 * @param array $ordering
316
	 * @param int $limit
317
	 * @param int $offset
318
	 * @param string $select
319
	 * @return TDbCommand query command.
320 40
	 */
321
	public function createFindCommand($where = '1=1', $parameters = [], $ordering = [], $limit = -1, $offset = -1, $select = '*')
322 40
	{
323 40
		$table = $this->getTableInfo()->getTableFullName();
324 40
		$fields = implode(', ', $this -> getSelectFieldList($select));
325 40
		$sql = "SELECT {$fields} FROM {$table}";
326 38
		if (!empty($where)) {
327
			$sql .= " WHERE {$where}";
328 40
		}
329
		return $this->applyCriterias($sql, $parameters, $ordering, $limit, $offset);
330
	}
331 40
332
	public function applyCriterias($sql, $parameters = [], $ordering = [], $limit = -1, $offset = -1)
333 40
	{
334 2
		if (count($ordering) > 0) {
335
			$sql = $this->applyOrdering($sql, $ordering);
336 40
		}
337 26
		if ($limit >= 0 || $offset >= 0) {
338
			$sql = $this->applyLimitOffset($sql, $limit, $offset);
339 40
		}
340 40
		$command = $this->createCommand($sql);
341 40
		$this->bindArrayValues($command, $parameters);
342
		return $command;
343
	}
344
345
	/**
346
	 * Creates a count(*) command for the table described in {@see setTableInfo TableInfo}.
347
	 * @param string $where count condition.
348
	 * @param array $parameters binding parameters.
349
	 * @param array $ordering
350
	 * @param int $limit
351
	 * @param int $offset
352
	 * @return TDbCommand count command.
353 4
	 */
354
	public function createCountCommand($where = '1=1', $parameters = [], $ordering = [], $limit = -1, $offset = -1)
355 4
	{
356
		return $this->createFindCommand($where, $parameters, $ordering, $limit, $offset, 'COUNT(*)');
357
	}
358
359
	/**
360
	 * Creates a delete command for the table described in {@see setTableInfo TableInfo}.
361
	 * The conditions for delete is given by the $where argument and the parameters
362
	 * for the condition is given by $parameters.
363
	 * @param string $where delete condition.
364
	 * @param array $parameters delete parameters.
365
	 * @return TDbCommand delete command.
366 12
	 */
367
	public function createDeleteCommand($where, $parameters = [])
368 12
	{
369 12
		$table = $this->getTableInfo()->getTableFullName();
370 12
		if (!empty($where)) {
371
			$where = ' WHERE ' . $where;
372 12
		}
373 12
		$command = $this->createCommand("DELETE FROM {$table}" . $where);
374 12
		$this->bindArrayValues($command, $parameters);
375
		return $command;
376
	}
377
378
	/**
379
	 * Creates an insert command for the table described in {@see setTableInfo TableInfo} for the given data.
380
	 * Each array key in the $data array must correspond to the column name of the table
381
	 * (if a column allows to be null, it may be omitted) to be inserted with
382
	 * the corresponding array value.
383
	 * @param array $data name-value pairs of new data to be inserted.
384
	 * @return TDbCommand insert command
385 8
	 */
386
	public function createInsertCommand($data)
387 8
	{
388 8
		$table = $this->getTableInfo()->getTableFullName();
389 8
		[$fields, $bindings] = $this->getInsertFieldBindings($data);
0 ignored issues
show
Bug introduced by
$data of type array is incompatible with the type object expected by parameter $values of Prado\Data\Common\TDbCom...etInsertFieldBindings(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

389
		[$fields, $bindings] = $this->getInsertFieldBindings(/** @scrutinizer ignore-type */ $data);
Loading history...
390 8
		$command = $this->createCommand("INSERT INTO {$table}({$fields}) VALUES ($bindings)");
391 8
		$this->bindColumnValues($command, $data);
392
		return $command;
393
	}
394
395
	/**
396
	 * Creates an update command for the table described in {@see setTableInfo TableInfo} for the given data.
397
	 * Each array key in the $data array must correspond to the column name to be updated with the corresponding array value.
398
	 * @param array $data name-value pairs of data to be updated.
399
	 * @param string $where update condition.
400
	 * @param array $parameters update parameters.
401
	 * @return TDbCommand update command.
402 3
	 */
403
	public function createUpdateCommand($data, $where, $parameters = [])
404 3
	{
405 3
		$table = $this->getTableInfo()->getTableFullName();
406
		if ($this->hasIntegerKey($parameters)) {
407
			$fields = implode(', ', $this->getColumnBindings($data, true));
0 ignored issues
show
Bug introduced by
$this->getColumnBindings($data, true) of type string is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

407
			$fields = implode(', ', /** @scrutinizer ignore-type */ $this->getColumnBindings($data, true));
Loading history...
408 3
		} else {
409
			$fields = implode(', ', $this->getColumnBindings($data));
410
		}
411 3
412 3
		if (!empty($where)) {
413
			$where = ' WHERE ' . $where;
414 3
		}
415 3
		$command = $this->createCommand("UPDATE {$table} SET {$fields}" . $where);
416 3
		$this->bindArrayValues($command, array_merge($data, $parameters));
417
		return $command;
418
	}
419
420
	/**
421
	 * Returns a list of insert field name and a list of binding names.
422
	 * @param object $values array or object to be inserted.
423
	 * @return array tuple ($fields, $bindings)
424 8
	 */
425
	protected function getInsertFieldBindings($values)
426 8
	{
427 8
		$fields = [];
428 8
		$bindings = [];
429 8
		foreach (array_keys($values) as $name) {
0 ignored issues
show
Bug introduced by
$values of type object is incompatible with the type array expected by parameter $array of array_keys(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

429
		foreach (array_keys(/** @scrutinizer ignore-type */ $values) as $name) {
Loading history...
430 8
			$fields[] = $this->getTableInfo()->getColumn($name)->getColumnName();
431
			$bindings[] = ':' . $name;
432 8
		}
433
		return [implode(', ', $fields), implode(', ', $bindings)];
434
	}
435
436
	/**
437
	 * Create a name-value or position-value if $position=true binding strings.
438
	 * @param array $values data for binding.
439
	 * @param bool $position true to bind as position values.
440
	 * @return string update column names with corresponding binding substrings.
441 3
	 */
442
	protected function getColumnBindings($values, $position = false)
443 3
	{
444 3
		$bindings = [];
445 3
		foreach (array_keys($values) as $name) {
446 3
			$column = $this->getTableInfo()->getColumn($name)->getColumnName();
447
			$bindings[] = $position ? $column . ' = ?' : $column . ' = :' . $name;
448 3
		}
449
		return $bindings;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $bindings returns the type array|string[] which is incompatible with the documented return type string.
Loading history...
450
	}
451
452
	/**
453
	 * @param string $sql SQL query string.
454
	 * @return TDbCommand corresponding database command.
455 47
	 */
456
	public function createCommand($sql)
457 47
	{
458 47
		$this->getDbConnection()->setActive(true);
459
		return $this->getDbConnection()->createCommand($sql);
460
	}
461
462
	/**
463
	 * Bind the name-value pairs of $values where the array keys correspond to column names.
464
	 * @param TDbCommand $command database command.
465
	 * @param array $values name-value pairs.
466 8
	 */
467
	public function bindColumnValues($command, $values)
468 8
	{
469 8
		foreach ($values as $name => $value) {
470 8
			$column = $this->getTableInfo()->getColumn($name);
471
			if ($value === null && $column->getAllowNull()) {
472
				$command->bindValue(':' . $name, null, PDO::PARAM_NULL);
473 8
			} else {
474
				$command->bindValue(':' . $name, $value, $column->getPdoType());
475
			}
476 8
		}
477
	}
478
479
	/**
480
	 * @param TDbCommand $command database command
481
	 * @param array $values values for binding.
482 46
	 */
483
	public function bindArrayValues($command, $values)
484 46
	{
485 19
		if ($this->hasIntegerKey($values)) {
486 19
			$values = array_values($values);
487 19
			for ($i = 0, $max = count($values); $i < $max; $i++) {
488
				$command->bindValue($i + 1, $values[$i], $this->getPdoType($values[$i]));
489
			}
490 35
		} else {
491 11
			foreach ($values as $name => $value) {
492 11
				$prop = $name[0] === ':' ? $name : ':' . $name;
493
				$command->bindValue($prop, $value, $this->getPdoType($value));
494
			}
495 46
		}
496
	}
497
498
	/**
499
	 * @param mixed $value PHP value
500
	 * @return null|int PDO parameter types.
501 114
	 */
502
	public static function getPdoType($value)
503 114
	{
504 114
		switch (gettype($value)) {
505 114
			case 'boolean': return PDO::PARAM_BOOL;
506 52
			case 'integer': return PDO::PARAM_INT;
507 13
			case 'string': return PDO::PARAM_STR;
508
			case 'NULL': return PDO::PARAM_NULL;
509 3
		}
510
		return null;
511
	}
512
513
	/**
514 46
	 * @param array $array
515
	 * @return bool true if any array key is an integer.
516 46
	 */
517 30
	protected function hasIntegerKey($array)
518 30
	{
519
		foreach ($array as $k => $v) {
520
			if (gettype($k) === 'integer') {
521 35
				return true;
522
			}
523
		}
524
		return false;
525
	}
526
}
527