Completed
Push — master ( 20ce86...00401a )
by Chris
02:49
created

AbstractSqlTranslator::identifier()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 11
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 11
loc 11
c 1
b 0
f 1
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
1
<?php
2
namespace Darya\Database\Query;
3
4
use Exception;
5
use Darya\Database;
6
use Darya\Database\Query\Translator;
7
use Darya\Database\Storage\Query\Join;
8
use Darya\Storage;
9
10
/**
11
 * An abstract query translator that prepares SQL common across more than one
12
 * RDBMS.
13
 * 
14
 * @author Chris Andrew <[email protected]>
15
 */
16
abstract class AbstractSqlTranslator implements Translator {
17
	
18
	/**
19
	 * Filter comparison operators.
20
	 * 
21
	 * @var array
22
	 */
23
	protected $operators = array('>=', '<=', '>', '<', '=', '!=', '<>', 'in', 'not in', 'is', 'is not', 'like', 'not like');
24
	
25
	/**
26
	 * Placeholder for values in prepared queries.
27
	 * 
28
	 * @var string
29
	 */
30
	protected $placeholder = '?';
31
	
32
	/**
33
	 * Concatenates the given set of strings that aren't empty.
34
	 * 
35
	 * Runs implode() after filtering out empty elements.
36
	 * 
37
	 * Delimiter defaults to a single whitespace character.
38
	 * 
39
	 * @param array  $strings
40
	 * @param string $delimiter [optional]
41
	 * @return string
42
	 */
43
	protected static function concatenate($strings, $delimiter = ' ') {
44
		$strings = array_filter($strings, function($value) {
45
			return !empty($value);
46
		});
47
		
48
		return implode($delimiter, $strings);
49
	}
50
	
51
	/**
52
	 * Determine whether the given limit and offset will make a difference to
53
	 * a statement.
54
	 * 
55
	 * Simply determines whether both are non-zero integers.
56
	 * 
57
	 * @param int $limit
58
	 * @param int $offset
59
	 * @return bool
60
	 */
61
	protected static function limitIsUseful($limit, $offset) {
62
		return (int) $limit !== 0 || (int) $offset !== 0;
63
	}
64
	
65
	/**
66
	 * Translate the given storage query into an SQL query.
67
	 * 
68
	 * @param Storage\Query $storageQuery
69
	 * @return Database\Query
70
	 * @throws Exception
71
	 */
72
	public function translate(Storage\Query $storageQuery) {
73
		$type = $storageQuery->type;
74
		
75
		$method = 'translate' . ucfirst($type);
76
		
77
		if (!method_exists($this, $method)) {
78
			throw new Exception("Could not translate query of unknown type '$type'");
79
		}
80
		
81
		$query = call_user_func_array(array($this, $method), array($storageQuery));
82
		
83
		return $query;
84
	}
85
	
86
	/**
87
	 * Translate a query that creates a record.
88
	 * 
89
	 * @param Storage\Query $storageQuery
90
	 * @return Database\Query
91
	 */
92
	protected function translateCreate(Storage\Query $storageQuery) {
93
		if ($storageQuery instanceof Database\Storage\Query && $storageQuery->insertSubquery) {
94
			return new Database\Query(
95
				$this->prepareInsertSelect($storageQuery->resource, $storageQuery->fields, $storageQuery->insertSubquery),
0 ignored issues
show
Documentation introduced by
The property $resource is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property $fields is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
96
				$this->parameters($storageQuery->insertSubquery)
97
			);
98
		}
99
		
100
		return new Database\Query(
101
			$this->prepareInsert($storageQuery->resource, $storageQuery->data),
102
			$this->parameters($storageQuery)
103
		);
104
	}
105
	
106
	/**
107
	 * Translate a query that reads records.
108
	 * 
109
	 * @param Storage\Query $storageQuery
110
	 * @return Database\Query
111
	 */
112
	protected function translateRead(Storage\Query $storageQuery) {
113
		if ($storageQuery instanceof Database\Storage\Query) {
114
			return $this->translateDatabaseRead($storageQuery);
115
		}
116
		
117
		return new Database\Query(
118
			$this->prepareSelect($storageQuery->resource,
119
				$this->prepareColumns($storageQuery->fields),
120
				null,
121
				$this->prepareWhere($storageQuery->filter),
122
				$this->prepareOrderBy($storageQuery->order),
123
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset),
124
				$storageQuery->distinct
125
			),
126
			$this->parameters($storageQuery)
127
		);
128
	}
