Completed
Push — master ( 5ca746...fdb513 )
by Chris
03:03
created

AbstractSqlTranslator::prepareInsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
namespace Darya\Database\Query;
3
4
use InvalidArgumentException;
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 InvalidArgumentException
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 InvalidArgumentException("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 translatable query.
223
	 * 
224
	 * Helper for handling the translation of query objects from query builders.
225
	 * 
226
	 * @param mixed $value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
227
	 * @return Database\Query
228
	 */
229
	protected function translateTranslatable($query) {
230
		if (!$this->translatable($query)) {
231
			throw new InvalidArgumentException("Cannot translate query of type '" . get_class($query) . "'");
232
		}
233
		
234
		if ($query instanceof Storage\Query\Builder) {
235
			$query = $query->query;
236
		}
237
		
238
		if ($query instanceof Storage\Query) {
239
			return $this->translate($query);
240
		}
241
	}
242
	
243
	/**
244
	 * Translate the given value if it is a query or query builder.
245
	 * 
246
	 * Returns the argument as is otherwise.
247
	 * 
248
	 * @param mixed $value
249
	 * @return mixed
250
	 */
251
	protected function translateValue($value) {
252
		$query = $this->translateTranslatable($value);
253
		
254
		return "($query->string)";
255
	}
256
	
257
	/**
258
	 * Prepare the given value for a prepared query.
259
	 * 
260
	 * If the value translatable, it is translated.
261
	 * 
262
	 * If the value is an array, it is recursively prepared.
263
	 * 
264
	 * @param mixed $value
265
	 * @return array|string
266
	 */
267 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...
268
		if (is_array($value)) {
269
			return array_map(array($this, 'value'), $value);
270
		}
271
		
272
		if ($this->translatable($value)) {
273
			return $this->translateValue($value);
274
		}
275
		
276
		return $this->resolveValue($value);
277
	}
278
	
279
	/**
280
	 * Resolve a placeholder or constant for the given parameter value.
281
	 * 
282
	 * @param mixed $value
283
	 * @return string
284
	 */
285
	protected function resolveValue($value) {
286
		if ($value === null) {
287
			return 'NULL';
288
		}
289
		
290
		if (is_bool($value)) {
291
			return $value ? 'TRUE' : 'FALSE';
292
		}
293
		
294
		return $this->placeholder;
295
	}
296
	
297
	/**
298
	 * Determine whether the given value resolves a placeholder.
299
	 * 
300
	 * @param mixed $value
301
	 * @return bool
302
	 */
303
	protected function resolvesPlaceholder($value) {
304
		return $this->resolveValue($value) === $this->placeholder;
305
	}
306
	
307
	/**
308
	 * Prepare a set of column aliases.
309
	 * 
310
	 * Uses the keys of the given array as identifiers and appends them to their
311
	 * values.
312
	 * 
313
	 * @param array $columns
314
	 * @return array
315
	 */
316
	protected function prepareColumnAliases(array $columns) {
317
		foreach ($columns as $alias => &$column) {
318
			if (is_string($alias) && preg_match('/^[\w]/', $alias)) {
319
				$aliasIdentifier = $this->identifier($alias);
320
				$column = "$column $aliasIdentifier";
321
			}
322
		}
323
		
324
		return $columns;
325
	}
326
	
327
	/**
328
	 * Prepare the given columns as a string.
329
	 * 
330
	 * @param array|string $columns
331
	 * @return string
332
	 */
333
	protected function prepareColumns($columns) {
334
		if (empty($columns)) {
335
			return '*';
336
		}
337
		
338
		$columns = (array) $this->identifier($columns);
339
		
340
		$columns = $this->prepareColumnAliases($columns);
341
		
342
		return implode(', ', $columns);
343
	}
344
	
345
	/**
346
	 * Determine whether the given operator is valid.
347
	 * 
348
	 * @param string $operator
349
	 * @return bool
350
	 */
