Passed
Push — v2 ( 9961a6...bb80e3 )
by Berend
02:35
created

AbstractActiveRecord::checkHookConstraints()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 5
Ratio 33.33 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 5
loc 15
ccs 8
cts 8
cp 1
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
/**
4
 * This file is part of the miBadger package.
5
 *
6
 * @author Michael Webbers <[email protected]>
7
 * @license http://opensource.org/licenses/Apache-2.0 Apache v2 License
8
 */
9
10
namespace miBadger\ActiveRecord;
11
12
use miBadger\Query\Query;
13
14
/**
15
 * The abstract active record class.
16
 *
17
 * @since 1.0.0
18
 */
19
abstract class AbstractActiveRecord implements ActiveRecordInterface
20
{
21
	const COLUMN_NAME_ID = 'id';
22
	const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
24
	/** @var \PDO The PDO object. */
25
	protected $pdo;
26
27
	/** @var null|int The ID. */
28
	private $id;
29
30
	/** @var array A map of column name to functions that hook the insert function */
31
	protected $registeredCreateHooks;
32
33
	/** @var array A map of column name to functions that hook the read function */
34
	protected $registeredReadHooks;
35
36
	/** @var array A map of column name to functions that hook the update function */
37
	protected $registeredUpdateHooks;
38
39
	/** @var array A map of column name to functions that hook the update function */
40
	protected $registeredDeleteHooks;	
41
42
	/** @var array A map of column name to functions that hook the search function */
43
	protected $registeredSearchHooks;
44
45
	/** @var array A list of table column definitions */
46
	protected $tableDefinition;
47
48
	/**
49
	 * Construct an abstract active record with the given PDO.
50
	 *
51
	 * @param \PDO $pdo
52
	 */
53 71
	public function __construct(\PDO $pdo)
54
	{
55 71
		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56 71
		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
57
58 71
		$this->setPdo($pdo);
59 71
		$this->tableDefinition = $this->getActiveRecordTableDefinition();
60 71
		$this->registeredCreateHooks = [];
61 71
		$this->registeredReadHooks = [];
62 71
		$this->registeredUpdateHooks = [];
63 71
		$this->registeredDeleteHooks = [];
64 71
		$this->registeredSearchHooks = [];
65
66
		// Extend table definition with default ID field, throw exception if field already exists
67 71
		if (array_key_exists('id', $this->tableDefinition)) {
68
			$message = "Table definition in record contains a field with name \"id\"";
69
			$message .= ", which is a reserved name by ActiveRecord";
70
			throw new ActiveRecordException($message, 0);
71
		}
72
73 71
		$this->tableDefinition[self::COLUMN_NAME_ID] =
74
		[
75 71
			'value' => &$this->id,
76
			'validate' => null,
77 71
			'type' => self::COLUMN_TYPE_ID,
78 71
			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY
79
		];
80 71
	}
81
82 28
	private function checkHookConstraints($columnName, $hookMap)
83
	{
84
		// Check whether column exists
85 28
		if (!array_key_exists($columnName, $this->tableDefinition)) 
86
		{
87 5
			throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
88
		}
89
90
		// Enforcing 1 hook per table column
91 23 View Code Duplication
		if (array_key_exists($columnName, $hookMap)) {
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...
92 5
			$message = "Hook is trying to register on an already registered column \"$columnName\", ";
93 5
			$message .= "do you have conflicting traits?";
94 5
			throw new ActiveRecordException($message, 0);
95
		}
96 23
	}
97
98
	/**
99
	 * Register a new hook for a specific column that gets called before execution of the create() method
100
	 * Only one hook per column can be registered at a time
101
	 * @param string $columnName The name of the column that is registered.
102
	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
103
	 */
104 6 View Code Duplication
	public function registerCreateHook($columnName, $fn)
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...
105
	{
106 6
		$this->checkHookConstraints($columnName, $this->registeredCreateHooks);
107
108 5
		if (is_string($fn) && is_callable([$this, $fn])) {
109 3
			$this->registeredCreateHooks[$columnName] = [$this, $fn];
110 2
		} else if (is_callable($fn)) {
111 2
			$this->registeredCreateHooks[$columnName] = $fn;
112
		} else {
113
			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
114
		}
115 5
	}
116
117
	/**
118
	 * Register a new hook for a specific column that gets called before execution of the read() method
119
	 * Only one hook per column can be registered at a time
120
	 * @param string $columnName The name of the column that is registered.
121
	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
122
	 */
123 3 View Code Duplication
	public function registerReadHook($columnName, $fn)
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...
124
	{
125 3
		$this->checkHookConstraints($columnName, $this->registeredReadHooks);
126
127 2
		if (is_string($fn) && is_callable([$this, $fn])) {
128
			$this->registeredReadHooks[$columnName] = [$this, $fn];
129 2
		} else if (is_callable($fn)) {
130 2
			$this->registeredReadHooks[$columnName] = $fn;
131
		} else {
132
			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
133
		}
134 2
	}
135
136
137
138
139
	/**
140
	 * Register a new hook for a specific column that gets called before execution of the update() method
141
	 * Only one hook per column can be registered at a time
142
	 * @param string $columnName The name of the column that is registered.
143
	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
144
	 */
145 6 View Code Duplication
	public function registerUpdateHook($columnName, $fn)
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...
146
	{
147 6
		$this->checkHookConstraints($columnName, $this->registeredUpdateHooks);
148
149 5
		if (is_string($fn) && is_callable([$this, $fn])) {
150 3
			$this->registeredUpdateHooks[$columnName] = [$this, $fn];
151 2
		} else if (is_callable($fn)) {
152 2
			$this->registeredUpdateHooks[$columnName] = $fn;
153
		} else {
154
			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
155
		}
156 5
	}
157
158
	/**
159
	 * Register a new hook for a specific column that gets called before execution of the delete() method
160
	 * Only one hook per column can be registered at a time
161
	 * @param string $columnName The name of the column that is registered.
162
	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
163
	 */
164 3 View Code Duplication
	public function registerDeleteHook($columnName, $fn)
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...
165
	{
166 3
		$this->checkHookConstraints($columnName, $this->registeredDeleteHooks);
167
168 2
		if (is_string($fn) && is_callable([$this, $fn])) {
169
			$this->registeredDeleteHooks[$columnName] = [$this, $fn];
170 2
		} else if (is_callable($fn)) {
171 2
			$this->registeredDeleteHooks[$columnName] = $fn;
172
		} else {
173
			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
174
		}
175 2
	}
176
177
	/**
178
	 * Register a new hook for a specific column that gets called before execution of the search() method
179
	 * Only one hook per column can be registered at a time
180
	 * @param string $columnName The name of the column that is registered.
181
	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object. The callable is required to take one argument: an instance of miBadger\Query\Query; 
182
	 */
183 13 View Code Duplication
	public function registerSearchHook($columnName, $fn)
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...
184
	{
185 13
		$this->checkHookConstraints($columnName, $this->registeredSearchHooks);
186
187 12
		if (is_string($fn) && is_callable([$this, $fn])) {
188 10
			$this->registeredSearchHooks[$columnName] = [$this, $fn];
189 2
		} else if (is_callable($fn)) {
190 2
			$this->registeredSearchHooks[$columnName] = $fn;
191
		} else {
192
			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
193
		}
194 12
	}
195
196
	/**
197
	 * Adds a new column definition to the table.
198
	 * @param string $columnName The name of the column that is registered.
199
	 * @param Array $definition The definition of that column.
200
	 */
201 38
	public function extendTableDefinition($columnName, $definition)
202
	{
203 38
		if ($this->tableDefinition === null) {
204
			throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor");
205
		}
206
207
		// Enforcing table can only be extended with new columns
208 38 View Code Duplication
		if (array_key_exists($columnName, $this->tableDefinition)) {
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...
209 1
			$message = "Table is being extended with a column that already exists, ";
210 1
			$message .= "\"$columnName\" conflicts with your table definition";
211 1
			throw new ActiveRecordException($message, 0);
212
		}
213
214 38
		$this->tableDefinition[$columnName] = $definition;
215 38
	}
216
217
	/**
218
	 * Returns the type string as it should appear in the mysql create table statement for the given column
219
	 * @return string The type string
220
	 */
221 22
	private function getDatabaseTypeString($colName, $type, $length)
222
	{
223 22
		if ($type === null) 
224
		{
225
			throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
226
		}
227
228 22
		switch (strtoupper($type)) {
229 22
			case 'DATETIME':
230 22
			case 'DATE':
231 22
			case 'TIME':
232 22
			case 'TEXT':
233 22
			case 'INT UNSIGNED':
234 22
				return $type;
235
236 21
			case 'VARCHAR':
237 21
				if ($length === null) {
238
					throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
239
				} else {
240 21
					return sprintf('%s(%d)', $type, $length);	
241
				}
242
243 9
			case 'INT':
244
			case 'TINYINT':
245
			case 'BIGINT':
246
			default: 	
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
247
			// @TODO(Default): throw exception, or implicitly assume that type is correct? (For when using SQL databases with different types)
248 9
				if ($length === null) {
249
					return $type;
250
				} else {
251 9
					return sprintf('%s(%d)', $type, $length);	
252
				}
253
		}
254
	}
255
256
	/**
257
	 * Builds the part of a MySQL create table statement that corresponds to the supplied column
258
	 * @param string $colName 	Name of the database column
259
	 * @param string $type 		The type of the string
260
	 * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
261
	 * @return string
262
	 */
263 22
	private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
264
	{
265
266 22
		$stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length));