129
	
130
	/**
131
	 * Translate a database storage query that reads records.
132
	 * 
133
	 * @param Database\Storage\Query $storageQuery
134
	 */
135
	protected function translateDatabaseRead(Database\Storage\Query $storageQuery) {
136
				return new Database\Query(
137
			$this->prepareSelect($storageQuery->resource,
0 ignored issues
show
Documentation introduced by
The property $resource is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
138
				$this->prepareColumns($storageQuery->fields),
0 ignored issues
show
Documentation introduced by
The property $fields is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
139
				$this->prepareJoins($storageQuery->joins),
140
				$this->prepareWhere($storageQuery->filter),
0 ignored issues
show
Documentation introduced by
The property $filter is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
141
				$this->prepareOrderBy($storageQuery->order),
0 ignored issues
show
Documentation introduced by
The property $order is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
142
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset),
0 ignored issues
show
Documentation introduced by
The property $limit is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property $offset is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
143
				$storageQuery->distinct
0 ignored issues
show
Documentation introduced by
The property $distinct is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
144
			),
145
			$this->parameters($storageQuery)
146
		);
147
	}
148
	
149
	/**
150
	 * Translate a query that updates records.
151
	 * 
152
	 * @param Storage\Query $storageQuery
153
	 * @return Database\Query
154
	 */
155
	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...
156
		return new Database\Query(
157
			$this->prepareUpdate($storageQuery->resource, $storageQuery->data,
158
				$this->prepareWhere($storageQuery->filter),
159
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
160
			),
161
			$this->parameters($storageQuery)
162
		);
163
	}
164
	
165
	/**
166
	 * Translate a query that deletes records.
167
	 * 
168
	 * @param Storage\Query $storageQuery
169
	 * @return Database\Query
170
	 */
171
	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...
172
		return new Database\Query(
173
			$this->prepareDelete($storageQuery->resource,
174
				$this->prepareWhere($storageQuery->filter),
175
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
176
			),
177
			$this->parameters($storageQuery)
178
		);
179
	}
180
	
181
	/**
182
	 * Resolve the given value as an identifier.
183
	 * 
184
	 * @param mixed $identifier
185
	 * @return string
186
	 */
187
	abstract protected function resolveIdentifier($identifier);
188
	
189
	/**
190
	 * Prepare the given identifier.
191
	 * 
192
	 * If the value is translatable, it is translated.
193
	 * 
194
	 * If the value is an array, it is recursively prepared.
195
	 * 
196
	 * @param mixed $identifier
197
	 * @return mixed
198
	 */
199 View Code Duplication
	protected function identifier($identifier) {
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...
200
		if (is_array($identifier)) {
201
			return array_map(array($this, 'identifier'), $identifier);
202
		}
203
		
204
		if ($this->translatable($identifier)) {
205
			return $this->translateValue($identifier);
206
		}
207
		
208
		return $this->resolveIdentifier($identifier);
209
	}
210
	
211
	/**
212
	 * Determine whether the given value is translatable.
213
	 * 
214
	 * @param mixed $value
215
	 * @return bool
216
	 */
217
	protected function translatable($value) {
218
		return $value instanceof Storage\Query\Builder || $value instanceof Storage\Query;
219
	}
220
	