351
	protected function validOperator($operator) {
352
		$operator = trim($operator);
353
		
354
		return in_array(strtolower($operator), $this->operators);
355
	}
356
	
357
	/**
358
	 * Prepare the given conditional operator.
359
	 * 
360
	 * Returns the equals operator if given value is not in the set of valid
361
	 * operators.
362
	 * 
363
	 * @param string $operator
364
	 * @return string
365
	 */
366
	protected function prepareRawOperator($operator) {
367
		$operator = trim($operator);
368
		
369
		return $this->validOperator($operator) ? strtoupper($operator) : '=';
370
	}
371
	
372
	/**
373
	 * Prepare an appropriate conditional operator for the given value.
374
	 * 
375
	 * @param string $operator
376
	 * @param mixed  $value    [optional]
377
	 * @return string
378
	 */
379
	protected function prepareOperator($operator, $value = null) {
380
		$operator = $this->prepareRawOperator($operator);
381
		
382 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...
383
			if ($operator === '=') {
384
				$operator = 'IS';
385
			}
386
			
387
			if ($operator === '!=') {
388
				$operator = 'IS NOT';
389
			}
390
		}
391
		
392
		if (is_array($value)) {
393
			if ($operator === '=') {
394
				$operator = 'IN';
395
			}
396
		
397
			if ($operator === '!=') {
398
				$operator = 'NOT IN';
399
			}
400
		}
401
		
402
		return $operator;
403
	}
404
	
405
	/**
406
	 * Prepare a join type.
407
	 * 
408
	 * @param string $type
409
	 * @return string
410
	 */
411
	protected function prepareJoinType($type) {
412
		if (in_array($type, array('left', 'right'))) {
413
			return strtoupper($type) . ' JOIN';
414
		}
415
		
416
		return 'JOIN';
417
	}
418
	
419
	/**
420
	 * Prepare a join table.
421
	 * 
422
	 * @param Join $join
423
	 * @return string
424
	 */
425
	protected function prepareJoinTable(Join $join) {
426
		$table = $this->identifier($join->resource);
427
		$alias = $this->identifier($join->alias);
428
		
429
		return $alias ? "$table $alias" : $table;
430
	}
431
	
432
	/**
433
	 * Prepare a single join condition.
434
	 * 
435
	 * TODO: Make this generic for WHERE or JOIN clauses. prepareCondition()?
436
	 * 
437
	 * @param string $condition
438
	 * @return string
439
	 */
440
	protected function prepareJoinCondition($condition) {
441
		$parts = preg_split('/\s+/', $condition, 3);
442
		
443
		if (count($parts) < 3) {
444
			return null;
445
		}
446
		
447
		list($first, $operator, $second) = $parts;
448
		
449
		return static::concatenate(array(
450
			$this->identifier($first),
451
			$this->prepareRawOperator($operator),
452
			$this->identifier($second)
453
		));
454
	}
455
	
456
	/**
457
	 * Prepare a join's conditions.
458
	 * 
459
	 * @param Join $join
460
	 * @return string
461
	 */
462
	protected function prepareJoinConditions(Join $join) {
463
		$conditions = array();
464
		
465
		foreach ($join->conditions as $condition) {
466
			$conditions[] = $this->prepareJoinCondition($condition);
467
		}
468
		
469
		$conditions = array_merge($conditions, $this->prepareFilter($join->filter));
470
		
471
		return static::concatenate($conditions, ' AND ');
472
	}
473
	
474
	/**
475
	 * Prepare an individual table join.
476
	 * 
477
	 * @param Join $join
478
	 * @return string
479
	 */
480
	protected function prepareJoin(Join $join) {
481
		$table = $this->prepareJoinTable($join);
482
		$conditions = $this->prepareJoinConditions($join);
483
		
484
		$clause = $table && $conditions ? "$table ON $conditions" : $table;
485
		
486
		if (empty($clause)) {
487
			return null;
488
		}
489
		
490
		$type = $this->prepareJoinType($join->type);
491
		
492
		return "$type $clause";
493
	}