267 22
		if ($properties & ColumnProperty::NOT_NULL) {
268 22
			$stmnt .= 'NOT NULL ';
269
		} else {
270 22
			$stmnt .= 'NULL ';
271
		}
272
273 22
		if ($default !== NULL) {
274 15
			$stmnt .= ' DEFAULT ' . $default . ' ';
275
		}
276
277 22
		if ($properties & ColumnProperty::AUTO_INCREMENT) {
278 22
			$stmnt .= 'AUTO_INCREMENT ';
279
		}
280
281 22
		if ($properties & ColumnProperty::UNIQUE) {
282 8
			$stmnt .= 'UNIQUE ';
283
		}
284
285 22
		if ($properties & ColumnProperty::PRIMARY_KEY) {
286 22
			$stmnt .= 'PRIMARY KEY ';
287
		}
288
289 22
		return $stmnt;
290
	}
291
292
	/**
293
	 * Sorts the column statement components in the order such that the id appears first, 
294
	 * 		followed by all other columns in alphabetical ascending order
295
	 * @param   Array $colStatements Array of column statements
296
	 * @return  Array
297
	 */
298 22
	private function sortColumnStatements($colStatements)
299
	{
300
		// Find ID statement and put it first
301 22
		$sortedStatements = [];
302
303 22
		$sortedStatements[] = $colStatements[self::COLUMN_NAME_ID];
304 22
		unset($colStatements[self::COLUMN_NAME_ID]);
305
306
		// Sort remaining columns in alphabetical order
307 22
		$columns = array_keys($colStatements);
308 22
		sort($columns);
309 22
		foreach ($columns as $colName) {
310 22
			$sortedStatements[] = $colStatements[$colName];
311
		}
312
313 22
		return $sortedStatements;
314
	}