221
	/**
222
	 * Translate the given value if it is a query or query builder.
223
	 * 
224
	 * Returns the argument as is otherwise.
225
	 * 
226
	 * @param mixed $value
227
	 * @return mixed
228
	 */
229 View Code Duplication
	protected function translateValue($value) {
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...
230
		if ($value instanceof Storage\Query\Builder) {
231
			$value = $value->query;
232
		}
233
		
234
		if ($value instanceof Storage\Query) {
235
			$query = $this->translate($value);
236
			
237
			$value = "($query)";
238
		}
239
		
240
		return $value;
241
	}
242
	
243
	/**
244
	 * Prepare the given value for a prepared query.
245
	 * 
246
	 * If the value translatable, it is translated.
247
	 * 
248
	 * If the value is an array, it is recursively prepared.
249
	 * 
250
	 * @param array|string $value
251
	 * @return array|string
252
	 */
253 View Code Duplication
	protected function value($value) {
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...
254
		if ($this->translatable($value)) {
255
			return $this->translateValue($value);
256
		}
257
		
258
		if (is_array($value)) {
259
			return array_map(array($this, 'value'), $value);
260
		}
261
		
262
		return $this->resolveValue($value);
263
	}
264
	
265
	/**
266
	 * Resolve a placeholder or constant for the given parameter value.
267
	 * 
268
	 * @param mixed $value
269
	 * @return string
270
	 */
271
	protected function resolveValue($value) {
272
		if ($value === null) {
273
			return 'NULL';
274
		}
275
		
276
		if (is_bool($value)) {
277
			return $value ? 'TRUE' : 'FALSE';
278
		}
279
		
280
		return $this->placeholder;
281
	}
282
	
283
	/**
284
	 * Determine whether the given value resolves a placeholder.
285
	 * 
286
	 * @param mixed $value
287
	 * @return bool
288
	 */
289
	protected function resolvesPlaceholder($value) {
290
		return $this->resolveValue($value) === $this->placeholder;
291
	}
292
	
293
	/**
294
	 * Prepare a set of column aliases.
295
	 * 
296
	 * Uses the keys of the given array as identifiers and appends them to their
297
	 * values.
298
	 * 
299
	 * @param array $columns
300
	 * @return array
301
	 */
302
	protected function prepareColumnAliases(array $columns) {
303
		foreach ($columns as $alias => &$column) {
304
			if (is_string($alias) && preg_match('/^[\w]/', $alias)) {
305
				$aliasIdentifier = $this->identifier($alias);
306
				$column = "$column $aliasIdentifier";
307
			}
308
		}
309
		
310
		return $columns;
311
	}
312
	
313
	/**
314
	 * Prepare the given columns as a string.
315
	 * 
316
	 * @param array|string $columns
317
	 * @return string
318
	 */
319
	protected function prepareColumns($columns) {
320
		if (empty($columns)) {
321
			return '*';
322
		}
323
		
324
		$columns = (array) $this->identifier($columns);
325
		
326
		$columns = $this->prepareColumnAliases($columns);
327
		
328
		return implode(', ', $columns);
329
	}
330
	
331
	/**
332
	 * Determine whether the given operator is valid.
333
	 * 
334
	 * @param string $operator
335
	 * @return bool
336
	 */
337
	protected function validOperator($operator) {
338
		$operator = trim($operator);
339
		
340
		return in_array(strtolower($operator), $this->operators);
341
	}
342
	
343
	/**
344
	 * Prepare the given conditional operator.
345
	 * 
346
	 * Returns the equals operator if given value is not in the set of valid
347
	 * operators.
348
	 * 
349
	 * @param string $operator
350
	 * @return string
351
	 */
352
	protected function prepareRawOperator($operator) {
353
		$operator = trim($operator);
354
		
355
		return $this->validOperator($operator) ? strtoupper($operator) : '=';
356
	}
357
	
