Completed
Branch master (099915)
by Fabio
08:02
created

TDbCommandBuilder::getSearchExpression()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 12
ccs 0
cts 9
cp 0
crap 12
rs 10
1
<?php
2
/**
3
 * TDbCommandBuilder class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 * @package Prado\Data\Common
9
 */
10
11
namespace Prado\Data\Common;
12
13
use PDO;
14
use Traversable;
15
16
/**
17
 * TDbCommandBuilder provides basic methods to create query commands for tables
18
 * giving by {@link setTableInfo TableInfo} the property.
19
 *
20
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
21
 * @package Prado\Data\Common
22
 * @since 3.1
23
 */
24
class TDbCommandBuilder extends \Prado\TComponent
25
{
26
	private $_connection;
27
	private $_tableInfo;
28
29
	/**
30
	 * @param null|TDbConnection $connection database connection.
0 ignored issues
show
Bug introduced by
The type Prado\Data\Common\TDbConnection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
	}
38
39
	/**
40
	 * @return TDbConnection database connection.
41
	 */
42 47
	public function getDbConnection()
43
	{
44 47
		return $this->_connection;
45
	}
46
47
	/**
48
	 * @param TDbConnection $value database connection.
49
	 */
50 52
	public function setDbConnection($value)
51
	{
52 52
		$this->_connection = $value;
53 52
	}
54
55
	/**
56
	 * @param TDbTableInfo $value table information.
57
	 */
58 88
	public function setTableInfo($value)
59
	{
60 88
		$this->_tableInfo = $value;
61 88
	}
62
63
	/**
64
	 * @return TDbTableInfo table information.
65
	 */
66 47
	public function getTableInfo()
67
	{
68 47
		return $this->_tableInfo;
69
	}
70
71
	/**
72
	 * Iterate through all the columns and returns the last insert id of the
73
	 * first column that has a sequence or serial.
74
	 * @return mixed last insert id, null if none is found.
75
	 */
76 7
	public function getLastInsertID()
77
	{
78 7
		foreach ($this->getTableInfo()->getColumns() as $column) {
79 7
			if ($column->hasSequence()) {
80 7
				return $this->getDbConnection()->getLastInsertID($column->getSequenceName());
81
			}
82
		}
83 6
	}
84
85
	/**
86
	 * Alters the sql to apply $limit and $offset. Default implementation is applicable
87
	 * for PostgreSQL, MySQL and SQLite.
88
	 * @param string $sql SQL query string.
89
	 * @param int $limit maximum number of rows, -1 to ignore limit.
90
	 * @param int $offset row offset, -1 to ignore offset.
91
	 * @return string SQL with limit and offset.
92
	 */
93 20
	public function applyLimitOffset($sql, $limit = -1, $offset = -1)
94
	{
95 20
		$limit = $limit !== null ? (int) $limit : -1;
96 20
		$offset = $offset !== null ? (int) $offset : -1;
97 20
		$limitStr = $limit >= 0 ? ' LIMIT ' . $limit : '';
98 20
		$offsetStr = $offset >= 0 ? ' OFFSET ' . $offset : '';
99 20
		return $sql . $limitStr . $offsetStr;
100
	}
101
102
	/**
103
	 * @param string $sql SQL string without existing ordering.
104
	 * @param array $ordering pairs of column names as key and direction as value.
105
	 * @return string modified SQL applied with ORDER BY.
106
	 */
107 2
	public function applyOrdering($sql, $ordering)
108
	{
109 2
		$orders = [];
110 2
		foreach ($ordering as $name => $direction) {
111 2
			$direction = strtolower($direction) == 'desc' ? 'DESC' : 'ASC';
112 2
			if (false !== strpos($name, '(') && false !== strpos($name, ')')) {
113
				// key is a function (bad practice, but we need to handle it)
114
				$key = $name;
115
			} else {
116
				// key is a column
117 2
				$key = $this->getTableInfo()->getColumn($name)->getColumnName();
118
			}
119 2
			$orders[] = $key . ' ' . $direction;
120
		}
121 2
		if (count($orders) > 0) {
122 2
			$sql .= ' ORDER BY ' . implode(', ', $orders);
123
		}
124 2
		return $sql;
125
	}
126
127
	/**
128
	 * Computes the SQL condition for search a set of column using regular expression
129
	 * (or LIKE, depending on database implementation) to match a string of
130
	 * keywords (default matches all keywords).
131
	 * @param array $fields list of column id for potential search condition.
132
	 * @param string $keywords string of keywords
133
	 * @return string SQL search condition matching on a set of columns.
134
	 */
135
	public function getSearchExpression($fields, $keywords)
136
	{
137
		if (strlen(trim($keywords)) == 0) {
138
			return '';
139
		}
140
		$words = preg_split('/\s/u', $keywords);
141
		$conditions = [];
142
		foreach ($fields as $field) {
143
			$column = $this->getTableInfo()->getColumn($field)->getColumnName();
144
			$conditions[] = $this->getSearchCondition($column, $words);
0 ignored issues
show
Bug introduced by
It seems like $words can also be of type false; however, parameter $words of Prado\Data\Common\TDbCom...r::getSearchCondition() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

144
			$conditions[] = $this->getSearchCondition($column, /** @scrutinizer ignore-type */ $words);
Loading history...
145
		}
146
		return '(' . implode(' OR ', $conditions) . ')';
147
	}
148
149
	/**
150
	 * @param string $column column name.
151
	 * @param array $words keywords
152
	 * @return string search condition for all words in one column.
153
	 */
154
	protected function getSearchCondition($column, $words)
155
	{
156
		$conditions = [];
157
		foreach ($words as $word) {
158
			$conditions[] = $column . ' LIKE ' . $this->getDbConnection()->quoteString('%' . $word . '%');
159
		}
160
		return '(' . implode(' AND ', $conditions) . ')';
161
	}
162
163
	/**
164
	 *
165
	 * Different behavior depends on type of passed data
166
	 * string
167
	 * 	usage without modification
168
	 *
169
	 * null
170
	 * 	will be expanded to full list of quoted table column names (quoting depends on database)
171
	 *
172
	 * array
173
	 * - Column names will be quoted if used as key or value of array
174
	 * 	<code>
175
	 * 	array('col1', 'col2', 'col2')
176
	 * 	// SELECT `col1`, `col2`, `col3` FROM...
177
	 * 	</code>
178
	 *
179
	 * - Column aliasing
180
	 * <code>
181
	 * array('mycol1' => 'col1', 'mycol2' => 'COUNT(*)')
182
	 * // SELECT `col1` AS mycol1, COUNT(*) AS mycol2 FROM...
183
	 * </code>
184
	 *
185
	 * - NULL and scalar values (strings will be quoted depending on database)
186
	 * <code>
187
	 * array('col1' => 'my custom string', 'col2' => 1.0, 'col3' => 'NULL')
188
	 * // SELECT "my custom string" AS `col1`, 1.0 AS `col2`, NULL AS `col3` FROM...
189
	 * </code>
190
	 *
191
	 * - If the *-wildcard char is used as key or value, add the full list of quoted table column names
192
	 * <code>
193
	 * array('col1' => 'NULL', '*')
194
	 * // SELECT `col1`, `col2`, `col3`, NULL AS `col1` FROM...
195
	 * </code>
196
	 * @param mixed $data
197
	 * @return array of generated fields - use implode(', ', $selectfieldlist) to collapse field list for usage
198
	 * @since 3.1.7
199
	 * @todo add support for table aliasing
200
	 * @todo add support for quoting of column aliasing
201
	 */
202 40
	public function getSelectFieldList($data = '*')
203
	{
204 40
		if (is_scalar($data)) {
205 40
			$tmp = explode(',', $data);
206 40
			$result = [];
207 40
			foreach ($tmp as $v) {
208 40
				$result[] = trim($v);
209
			}
210 40
			return $result;
211
		}
212
213
		$bHasWildcard = false;
214
		$result = [];
215
		if (is_array($data) || $data instanceof Traversable) {
216
			$columns = $this->getTableInfo()->getColumns();
217
			foreach ($data as $key => $value) {
218
				if ($key === '*' || $value === '*') {
219
					$bHasWildcard = true;
220
					continue;
221
				}
222
223
				if (strToUpper($key) === 'NULL') {
224
					$result[] = 'NULL';
225
					continue;
226
				}
227
228
				if (strpos($key, '(') !== false && strpos($key, ')') !== false) {
229
					$result[] = $key;
230
					continue;
231
				}
232
233
				if (stripos($key, 'AS') !== false) {
234
					$result[] = $key;
235
					continue;
236
				}
237
238
				if (stripos($value, 'AS') !== false) {
239
					$result[] = $value;
240
					continue;
241
				}
242
243
				$v = isset($columns[$value]);
244
				$k = isset($columns[$key]);
245
				if (is_int($key) && $v) {
246
					$key = $value;
247
					$k = $v;
248
				}
249
250
				if (strToUpper($value) === 'NULL') {
251
					if ($k) {
252
						$result[] = 'NULL AS ' . $columns[$key]->getColumnName();
253
					} else {
254
						$result[] = 'NULL' . (is_string($key) ? (' AS ' . (string) $key) : '');
255
					}
256
					continue;
257
				}
258
259
				if (strpos($value, '(') !== false && strpos($value, ')') !== false) {
260
					if ($k) {
261
						$result[] = $value . ' AS ' . $columns[$key]->getColumnName();
262
					} else {
263
						$result[] = $value . (is_string($key) ? (' AS ' . (string) $key) : '');
264
					}
265
					continue;
266
				}
267
268
				if ($v && $key == $value) {
269
					$result[] = $columns[$value]->getColumnName();
270
					continue;
271
				}
272
273
				if ($k && $value == null) {
274
					$result[] = $columns[$key]->getColumnName();
275
					continue;
276
				}
277
278
				if (is_string($key) && $v) {
279
					$result[] = $columns[$value]->getColumnName() . ' AS ' . $key;
280
					continue;
281
				}
282
283
				if (is_numeric($value) && $k) {
284
					$result[] = $value . ' AS ' . $columns[$key]->getColumnName();
285
					continue;
286
				}
287
288
				if (is_string($value) && $k) {
289
					$result[] = $this->getDbConnection()->quoteString($value) . ' AS ' . $columns[$key]->getColumnName();
290
					continue;
291
				}
292
293
				if (!$v && !$k && is_int($key)) {
294
					$result[] = is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string) $value);
295
					continue;
296
				}
297
298
				$result[] = (is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string) $value)) . ' AS ' . $key;