315
316
	/**
317
	 * Builds the MySQL Create Table statement for the internal table definition
318
	 * @return string
319
	 */
320 22
	public function buildCreateTableSQL()
321
	{
322 22
		$columnStatements = [];
323 22
		foreach ($this->tableDefinition as $colName => $definition) {
324
			// Destructure column definition
325 22
			$type    = $definition['type'] ?? null;
326 22
			$default = $definition['default'] ?? null;
327 22
			$length  = $definition['length'] ?? null;
328 22
			$properties = $definition['properties'] ?? null;
329
330 22
			if (isset($definition['relation']) && $type !== null) {
331
				$msg = "Column \"$colName\": ";
332
				$msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
333
				throw new ActiveRecordException($msg);
334 22
			} else if (isset($definition['relation'])) {
335 2
				$type = self::COLUMN_TYPE_ID;
336
			}
337
338 22
			$columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
339
		}
340
341
		// Sort table (first column is id, the remaining are alphabetically sorted)
342 22
		$columnStatements = $this->sortColumnStatements($columnStatements);
343
344 22
		$sql = 'CREATE TABLE ' . $this->getActiveRecordTable() . ' ';
345 22
		$sql .= "(\n";
346 22
		$sql .= join($columnStatements, ",\n");
347 22
		$sql .= "\n);";
348
349 22
		return $sql;
350
	}