358
	/**
359
	 * Prepare an appropriate conditional operator for the given value.
360
	 * 
361
	 * @param string $operator
362
	 * @param mixed  $value    [optional]
363
	 * @return string
364
	 */
365
	protected function prepareOperator($operator, $value = null) {
366
		$operator = $this->prepareRawOperator($operator);
367
		
368 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...
369
			if ($operator === '=') {
370
				$operator = 'IS';
371
			}
372
			
373
			if ($operator === '!=') {
374
				$operator = 'IS NOT';
375
			}
376
		}
377
		
378
		if (is_array($value)) {
379
			if ($operator === '=') {
380
				$operator = 'IN';
381
			}
382
		
383
			if ($operator === '!=') {
384
				$operator = 'NOT IN';
385
			}
386
		}
387
		
388
		return $operator;
389
	}
390
	
391
	/**
392
	 * Prepare a join type.
393
	 * 
394
	 * @param string $type
395
	 * @return string
396
	 */
397
	protected function prepareJoinType($type) {
398
		if (in_array($type, array('left', 'right'))) {
399
			return strtoupper($type) . ' JOIN';
400
		}
401
		
402
		return 'JOIN';
403
	}
404
	
405
	/**
406
	 * Prepare a join table.
407
	 * 
408
	 * @param Join $join
409
	 * @return string
410
	 */
411
	protected function prepareJoinTable(Join $join) {
412
		$table = $this->identifier($join->resource);
413
		$alias = $this->identifier($join->alias);
414
		
415
		return $alias ? "$table $alias" : $table;
416
	}
417
	
418
	/**
419
	 * Prepare a single join condition.
420
	 * 
421
	 * TODO: Make this generic for WHERE or JOIN clauses. prepareCondition()?
422
	 * 
423
	 * @param string $condition
424
	 * @return string
425
	 */
426
	protected function prepareJoinCondition($condition) {
427
		$parts = preg_split('/\s+/', $condition, 3);
428
		
429
		if (count($parts) < 3) {
430
			return null;
431
		}
432
		
433
		list($first, $operator, $second) = $parts;
434
		
435
		return static::concatenate(array(
436
			$this->identifier($first),
437
			$this->prepareRawOperator($operator),
438
			$this->identifier($second)
439
		));
440
	}
441
	
442
	/**
443
	 * Prepare a join's conditions.
444
	 * 
445
	 * @param Join $join
446
	 * @return string
447
	 */
448
	protected function prepareJoinConditions(Join $join) {
449
		$result = null;
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
450
		
451
		$conditions = array();
452
		
453
		foreach ($join->conditions as $condition) {
454
			$conditions[] = $this->prepareJoinCondition($condition);
455
		}
456
		
457
		$conditions = array_merge($conditions, $this->prepareFilter($join->filter));
0 ignored issues
show
Documentation introduced by
The property $filter is declared protected in Darya\Database\Storage\Query\Join. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
458
		
459
		return static::concatenate($conditions, ' AND ');
460
	}
461
	
462
	/**
463
	 * Prepare an individual table join.
464
	 * 
465
	 * @param Join $join
466
	 * @return string
467
	 */
468
	protected function prepareJoin(Join $join) {
469
		$table = $this->prepareJoinTable($join);
470
		$conditions = $this->prepareJoinConditions($join);
471
		
472
		$clause = $table && $conditions ? "$table ON $conditions" : $table;
473
		
474
		if (empty($clause)) {
475
			return null;
476
		}
477
		
478
		$type = $this->prepareJoinType($join->type);
479
		
480
		return "$type $clause";
481
	}
482
	
483
	/**
484
	 * Prepare table joins.
485
	 * 
486
	 * @param array $joins
487
	 * @return string
488
	 */
489
	protected function prepareJoins(array $joins) {
490
		$clauses = array();
491
		
492
		foreach ($joins as $join) {
493
			$clauses[] = $this->prepareJoin($join);
494
		}
495
		
496
		return static::concatenate($clauses);
497
	}