494
	
495
	/**
496
	 * Prepare table joins.
497
	 * 
498
	 * @param array $joins
499
	 * @return string
500
	 */
501
	protected function prepareJoins(array $joins) {
502
		$clauses = array();
503
		
504
		foreach ($joins as $join) {
505
			$clauses[] = $this->prepareJoin($join);
506
		}
507
		
508
		return static::concatenate($clauses);
509
	}
510
	
511
	/**
512
	 * Prepare an individual filter condition.
513
	 * 
514
	 * @param string $column
515
	 * @param mixed  $given
516
	 * @return string
517
	 */
518
	protected function prepareFilterCondition($column, $given) {
519
		list($left, $right) = array_pad(preg_split('/\s+/', $column, 2), 2, null);
520
		
521
		$column = $this->prepareColumns($left);
522
		
523
		$operator = $this->prepareOperator($right, $given);
524
		$value    = $this->value($given);
525
		
526
		// If the given value is null and whatever's on the right isn't a valid
527
		// operator we can attempt to split again and find a second identifier
528
		if ($given === null && !empty($right) && !$this->validOperator($right)) {
529
			list($operator, $identifier) = array_pad(preg_split('/\s+([\w\.]+)$/', $right, 2, PREG_SPLIT_DELIM_CAPTURE), 2, null);
530
			
531
			if (!empty($identifier)) {
532
				$operator = $this->prepareRawOperator($operator);
533
				$value    = $this->identifier($identifier);
534
			}
535
		}
536
		
537
		if (is_array($value)) {
538
			$value = "(" . implode(", ", $value) . ")";
539
		}
540
		
541
		return "$column $operator $value";
542
	}
543
	
544
	/**
545
	 * Prepare a filter as a set of query conditions.
546
	 * 
547
	 * TODO: Could numeric keys be dealt with by prepareJoinCondition()?
548
	 * 
549
	 * @param array $filter
550
	 * @return array
551
	 */
552
	protected function prepareFilter(array $filter) {
553
		$conditions = array();
554
		
555
		foreach ($filter as $column => $value) {
556
			if (strtolower($column) == 'or') {
557
				$conditions[] = '(' . $this->prepareWhere($value, 'OR', true) . ')';
558
			} else {
559
				$conditions[] = $this->prepareFilterCondition($column, $value);
560
			}
561
		}
562
		
563
		return $conditions;
564
	}
565
	
566
	/**
567
	 * Prepare a WHERE clause using the given filter and comparison operator.
568
	 * 
569
	 * Example filter key-values and their SQL equivalents:
570
	 *     'id'        => 1,       // id = '1'
571
	 *     'name like' => 'Chris', // name LIKE 'Chris'
572
	 *     'count >'   => 10,      // count > '10'
573
	 *     'type in'   => [1, 2],  // type IN (1, 2)
574
	 *     'type'      => [3, 4]   // type IN (3, 4)
575
	 * 
576
	 * Comparison operator between conditions defaults to 'AND'.
577
	 * 
578
	 * @param array  $filter
579
	 * @param string $comparison   [optional]
580
	 * @param bool   $excludeWhere [optional]
581
	 * @return string
582
	 */
583
	protected function prepareWhere(array $filter, $comparison = 'AND', $excludeWhere = false) {
584
		$conditions = $this->prepareFilter($filter);
585
		
586
		if (empty($conditions)) {
587
			return null;
588
		}
589
		
590
		$clause = implode(" $comparison ", $conditions);
591
		
592
		return !$excludeWhere ? "WHERE $clause" : $clause;
593
	}
594
	
595
	/**
596
	 * Prepare an individual order condition.
597
	 * 
598
	 * @param string $column
599
	 * @param string $direction [optional]
600
	 * @return string
601
	 */