351
352
	/**
353
	 * Creates the entity as a table in the database
354
	 */
355 22
	public function createTable()
356
	{
357 22
		$this->pdo->query($this->buildCreateTableSQL());
358 22
	}
359
360
	/**
361
	 * builds a MySQL constraint statement for the given parameters
362
	 * @param string $parentTable
363
	 * @param string $parentColumn
364
	 * @param string $childTable
365
	 * @param string $childColumn
366
	 * @return string The MySQL table constraint string
367
	 */
368 4
	protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
369
	{
370
		$template = <<<SQL
371 4
ALTER TABLE `%s`
372
ADD CONSTRAINT
373
FOREIGN KEY (`%s`)
374
REFERENCES `%s`(`%s`)
375
ON DELETE CASCADE;
376
SQL;
377 4
		return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
378
	}
379
380
	/**
381
	 * Iterates over the specified constraints in the table definition, 
382
	 * 		and applies these to the database.
383
	 */
384 1
	public function createTableConstraints()
385
	{
386
		// Iterate over columns, check whether "relation" field exists, if so create constraint
387 1
		foreach ($this->tableDefinition as $colName => $definition) {
388 1
			if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
389
				// Forge new relation
390 1
				$target = $definition['relation'];
391 1
				$constraintSql = $this->buildConstraint($target->getActiveRecordTable(), 'id', $this->getActiveRecordTable(), $colName);
392
393 1
				$this->pdo->query($constraintSql);
394
			}
395
		}
396 1
	}
397
398
	/**
399
	 * Returns the name -> variable mapping for the table definition.
400
	 * @return Array The mapping
401
	 */
402 44
	protected function getActiveRecordColumns()
403
	{
404 44
		$bindings = [];
405 44
		foreach ($this->tableDefinition as $colName => $definition) {
406
407
			// Ignore the id column (key) when inserting or updating
408 44
			if ($colName == self::COLUMN_NAME_ID) {
409 44
				continue;
410
			}
411
412 44
			$bindings[$colName] = &$definition['value'];
413
		}
414 44
		return $bindings;
415
	}
416
417
	/**
418
	 * {@inheritdoc}
419
	 */
420 19
	public function create()
421
	{
422 19
		foreach ($this->registeredCreateHooks as $colName => $fn) {
423
			// @TODO: Would it be better to pass the Query to the function?
424 3
			$fn();
425
		}
426
427
		try {
428 19
			(new Query($this->getPdo(), $this->getActiveRecordTable()))
429 19
				->insert($this->getActiveRecordColumns())
430 19
				->execute();
431
432 17
			$this->setId(intval($this->getPdo()->lastInsertId()));
433 2
		} catch (\PDOException $e) {
434 2
			throw new ActiveRecordException($e->getMessage(), 0, $e);
435
		}
436
437 17
		return $this;
438
	}
439
440
	/**
441
	 * {@inheritdoc}
442
	 */
443 17
	public function read($id)
444
	{
445 17
		foreach ($this->registeredReadHooks as $colName => $fn) {
446
			// @TODO: Would it be better to pass the Query to the function?
447 1
			$fn();
448
		}
449
450
		try {
451 17
			$row = (new Query($this->getPdo(), $this->getActiveRecordTable()))
452 17
				->select()
453 17
				->where('id', '=', $id)
454 17
				->execute()
455 16
				->fetch();
456
457 16
			if ($row === false) {
458 3
				throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getActiveRecordTable()));
459
			}
460
461 13
			$this->fill($row)->setId($id);