498
	
499
	/**
500
	 * Prepare an individual filter condition.
501
	 * 
502
	 * @param string $column
503
	 * @param mixed  $given
504
	 * @return string
505
	 */
506
	protected function prepareFilterCondition($column, $given) {
507
		list($left, $right) = array_pad(preg_split('/\s+/', $column, 2), 2, null);
508
		
509
		$column = $this->prepareColumns($left);
510
		
511
		$operator = $this->prepareOperator($right, $given);
512
		$value    = $this->value($given);
513
		
514
		// If the given value is null and whatever's on the right isn't a valid
515
		// operator we can attempt to split again and find a second identifier
516
		if ($given === null && !empty($right) && !$this->validOperator($right)) {
517
			list($operator, $identifier) = array_pad(preg_split('/\s+([\w\.]+)$/', $right, 2, PREG_SPLIT_DELIM_CAPTURE), 2, null);
518
			
519
			if (!empty($identifier)) {
520
				$operator = $this->prepareRawOperator($operator);
521
				$value    = $this->identifier($identifier);
522
			}
523
		}
524
		
525
		if (is_array($value)) {
526
			$value = "(" . implode(", ", $value) . ")";
527
		}
528
		
529
		return "$column $operator $value";
530
	}
531
	
532
	/**
533
	 * Prepare a filter as a set of query conditions.
534
	 * 
535
	 * TODO: Could numeric keys be dealt with by prepareJoinCondition()?
536
	 * 
537
	 * @param array $filter
538
	 * @return array
539
	 */
540
	protected function prepareFilter(array $filter) {
541
		$conditions = array();
542
		
543
		foreach ($filter as $column => $value) {
544
			if (strtolower($column) == 'or') {
545
				$conditions[] = '(' . $this->prepareWhere($value, 'OR', true) . ')';
546
			} else {
547
				$conditions[] = $this->prepareFilterCondition($column, $value);
548
			}
549
		}
550
		
551
		return $conditions;
552
	}
553
	
554
	/**
555
	 * Prepare a WHERE clause using the given filter and comparison operator.
556
	 * 
557
	 * Example filter key-values and their SQL equivalents:
558
	 *     'id'        => 1,       // id = '1'
559
	 *     'name like' => 'Chris', // name LIKE 'Chris'
560
	 *     'count >'   => 10,      // count > '10'
561
	 *     'type in'   => [1, 2],  // type IN (1, 2)
562
	 *     'type'      => [3, 4]   // type IN (3, 4)
563
	 * 
564
	 * Comparison operator between conditions defaults to 'AND'.
565
	 * 
566
	 * @param array  $filter
567
	 * @param string $comparison   [optional]
568
	 * @param bool   $excludeWhere [optional]
569
	 * @return string
570
	 */
571
	protected function prepareWhere(array $filter, $comparison = 'AND', $excludeWhere = false) {
572
		$conditions = $this->prepareFilter($filter);
573
		
574
		if (empty($conditions)) {
575
			return null;
576
		}
577
		
578
		$clause = implode(" $comparison ", $conditions);
579
		
580
		return !$excludeWhere ? "WHERE $clause" : $clause;
581
	}
582
	
583
	/**
584
	 * Prepare an individual order condition.
585
	 * 
586
	 * @param string $column
587
	 * @param string $direction [optional]
588
	 * @return string
589
	 */
590
	protected function prepareOrder($column, $direction = null) {
591
		$column = $this->identifier($column);
592
		$direction = $direction !== null ? strtoupper($direction) : 'ASC';
593
		
594
		return !empty($column) ? "$column $direction" : null;
595
	}
596
	