602
	protected function prepareOrder($column, $direction = null) {
603
		$column = $this->identifier($column);
604
		$direction = $direction !== null ? strtoupper($direction) : 'ASC';
605
		
606
		return !empty($column) ? "$column $direction" : null;
607
	}
608
	
609
	/**
610
	 * Prepare an ORDER BY clause using the given order.
611
	 * 
612
	 * Example order key-values:
613
	 *     'column',
614
	 *     'other_column'   => 'ASC',
615
	 *     'another_column' => 'DESC
616
	 * 
617
	 * Ordered ascending by default.
618
	 * 
619
	 * @param array|string $order
620
	 * @return string
621
	 */
622
	protected function prepareOrderBy($order) {
623
		$conditions = array();
624
		
625
		foreach ((array) $order as $key => $value) {
626
			if (is_numeric($key)) {
627
				$conditions[] = $this->prepareOrder($value);
628
			} else {
629
				$conditions[] = $this->prepareOrder($key, $value);
630
			}
631
		}
632
		
633
		return count($conditions) ? 'ORDER BY ' . implode(', ', $conditions) : null;
634
	}
635
	
636
	/**
637
	 * Prepare a LIMIT clause using the given limit and offset.
638
	 * 
639
	 * @param int $limit  [optional]
640
	 * @param int $offset [optional]
641
	 * @return string
642
	 */
643
	abstract protected function prepareLimit($limit = 0, $offset = 0);
644
	
645
	/**
646
	 * Prepare a SELECT statement using the given columns, table, clauses and
647
	 * options.
648
	 * 
649
	 * TODO: Simplify this so that prepareSelect only actually prepares the
650
	 *       SELECT and FROM clauses. The rest could be concatenated by
651
	 *       translateRead().
652
	 * 
653
	 * @param string       $table
654
	 * @param array|string $columns
655
	 * @param string       $joins    [optional]
656
	 * @param string       $where    [optional]
657
	 * @param string       $order    [optional]
658
	 * @param string       $limit    [optional]
659
	 * @param bool         $distinct [optional]
660
	 * @return string
661
	 */
662
	abstract protected function prepareSelect($table, $columns, $joins = null, $where = null, $order = null, $limit = null, $distinct = false);
663
	
664
	/**
665
	 * Prepare an INSERT INTO statement using the given table and data.
666
	 * 
667
	 * @param string $table
668
	 * @param array  $data
669
	 * @return string
670
	 */
671
	protected function prepareInsert($table, array $data) {
672
		$table = $this->identifier($table);
673
		
674
		$columns = $this->identifier(array_keys($data));
675
		$values  = $this->value(array_values($data));
676
		
677
		$columns = "(" . implode(", ", $columns) . ")";
678
		$values  = "(" . implode(", ", $values) . ")";
679
		
680
		return static::concatenate(array('INSERT INTO', $table, $columns, 'VALUES', $values));
681
	}
682
	
683
	/**
684
	 * Prepare an INSERT SELECT statement using the given table and
685
	 * subquery.
686
	 * 
687
	 * @param string        $table
688
	 * @param array         $columns
689
	 * @param Storage\Query $subquery
690
	 * @return string
691
	 */
692
	public function prepareInsertSelect($table, array $columns, Storage\Query $subquery) {
693
		$table = $this->identifier($table);
694
		
695
		if (!empty($columns)) {
696
			$columns = $this->identifier($columns);
697
			$columns = "(" . implode(", ", $columns) . ")";
698
		}
699
		
700
		$subquery = (string) $this->translate($subquery);
701
		
702
		return static::concatenate(array('INSERT INTO', $table, $columns, $subquery));
703
	}
704
	
705
	/**
706
	 * Prepare an UPDATE statement with the given table, data and clauses.
707
	 * 
708
	 * @param string $table
709
	 * @param array  $data
710
	 * @param string $where [optional]
711
	 * @param string $limit [optional]
712
	 * @return string
713
	 */
714
	abstract protected function prepareUpdate($table, $data, $where = null, $limit = null);
715
	
