Completed
Push — master ( a7bf2b...7b6cb0 )
by Chris
02:36
created

AbstractSqlTranslator   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 435
Duplicated Lines 6.21 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 19
Bugs 7 Features 3
Metric Value
wmc 51
c 19
b 7
f 3
lcom 1
cbo 2
dl 27
loc 435
rs 8.3206

23 Methods

Rating   Name   Duplication   Size   Complexity  
A concatenate() 0 7 1
A limitIsUseful() 0 3 2
A translate() 0 13 2
A translateCreate() 0 6 1
A translateRead() 0 12 1
A translateUpdate() 9 9 1
A translateDelete() 9 9 1
identifier() 0 1 ?
A value() 0 7 2
A resolve() 0 11 4
A resolvesPlaceholder() 0 3 1
A prepareColumns() 0 9 2
C prepareOperator() 9 25 8
A prepareFilter() 0 13 2
B prepareWhere() 0 20 5
A prepareOrder() 0 6 3
A prepareOrderBy() 0 13 4
prepareSelect() 0 1 ?
A prepareInsert() 0 11 1
prepareUpdate() 0 1 ?
prepareDelete() 0 1 ?
C filterParameters() 0 23 7
A parameters() 0 13 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractSqlTranslator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractSqlTranslator, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Darya\Database\Query;
3
4
use Exception;
5
use Darya\Database;
6
use Darya\Database\Query\Translator;
7
use Darya\Storage;
8
9
/**
10
 * An abstract query translator that prepares SQL common across more than one
11
 * RDBMS.
12
 * 
13
 * @author Chris Andrew <[email protected]>
14
 */
15
abstract class AbstractSqlTranslator implements Translator {
16
	
17
	/**
18
	 * @var array Filter comparison operators
19
	 */
20
	protected $operators = array('>=', '<=', '>', '<', '=', '!=', '<>', 'in', 'not in', 'is', 'is not', 'like', 'not like');
21
	
22
	/**
23
	 * Concatenates the given set of strings that aren't empty.
24
	 * 
25
	 * Runs implode() after filtering out empty elements.
26
	 * 
27
	 * Delimiter defaults to a single whitespace character.
28
	 * 
29
	 * @param array  $strings
30
	 * @param string $delimiter [optional]
31
	 * @return string
32
	 */
33
	protected static function concatenate($strings, $delimiter = ' ') {
34
		$strings = array_filter($strings, function($value) {
35
			return !empty($value);
36
		});
37
		
38
		return implode($delimiter, $strings);
39
	}
40
	
41
	/**
42
	 * Determine whether the given limit and offset will make a difference to
43
	 * a statement.
44
	 * 
45
	 * Simply determines whether both are non-zero integers.
46
	 * 
47
	 * @param int $limit
48
	 * @param int $offset
49
	 * @return bool
50
	 */
51
	protected static function limitIsUseful($limit, $offset) {
52
		return (int) $limit !== 0 || (int) $offset !== 0;
53
	}
54
	
55
	/**
56
	 * Translate the given storage query into an SQL query.
57
	 * 
58
	 * @param Storage\Query $storageQuery
59
	 * @return Database\Query
60
	 * @throws Exception
61
	 */
62
	public function translate(Storage\Query $storageQuery) {
63
		$type = $storageQuery->type;
64
		
65
		$method = 'translate' . ucfirst($type);
66
		
67
		if (!method_exists($this, $method)) {
68
			throw new Exception("Could not translate query of unsupported type '$type'");
69
		}
70
		
71
		$query = call_user_func_array(array($this, $method), array($storageQuery));
72
		
73
		return $query;
74
	}
75
	
76
	/**
77
	 * Translate a query that creates a record.
78
	 * 
79
	 * @param Storage\Query $storageQuery
80
	 * @return Database\Query
81
	 */
82
	protected function translateCreate(Storage\Query $storageQuery) {
83
		return new Database\Query(
84
			$this->prepareInsert($storageQuery->resource, $storageQuery->data),
85
			$this->parameters($storageQuery)
86
		);
87
	}
88
	
89
	/**
90
	 * Translate a query that reads records.
91
	 * 
92
	 * @param Storage\Query $storageQuery
93
	 * @return Database\Query
94
	 */
95
	protected function translateRead(Storage\Query $storageQuery) {
96
		return new Database\Query(
97
			$this->prepareSelect($storageQuery->resource,
98
				$this->prepareColumns($storageQuery->fields),
99
				$this->prepareWhere($storageQuery->filter),
100
				$this->prepareOrderBy($storageQuery->order),
101
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset),
102
				$storageQuery->distinct
103
			),
104
			$this->parameters($storageQuery)
105
		);
106
	}
107
	
108
	/**
109
	 * Translate a query that updates records.
110
	 * 
111
	 * @param Storage\Query $storageQuery
112
	 * @return Database\Query
113
	 */
114 View Code Duplication
	protected function translateUpdate(Storage\Query $storageQuery) {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
115
		return new Database\Query(
116
			$this->prepareUpdate($storageQuery->resource, $storageQuery->data,
117
				$this->prepareWhere($storageQuery->filter),
118
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
119
			),
120
			$this->parameters($storageQuery)
121
		);
122
	}