299
			}
300
		}
301
302
		if ($data === null || count($result) == 0 || $bHasWildcard) {
303
			$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...
304
		}
305
306
		return $result;
307
	}
308
309
	/**
310
	 * Appends the $where condition to the string "SELECT * FROM tableName WHERE ".
311
	 * The tableName is obtained from the {@link setTableInfo TableInfo} property.
312
	 * @param string $where query condition
313
	 * @param array $parameters condition parameters.
314
	 * @param array $ordering
315
	 * @param int $limit
316
	 * @param int $offset
317
	 * @param string $select
318
	 * @return TDbCommand query command.
319
	 */
320 40
	public function createFindCommand($where = '1=1', $parameters = [], $ordering = [], $limit = -1, $offset = -1, $select = '*')
321
	{
322 40
		$table = $this->getTableInfo()->getTableFullName();
323 40
		$fields = implode(', ', $this -> getSelectFieldList($select));
324 40
		$sql = "SELECT {$fields} FROM {$table}";
325 40
		if (!empty($where)) {
326 38
			$sql .= " WHERE {$where}";
327
		}
328 40
		return $this->applyCriterias($sql, $parameters, $ordering, $limit, $offset);
329
	}
330
331 40
	public function applyCriterias($sql, $parameters = [], $ordering = [], $limit = -1, $offset = -1)