462 4
		} catch (\PDOException $e) {
463 1
			throw new ActiveRecordException($e->getMessage(), 0, $e);
464
		}
465
466 13
		return $this;
467
	}
468
469
	/**
470
	 * {@inheritdoc}
471
	 */
472 8 View Code Duplication
	public function update()
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...
473
	{
474 8
		foreach ($this->registeredUpdateHooks as $colName => $fn) {
475
			// @TODO: Would it be better to pass the Query to the function?
476 2
			$fn();
477
		}
478
479
		try {
480 8
			(new Query($this->getPdo(), $this->getActiveRecordTable()))
481 8
				->update($this->getActiveRecordColumns())
482 8
				->where('id', '=', $this->getId())
483 8
				->execute();
484 2
		} catch (\PDOException $e) {
485 2
			throw new ActiveRecordException($e->getMessage(), 0, $e);
486
		}
487
488 6
		return $this;
489
	}
490
491
	/**
492
	 * {@inheritdoc}
493
	 */
494 6 View Code Duplication
	public function delete()
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...
495
	{
496 6
		foreach ($this->registeredDeleteHooks as $colName => $fn) {
497
			// @TODO: Would it be better to pass the Query to the function?
498 1
			$fn();
499
		}
500
501
		try {
502 6
			(new Query($this->getPdo(), $this->getActiveRecordTable()))
503 6
				->delete()
504 6
				->where('id', '=', $this->getId())
505 6
				->execute();
506
507 5
			$this->setId(null);
508 1
		} catch (\PDOException $e) {
509 1
			throw new ActiveRecordException($e->getMessage(), 0, $e);
510
		}
511
512 5
		return $this;
513
	}
514
515
	/**
516
	 * {@inheritdoc}
517
	 */
518 2
	public function sync()
519
	{
520 2
		if (!$this->exists()) {
521 1
			return $this->create();
522
		}
523
524 1
		return $this->update();
525
	}
526
527
	/**
528
	 * {@inheritdoc}
529
	 */
530 3
	public function exists()
531
	{
532 3
		return $this->getId() !== null;
533
	}
534
535
	/**
536
	 * {@inheritdoc}
537
	 */
538 26
	public function fill(array $attributes)
539
	{
540 26
		$columns = $this->getActiveRecordColumns();
541 26
		$columns['id'] = &$this->id;
542
543 26
		foreach ($attributes as $key => $value) {
544 26
			if (array_key_exists($key, $columns)) {
545 26
				$columns[$key] = $value;
546
			}
547
		}
548
549 26
		return $this;
550
	}
551
552
	/**
553
	 * {@inheritdoc}
554
	 */
555 3
	public function searchOne(array $where = [], array $orderBy = [])
556
	{
557
		try {
558 3
			$row = $this->getSearchQueryResult($where, $orderBy, 1)->fetch();
559
560 2
			if ($row === false) {
561 1
				throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->getActiveRecordTable()));
562
			}
563
564 1
			return $this->fill($row)->setId($row['id']);
565 2
		} catch (\PDOException $e) {
566 1
			throw new ActiveRecordException($e->getMessage(), 0, $e);
567
		}
568
	}
569
570
	/**
571
	 * {@inheritdoc}
572
	 */
573 13
	public function search(array $where = [], array $orderBy = [], $limit = -1, $offset = 0)
574
	{
575
		try {
576 13
			$queryResult = $this->getSearchQueryResult($where, $orderBy, $limit, $offset);
577 11
			$result = [];
578
579 11
			foreach ($queryResult as $row) {
580 11
				$new = clone $this;
581
582 11
				$result[] = $new->fill($row)->setId($row['id']);
583
			}
584
585 11
			return $result;
586 2
		} catch (\PDOException $e) {
587 2
			throw new ActiveRecordException($e->getMessage(), 0, $e);
588
		}
589
	}
590
591
	/**
592
	 * Returns the search query result with the given where, order by, limit and offset clauses.
593
	 *
594
	 * @param array $where = []
595
	 * @param array $orderBy = []
596
	 * @param int $limit = -1
597
	 * @param int $offset = 0
598
	 * @return \miBadger\Query\QueryResult the search query result with the given where, order by, limit and offset clauses.
599
	 */