123
	
124
	/**
125
	 * Translate a query that deletes records.
126
	 * 
127
	 * @param Storage\Query $storageQuery
128
	 * @return Database\Query
129
	 */
130 View Code Duplication
	protected function translateDelete(Storage\Query $storageQuery) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
131
		return new Database\Query(
132
			$this->prepareDelete($storageQuery->resource,
133
				$this->prepareWhere($storageQuery->filter),
134
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
135
			),
136
			$this->parameters($storageQuery)
137
		);
138
	}
139
	
140
	/**
141
	 * Escape the given identifier.
142
	 * 
143
	 * If the value is an array, it is recursively escaped.
144
	 * 
145
	 * If the value is not a string, it is returned unmodified.
146
	 * 
147
	 * @param mixed $identifier
148
	 * @return mixed
149
	 */
150
	abstract protected function identifier($identifier);
151
	
152
	/**
153
	 * Prepare the given value.
154
	 * 
155
	 * If the value is an array, it is recursively prepared.
156
	 * 
157
	 * @param array|string $value
158
	 * @return array|string
159
	 */
160
	protected function value($value) {
161
		if (is_array($value)) {
162
			return array_map(array($this, 'value'), $value);
163
		}
164
		
165
		return $this->resolve($value);
166
	}
167
	
168
	/**
169
	 * Resolve a placeholder or constant for the given parameter value.
170
	 * 
171
	 * @param mixed $value
172
	 * @return string
173
	 */
174
	protected function resolve($value) {
175
		if ($value === null) {
176
			return 'NULL';
177
		}
178
		
179
		if (is_bool($value)) {
180
			return $value ? 'TRUE' : 'FALSE';
181
		}
182
		
183
		return '?';
184
	}
185
	
186
	/**
187
	 * Determine whether the given value resolves a placeholder.
188
	 * 
189
	 * @param mixed $value
190
	 * @return bool
191
	 */
192
	protected function resolvesPlaceholder($value) {
193
		return $this->resolve($value) === '?';
194
	}
195
	
196
	/**
197
	 * Prepare the given columns as a string.
198
	 * 
199
	 * @param array|string $columns
200
	 * @return string
201
	 */
202
	protected function prepareColumns($columns) {
203
		if (empty($columns)) {
204
			return '*';
205
		}
206
		
207
		$columns = (array) $this->identifier($columns);
208
		
209
		return implode(', ', $columns);
210
	}
211
	
212
	/**
213
	 * Prepare a default operator for the given value.
214
	 * 
215
	 * @param string $operator
216
	 * @param mixed  $value
217
	 * @return string
218
	 */