597
	/**
598
	 * Prepare an ORDER BY clause using the given order.
599
	 * 
600
	 * Example order key-values:
601
	 *     'column',
602
	 *     'other_column'   => 'ASC',
603
	 *     'another_column' => 'DESC
604
	 * 
605
	 * Ordered ascending by default.
606
	 * 
607
	 * @param array|string $order
608
	 * @return string
609
	 */
610
	protected function prepareOrderBy($order) {
611
		$conditions = array();
612
		
613
		foreach ((array) $order as $key => $value) {
614
			if (is_numeric($key)) {
615
				$conditions[] = $this->prepareOrder($value);
616
			} else {
617
				$conditions[] = $this->prepareOrder($key, $value);
618
			}
619
		}
620
		
621
		return count($conditions) ? 'ORDER BY ' . implode(', ', $conditions) : null;
622
	}
623
	
624
	/**
625
	 * Prepare a LIMIT clause using the given limit and offset.
626
	 * 
627
	 * @param int $limit  [optional]
628
	 * @param int $offset [optional]
629
	 * @return string
630
	 */
631
	abstract protected function prepareLimit($limit = 0, $offset = 0);
632
	
633
	/**
634
	 * Prepare a SELECT statement using the given columns, table, clauses and
635
	 * options.
636
	 * 
637
	 * TODO: Simplify this so that prepareSelect only actually prepares the
638
	 *       SELECT and FROM clauses. The rest could be concatenated by
639
	 *       translateRead().
640
	 * 
641
	 * @param string       $table
642
	 * @param array|string $columns
643
	 * @param string       $joins    [optional]
644
	 * @param string       $where    [optional]
645
	 * @param string       $order    [optional]
646
	 * @param string       $limit    [optional]
647
	 * @param bool         $distinct [optional]
648
	 * @return string
649
	 */
650
	abstract protected function prepareSelect($table, $columns, $joins = null, $where = null, $order = null, $limit = null, $distinct = false);
651
	
652
	/**
653
	 * Prepare an INSERT INTO statement using the given table and data.
654
	 * 
655
	 * @param string $table
656
	 * @param array  $data
657
	 * @return string
658
	 */
659
	protected function prepareInsert($table, array $data) {
660
		$table = $this->identifier($table);
661
		
662
		$columns = $this->identifier(array_keys($data));
663
		$values  = $this->value(array_values($data));
664
		
665
		$columns = "(" . implode(", ", $columns) . ")";
666
		$values  = "(" . implode(", ", $values) . ")";
667
		
668
		return static::concatenate(array('INSERT INTO', $table, $columns, 'VALUES', $values));
669
	}
670
	
671
	/**
672
	 * Prepare an INSERT SELECT statement using the given table and
673
	 * subquery.
674
	 * 
675
	 * @param string        $table
676
	 * @param array         $columns
677
	 * @param Storage\Query $subquery
678
	 * @return string
679
	 */
680
	public function prepareInsertSelect($table, array $columns, Storage\Query $subquery) {
681
		$table = $this->identifier($table);
682
		
683
		$columns = $this->identifier($columns);
684
		$columns = "(" . implode(", ", $columns) . ")";
685
		
686
		$subquery = (string) $this->translate($subquery);
687
		
688
		return static::concatenate(array('INSERT INTO', $table, $columns, $subquery));
689
	}
690
	
691
	/**
692
	 * Prepare an UPDATE statement with the given table, data and clauses.
693
	 * 
694
	 * @param string $table
695
	 * @param array  $data
696
	 * @param string $where [optional]
697
	 * @param string $limit [optional]
698
	 * @return string
699
	 */
700
	abstract protected function prepareUpdate($table, $data, $where = null, $limit = null);
701
	
702
	/**
703
	 * Prepare a DELETE statement with the given table and clauses.
704
	 * 
705
	 * @param string $table
706
	 * @param string $where [optional]
707
	 * @param string $limit [optional]
708
	 * @return string
709
	 */
710
	abstract protected function prepareDelete($table, $where = null, $limit = null);