716
	/**
717
	 * Prepare a DELETE statement with the given table and clauses.
718
	 * 
719
	 * @param string $table
720
	 * @param string $where [optional]
721
	 * @param string $limit [optional]
722
	 * @return string
723
	 */
724
	abstract protected function prepareDelete($table, $where = null, $limit = null);
725
	
726
	/**
727
	 * Prepare a set of query parameters from the given set of columns.
728
	 * 
729
	 * @param array $columns
730
	 * @return array
731
	 */
732
	protected function columnParameters($columns) {
733
		$parameters = array();
734
		
735
		foreach ($columns as $column) {
736
			if ($column instanceof Storage\Query\Builder) {
737
				$column = $column->query;
738
			}
739
			
740
			if ($column instanceof Storage\Query) {
741
				$parameters = array_merge($parameters, $this->parameters($column));
742
			}
743
		}
744
		
745
		return $parameters;
746
	}
747
	
748
	/**
749
	 * Prepare a set of query parameters from the given data.
750
	 * 
751
	 * @param array $data
752
	 * @return array
753
	 */
754
	protected function dataParameters($data) {
755
		$parameters = array();
756
		
757
		foreach ($data as $value) {
758
			if ($this->resolvesPlaceholder($value)) {
759
				$parameters[] = $value;
760
			}
761
		}
762
		
763
		return $parameters;
764
	}
765
	
766
	/**
767
	 * Prepare a set of query parameters from the given set of joins.
768
	 * 
769
	 * @param Join[] $joins
770
	 * @return array
771
	 */
772
	protected function joinParameters($joins) {
773
		$parameters = array();
774
		
775
		foreach ($joins as $join) {
776
			$parameters = array_merge($parameters, $this->filterParameters($join->filter));
777
		}
778
		
779
		return $parameters;
780
	}
781
	
782
	/**
783
	 * Prepare a set of query parameters from the given filter.
784
	 * 
785
	 * @param array $filter
786
	 * @return array
787
	 */
788
	protected function filterParameters($filter) {
789
		$parameters = array();
790
		
791
		foreach ($filter as $index => $value) {
792
			if (is_array($value)) {
793
				if (strtolower($index) === 'or') {
794
					$parameters = array_merge($parameters, $this->filterParameters($value));
795
				} else {
796
					foreach ($value as $in) {
797
						if ($this->resolvesPlaceholder($value)) {
798
							$parameters[] = $in;
799
						}
800
					}
801
				}
802
				
803
				continue;
804
			}
805
			
806
			if ($value instanceof Storage\Query\Builder) {
807
				$value = $value->query;
808
			}
809
			
810
			if ($value instanceof Storage\Query) {
811
				$parameters = array_merge($parameters, $this->parameters($value));
812
				
813
				continue;
814
			}
815
			
816
			if ($this->resolvesPlaceholder($value)) {
817
				$parameters[] = $value;
818
			}
819
		}
820
		
821
		return $parameters;
822
	}
823
	
824
	/**
825
	 * Retrieve an array of parameters from the given query for executing a
826
	 * prepared query.
827
	 * 
828
	 * @param Storage\Query $storageQuery
829
	 * @return array
830
	 */
831
	public function parameters(Storage\Query $storageQuery) {
832
		$parameters = $this->columnParameters($storageQuery->fields);
833
		
834
		if (in_array($storageQuery->type, array(Storage\Query::CREATE, Storage\Query::UPDATE))) {
835
			$parameters = $this->dataParameters($storageQuery->data);
836
		}
837
		
838
		$joinParameters = array();
839
		
840
		if ($storageQuery instanceof Database\Storage\Query)
841
			$joinParameters = $this->joinParameters($storageQuery->joins);
842
		
843
		$parameters = array_merge(
844
			$parameters,
845
			$joinParameters,
846
			$this->filterParameters($storageQuery->filter)
847
		);
848
		
849
		return $parameters;
850
	}
851
	
852
}
853