332
	{
333 40
		if (count($ordering) > 0) {
334 2
			$sql = $this->applyOrdering($sql, $ordering);
335
		}
336 40
		if ($limit >= 0 || $offset >= 0) {
337 26
			$sql = $this->applyLimitOffset($sql, $limit, $offset);
338
		}
339 40
		$command = $this->createCommand($sql);
340 40
		$this->bindArrayValues($command, $parameters);
341 40
		return $command;
342
	}
343
344
	/**
345
	 * Creates a count(*) command for the table described in {@link setTableInfo TableInfo}.
346
	 * @param string $where count condition.
347
	 * @param array $parameters binding parameters.
348
	 * @param array $ordering
349
	 * @param int $limit
350
	 * @param int $offset
351
	 * @return TDbCommand count command.
352
	 */
353 4
	public function createCountCommand($where = '1=1', $parameters = [], $ordering = [], $limit = -1, $offset = -1)
354
	{
355 4
		return $this->createFindCommand($where, $parameters, $ordering, $limit, $offset, 'COUNT(*)');
356
	}
357
358
	/**
359
	 * Creates a delete command for the table described in {@link setTableInfo TableInfo}.
360
	 * The conditions for delete is given by the $where argument and the parameters
361
	 * for the condition is given by $parameters.
362
	 * @param string $where delete condition.
363
	 * @param array $parameters delete parameters.
364
	 * @return TDbCommand delete command.
365
	 */