600 16
	private function getSearchQueryResult(array $where = [], array $orderBy = [], $limit = -1, $offset = 0)
601
	{
602 16
		$query = (new Query($this->getPdo(), $this->getActiveRecordTable()))
603 16
			->select();
604
605 16
		$this->getSearchQueryWhere($query, $where);
606 16
		$this->getSearchQueryOrderBy($query, $orderBy);
607 16
		$this->getSearchQueryLimit($query, $limit, $offset);
608
609
		// Ignore all trait modifiers for which a where clause was specified
610 16
		$registeredSearchHooks = $this->registeredSearchHooks;
611 16
		foreach ($where as $index => $clause) {
612 9
			$colName = $clause[0];
613 9
			unset($registeredSearchHooks[$colName]);
614
		}
615
616
		// Allow traits to modify the query
617 16
		foreach ($registeredSearchHooks as $column => $searchFunction) {
618 1
			$searchFunction($query);
619
		}
620
621 16
		return $query->execute();
622
	}
623
624
	/**
625
	 * Returns the given query after adding the given where conditions.
626
	 *
627
	 * @param \miBadger\Query\Query $query
628
	 * @param array $where
629
	 * @return \miBadger\Query\Query the given query after adding the given where conditions.
630
	 */
631 16
	private function getSearchQueryWhere($query, $where)
632
	{
633 16
		foreach ($where as $key => $value) {
634 9
			$query->where($value[0], $value[1], $value[2]);
635
		}
636
637 16
		return $query;
638
	}
639
640
	/**
641
	 * Returns the given query after adding the given order by conditions.
642
	 *
643
	 * @param \miBadger\Query\Query $query
644
	 * @param array $orderBy
645
	 * @return \miBadger\Query\Query the given query after adding the given order by conditions.
646
	 */
647 16
	private function getSearchQueryOrderBy($query, $orderBy)
648
	{
649 16
		foreach ($orderBy as $key => $value) {
650 1
			$query->orderBy($key, $value);
651
		}
652
653 16
		return $query;
654
	}
655
656
	/**
657
	 * Returns the given query after adding the given limit and offset conditions.
658
	 *
659
	 * @param \miBadger\Query\Query $query
660
	 * @param int $limit
661
	 * @param int $offset
662
	 * @return \miBadger\Query\Query the given query after adding the given limit and offset conditions.
663
	 */
664 16
	private function getSearchQueryLimit($query, $limit, $offset)
665
	{
666 16
		if ($limit > -1) {
667 5
			$query->limit($limit);
668 5
			$query->offset($offset);
669
		}
670
671 16
		return $query;
672
	}
673
674
	/**
675
	 * Returns the PDO.
676
	 *
677
	 * @return \PDO the PDO.
678
	 */
679 50
	public function getPdo()
680
	{
681 50
		return $this->pdo;
682
	}
683
684
	/**
685
	 * Set the PDO.
686
	 *
687
	 * @param \PDO $pdo
688
	 * @return $this
689
	 */
690 71
	protected function setPdo($pdo)
691
	{
692 71
		$this->pdo = $pdo;
693
694 71
		return $this;
695
	}
696
697
	/**
698
	 * Returns the ID.
699
	 *
700
	 * @return null|int The ID.
701
	 */
702 21
	public function getId()
703
	{
704 21
		return $this->id;
705
	}
706
707
	/**
708
	 * Set the ID.
709
	 *
710
	 * @param int $id
711
	 * @return $this
712
	 */
713 39
	protected function setId($id)
714
	{
715 39
		$this->id = $id;
716
717 39
		return $this;
718
	}
719
720
	/**
721
	 * Returns the active record table.
722
	 *
723
	 * @return string the active record table.
724
	 */
725
	abstract protected function getActiveRecordTable();
726
727
	/**
728
	 * Returns the active record columns.
729
	 *
730
	 * @return array the active record columns.
731
	 */
732
	abstract protected function getActiveRecordTableDefinition();
733
}
734