711
	
712
	/**
713
	 * Prepare a set of query parameters from the given set of columns.
714
	 * 
715
	 * @param array $columns
716
	 * @return array
717
	 */
718 View Code Duplication
	protected function columnParameters($columns) {
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...
719
		$parameters = array();
720
		
721
		foreach ($columns as $column) {
722
			if ($column instanceof Storage\Query\Builder) {
723
				$column = $column->query;
724
			}
725
			
726
			if ($column instanceof Storage\Query) {
727
				$parameters = array_merge($parameters, $this->parameters($column));
728
			}
729
		}
730
		
731
		return $parameters;
732
	}
733
	
734
	/**
735
	 * Prepare a set of query parameters from the given data.
736
	 * 
737
	 * @param array $data
738
	 * @return array
739
	 */
740
	protected function dataParameters($data) {
741
		$parameters = array();
742
		
743
		foreach ($data as $value) {
744
			if ($this->resolvesPlaceholder($value)) {
745
				$parameters[] = $value;
746
			}
747
		}
748
		
749
		return $parameters;
750
	}
751
	
752
	/**
753
	 * Prepare a set of query parameters from the given set of joins.
754
	 * 
755
	 * @param Storage\Query\Join[] $joins
756
	 * @return array
757
	 */
758
	protected function joinParameters($joins) {
759
		$parameters = array();
760
		
761
		foreach ($joins as $join) {
762
			$parameters = array_merge($parameters, $this->filterParameters($join->filter));
763
		}
764
		
765
		return $parameters;
766
	}
767
	
768
	/**
769
	 * Prepare a set of query parameters from the given filter.
770
	 * 
771
	 * @param array $filter
772
	 * @return array
773
	 */
774
	protected function filterParameters($filter) {
775
		$parameters = array();
776
		
777
		foreach ($filter as $index => $value) {
778
			if (is_array($value)) {
779
				if (strtolower($index) === 'or') {
780
					$parameters = array_merge($parameters, $this->filterParameters($value));
781
				} else {
782
					foreach ($value as $in) {
783
						if ($this->resolvesPlaceholder($value)) {
784
							$parameters[] = $in;
785
						}
786
					}
787
				}
788
				
789
				continue;
790
			}
791
			
792
			if ($value instanceof Storage\Query\Builder) {
793
				$value = $value->query;
794
			}
795
			
796
			if ($value instanceof Storage\Query) {
797
				$parameters = array_merge($parameters, $this->parameters($value));
798
				
799
				continue;
800
			}
801
			
802
			if ($this->resolvesPlaceholder($value)) {
803
				$parameters[] = $value;
804
			}
805
		}
806
		
807
		return $parameters;
808
	}
809
	
810
	/**
811
	 * Retrieve an array of parameters from the given query for executing a
812
	 * prepared query.
813
	 * 
814
	 * @param Storage\Query $storageQuery
815
	 * @return array
816
	 */
817
	public function parameters(Storage\Query $storageQuery) {
818
		$parameters = $this->columnParameters($storageQuery->fields);
819
		
820
		if (in_array($storageQuery->type, array(Storage\Query::CREATE, Storage\Query::UPDATE))) {
821
			$parameters = $this->dataParameters($storageQuery->data);
822
		}
823
		
824
		$joinParameters = array();
825
		
826
		if ($storageQuery instanceof Database\Storage\Query)
827
			$joinParameters = $this->joinParameters($storageQuery->joins);
0 ignored issues
show
Documentation introduced by
$storageQuery->joins is of type array<integer,object<Dar...se\Storage\Query\Join>>, but the function expects a array<integer,object<Darya\Storage\Query\Join>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
828
		
829
		$parameters = array_merge(
830
			$parameters,
831
			$joinParameters,
832
			$this->filterParameters($storageQuery->filter)
833
		);
834
		
835
		return $parameters;
836
	}
837
	
838
}
839