366 12
	public function createDeleteCommand($where, $parameters = [])
367
	{
368 12
		$table = $this->getTableInfo()->getTableFullName();
369 12
		if (!empty($where)) {
370 12
			$where = ' WHERE ' . $where;
371
		}
372 12
		$command = $this->createCommand("DELETE FROM {$table}" . $where);
373 12
		$this->bindArrayValues($command, $parameters);
374 12
		return $command;
375
	}
376
377
	/**
378
	 * Creates an insert command for the table described in {@link setTableInfo TableInfo} for the given data.
379
	 * Each array key in the $data array must correspond to the column name of the table
380
	 * (if a column allows to be null, it may be omitted) to be inserted with
381
	 * the corresponding array value.
382
	 * @param array $data name-value pairs of new data to be inserted.
383
	 * @return TDbCommand insert command
384
	 */
385 8
	public function createInsertCommand($data)
386
	{
387 8
		$table = $this->getTableInfo()->getTableFullName();
388 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

388
		[$fields, $bindings] = $this->getInsertFieldBindings(/** @scrutinizer ignore-type */ $data);
Loading history...
389 8
		$command = $this->createCommand("INSERT INTO {$table}({$fields}) VALUES ($bindings)");
390 8
		$this->bindColumnValues($command, $data);
391 8
		return $command;
392
	}
393
394
	/**
395
	 * Creates an update command for the table described in {@link setTableInfo TableInfo} for the given data.
396
	 * Each array key in the $data array must correspond to the column name to be updated with the corresponding array value.
397
	 * @param array $data name-value pairs of data to be updated.
398
	 * @param string $where update condition.
399
	 * @param array $parameters update parameters.
400
	 * @return TDbCommand update command.
401
	 */
402 3
	public function createUpdateCommand($data, $where, $parameters = [])
403
	{
404 3
		$table = $this->getTableInfo()->getTableFullName();
405 3
		if ($this->hasIntegerKey($parameters)) {
406
			$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

406
			$fields = implode(', ', /** @scrutinizer ignore-type */ $this->getColumnBindings($data, true));
Loading history...
407
		} else {
408 3
			$fields = implode(', ', $this->getColumnBindings($data));
409
		}
410
411 3
		if (!empty($where)) {
412 3
			$where = ' WHERE ' . $where;
413
		}
414 3
		$command = $this->createCommand("UPDATE {$table} SET {$fields}" . $where);
415 3
		$this->bindArrayValues($command, array_merge($data, $parameters));
416 3
		return $command;
417
	}
418
419
	/**
420
	 * Returns a list of insert field name and a list of binding names.
421
	 * @param object $values array or object to be inserted.
422
	 * @return array tuple ($fields, $bindings)
423
	 */
424 8
	protected function getInsertFieldBindings($values)
425
	{
426 8
		$fields = [];
427 8
		$bindings = [];
428 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 $input 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

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