219
	protected function prepareOperator($operator, $value) {
220
		$operator = in_array(strtolower($operator), $this->operators) ? strtoupper($operator) : '=';
221
		
222 View Code Duplication
		if (!$this->resolvesPlaceholder($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223
			if ($operator === '=') {
224
				$operator = 'IS';
225
			}
226
			
227
			if ($operator === '!=') {
228
				$operator = 'IS NOT';
229
			}
230
		}
231
		
232
		if (is_array($value)) {
233
			if ($operator === '=') {
234
				$operator = 'IN';
235
			}
236
		
237
			if ($operator === '!=') {
238
				$operator = 'NOT IN';
239
			}
240
		}
241
		
242
		return $operator;
243
	}
244
	
245
	/**
246
	 * Prepare an individual filter condition.
247
	 * 
248
	 * @param string $column
249
	 * @param mixed  $value
250
	 * @return string
251
	 */
252
	protected function prepareFilter($column, $value) {
253
		list($column, $operator) = array_pad(explode(' ', $column, 2), 2, null);
254
		
255
		$column = $this->prepareColumns($column);
256
		$operator = $this->prepareOperator($operator, $value);
257
		$value = $this->value($value);
258
		
259
		if (is_array($value)) {
260
			$value = "(" . implode(", ", $value) . ")";
261
		}
262
		
263
		return "$column $operator $value";
264
	}
265
	
266
	/**
267
	 * Prepare a WHERE clause using the given filter and comparison operator.
268
	 * 
269
	 * Example filter key-values and their SQL equivalents:
270
	 *     'id'        => 1,       // id = '1'
271
	 *     'name like' => 'Chris', // name LIKE 'Chris'
272
	 *     'count >'   => 10,      // count > '10'
273
	 *     'type in'   => [1, 2],  // type IN (1, 2)
274
	 *     'type'      => [3, 4]   // type IN (3, 4)
275
	 * 
276
	 * Comparison operator between conditions defaults to 'AND'.
277
	 * 
278
	 * @param array  $filter
279
	 * @param string $comparison   [optional]
280
	 * @param bool   $excludeWhere [optional]
281
	 * @return string
282
	 */
283
	protected function prepareWhere(array $filter, $comparison = 'AND', $excludeWhere = false) {
284
		$conditions = array();
285
		
286
		foreach ($filter as $column => $value) {
287
			if (strtolower($column) == 'or') {
288
				$conditions[] = '(' . $this->prepareWhere($value, 'OR', true) . ')';
289
			} else {
290
				$conditions[] = $this->prepareFilter($column, $value);
291
			}
292
		}
293
		
294
		if (!count($conditions)) {
295
			return null;
296
		}
297
		
298
		$where = $excludeWhere ? '' : 'WHERE ';
299
		$where .= implode(" $comparison ", $conditions);
300
		
301
		return $where;
302
	}
303
	
304
	/**
305
	 * Prepare an individual order condition.
306
	 * 
307
	 * @param string $column
308
	 * @param string $direction [optional]
309
	 * @return string
310
	 */
311
	protected function prepareOrder($column, $direction = null) {
312
		$column = $this->identifier($column);
313
		$direction = $direction !== null ? strtoupper($direction) : 'ASC';
314
		
315
		return !empty($column) ? "$column $direction" : null;
316
	}
317
	
318
	/**
319
	 * Prepare an ORDER BY clause using the given order.
320
	 * 
321
	 * Example order key-values:
322
	 *     'column',
323
	 *     'other_column'   => 'ASC',
324
	 *     'another_column' => 'DESC
325
	 * 
326
	 * Ordered ascending by default.
327
	 * 
328
	 * @param array|string $order
329
	 * @return string
330
	 */
331
	protected function prepareOrderBy($order) {
332
		$conditions = array();
333
		
334
		foreach ((array) $order as $key => $value) {
335
			if (is_numeric($key)) {
336
				$conditions[] = $this->prepareOrder($value);
337
			} else {
338
				$conditions[] = $this->prepareOrder($key, $value);
339
			}
340
		}
341
		
342
		return count($conditions) ? 'ORDER BY ' . implode(', ', $conditions) : null;
343
	}
344
	
345
	/**
346
	 * Prepare a SELECT statement using the given columns, table, clauses and
347
	 * options.
348
	 * 
349
	 * @param string       $table
350
	 * @param array|string $columns
351
	 * @param string       $where    [optional]
352
	 * @param string       $order    [optional]
353
	 * @param string       $limit    [optional]
354
	 * @param bool         $distinct [optional]
355
	 * @return string
356
	 */
357
	abstract protected function prepareSelect($table, $columns, $where = null, $order = null, $limit = null, $distinct = false);
358
	
359
	/**
360
	 * Prepare an INSERT INTO statement using the given table and data.
361
	 * 
362
	 * @param string $table
363
	 * @param array  $data
364
	 * @return string
365
	 */
366
	protected function prepareInsert($table, array $data) {
367
		$table = $this->identifier($table);
368
		
369
		$columns = $this->identifier(array_keys($data));
370
		$values  = $this->value(array_values($data));
371
		
372
		$columns = "(" . implode(", ", $columns) . ")";
373
		$values  = "(" . implode(", ", $values) . ")";
374
		
375
		return static::concatenate(array('INSERT INTO', $table, $columns, 'VALUES', $values));
376
	}
377
	
378
	/**
379
	 * Prepare an UPDATE statement with the given table, data and clauses.
380
	 * 
381
	 * @param string $table
382
	 * @param array  $data
383
	 * @param string $where [optional]
384
	 * @param string $limit [optional]
385
	 * @return string
386
	 */
387
	abstract protected function prepareUpdate($table, $data, $where = null, $limit = null);
388
	
389
	/**
390
	 * Prepare a DELETE statement with the given table and clauses.
391
	 * 
392
	 * @param string $table
393
	 * @param string $where [optional]
394
	 * @param string $limit [optional]
395
	 * @return string
396
	 */
397
	abstract protected function prepareDelete($table, $where = null, $limit = null);
398
	
399
	/**
400
	 * Prepare the given filter as an array of prepared query parameters.
401
	 * 
402
	 * @return array
403
	 */
404
	protected function filterParameters($filter) {
405
		$parameters = array();
406
		
407
		foreach ($filter as $index => $value) {
408
			if (is_array($value)) {
409
				if (strtolower($index) === 'or') {
410
					$parameters = array_merge($parameters, $this->filterParameters($value));
411
				} else {
412
					foreach ($value as $in) {
413
						if ($this->resolvesPlaceholder($value)) {
414
							$parameters[] = $in;
415
						}
416
					}
417
				}
418
			} else {
419
				if ($this->resolvesPlaceholder($value)) {
420
					$parameters[] = $value;
421
				}
422
			}
423
		}
424
		
425
		return $parameters;
426
	}
427
	
428
	/**
429
	 * Retrieve an array of parameters from the given query for executing a
430
	 * prepared query.
431
	 * 
432
	 * @param Storage\Query $query
433
	 * @return array
434
	 */
435
	public function parameters(Storage\Query $query) {
436
		$parameters = array();
437
		
438
		foreach ($query->data as $value) {
439
			if ($this->resolvesPlaceholder($value)) {
440
				$parameters[] = $value;
441
			}
442
		}
443
		
444
		$parameters = array_merge($parameters, $this->filterParameters($query->filter));
445
		
446
		return $parameters;
447
	}
448
	
449
}
450