Completed
Push — v2 ( a375df...98ab20 )
by Berend
04:42
created
src/Traits/AutoApi.php 2 patches
Indentation   +411 added lines, -411 removed lines patch added patch discarded remove patch
@@ -9,431 +9,431 @@
 block discarded – undo
9 9
 
10 10
 trait AutoApi
11 11
 {
12
-	/* =======================================================================
12
+    /* =======================================================================
13 13
 	 * ===================== Automatic API Support ===========================
14 14
 	 * ======================================================================= */
15 15
 
16
-	/** @var array A map of column name to functions that hook the insert function */
17
-	protected $createHooks;
16
+    /** @var array A map of column name to functions that hook the insert function */
17
+    protected $createHooks;
18 18
 
19
-	/** @var array A map of column name to functions that hook the read function */
20
-	protected $readHooks;
19
+    /** @var array A map of column name to functions that hook the read function */
20
+    protected $readHooks;
21 21
 
22
-	/** @var array A map of column name to functions that hook the update function */
23
-	protected $updateHooks;
22
+    /** @var array A map of column name to functions that hook the update function */
23
+    protected $updateHooks;
24 24
 
25
-	/** @var array A map of column name to functions that hook the update function */
26
-	protected $deleteHooks;	
25
+    /** @var array A map of column name to functions that hook the update function */
26
+    protected $deleteHooks;	
27 27
 
28
-	/** @var array A map of column name to functions that hook the search function */
29
-	protected $searchHooks;
28
+    /** @var array A map of column name to functions that hook the search function */
29
+    protected $searchHooks;
30 30
 
31
-	/** @var array A list of table column definitions */
32
-	protected $tableDefinition;
31
+    /** @var array A list of table column definitions */
32
+    protected $tableDefinition;
33 33
 
34 34
 
35
-	/**
36
-	 * @param Array $queryparams associative array of query params. Reserved options are
37
-	 *                             "search_order_by", "search_order_direction", "search_limit", "search_offset"
38
-	 *                             or column names corresponding to an instance of miBadger\Query\QueryExpression
39
-	 * @param Array $fieldWhitelist names of the columns that will appear in the output results
40
-	 * 
41
-	 * @return Array an associative array containing the query parameters, and a data field containing an array of search results (associative arrays indexed by the keys in $fieldWhitelist)
42
-	 */
43
-	public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null, int $maxResultLimit = 100): Array
44
-	{
45
-		$query = $this->search();
35
+    /**
36
+     * @param Array $queryparams associative array of query params. Reserved options are
37
+     *                             "search_order_by", "search_order_direction", "search_limit", "search_offset"
38
+     *                             or column names corresponding to an instance of miBadger\Query\QueryExpression
39
+     * @param Array $fieldWhitelist names of the columns that will appear in the output results
40
+     * 
41
+     * @return Array an associative array containing the query parameters, and a data field containing an array of search results (associative arrays indexed by the keys in $fieldWhitelist)
42
+     */
43
+    public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null, int $maxResultLimit = 100): Array
44
+    {
45
+        $query = $this->search();
46 46
 
47
-		// Build query
48
-		$orderColumn = $queryParams['search_order_by'] ?? null;
49
-		if (!in_array($orderColumn, $fieldWhitelist)) {
50
-			$orderColumn = null;
51
-		}
47
+        // Build query
48
+        $orderColumn = $queryParams['search_order_by'] ?? null;
49
+        if (!in_array($orderColumn, $fieldWhitelist)) {
50
+            $orderColumn = null;
51
+        }
52 52
 
53
-		$orderDirection = $queryParams['search_order_direction'] ?? null;
54
-		if ($orderColumn !== null) {
55
-			$query->orderBy($orderColumn, $orderDirection);
56
-		}
53
+        $orderDirection = $queryParams['search_order_direction'] ?? null;
54
+        if ($orderColumn !== null) {
55
+            $query->orderBy($orderColumn, $orderDirection);
56
+        }
57 57
 		
58
-		if ($whereClause !== null) {
59
-			$query->where($whereClause);
60
-		}
61
-
62
-		$limit = min((int) ($queryParams['search_limit'] ?? $maxResultLimit), $maxResultLimit);
63
-		$query->limit($limit);
64
-
65
-		$offset = $queryParams['search_offset'] ?? 0;
66
-		$query->offset($offset);
67
-
68
-		$numPages = $query->getNumberOfPages();
69
-		$currentPage = $query->getCurrentPage();
70
-
71
-		// Fetch results
72
-		$results = $query->fetchAll();
73
-		$resultsArray = [];
74
-		foreach ($results as $result) {
75
-			$resultsArray[] = $result->toArray($fieldWhitelist);
76
-		}
77
-
78
-		return [
79
-			'search_offset' => $offset,
80
-			'search_limit' => $limit,
81
-			'search_order_by' => $orderColumn,
82
-			'search_order_direction' => $orderDirection,
83
-			'search_pages' => $numPages,
84
-			'search_current' => $currentPage,
85
-			'data' => $resultsArray
86
-		];
87
-	}
88
-
89
-	/**
90
-	 * Performs a read call on the entity (modifying the current object) 
91
-	 * 	and returns a result array ($error, $data), with an optional error, and the results array (as filtered by the whitelist)
92
-	 * 	containing the loaded data.
93
-	 * 
94
-	 * @param string|int $id the id of the current entity
95
-	 * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
96
-	 * 
97
-	 * @param Array [$error, $result]
98
-	 * 				Where $error contains the error message (@TODO: & Error type?)
99
-	 * 				Where result is an associative array containing the data for this record, and the keys are a subset of $fieldWhitelist
100
-	 * 				
101
-	 */
102
-	public function apiRead($id, Array $fieldWhitelist): Array
103
-	{
104
-		try {
105
-			$this->read($id);	
106
-		} catch (ActiveRecordException $e) {
107
-			if ($e->getCode() === ActiveRecordException::NOT_FOUND) {
108
-				$err = [
109
-					'type' => 'invalid',
110
-					'message' => $e->getMessage()
111
-				];
112
-				return [$err, null];
113
-			}
114
-			throw $e;
115
-		}
116
-		return [null, $this->toArray($fieldWhitelist)];
117
-	}
118
-
119
-	/* =============================================================
58
+        if ($whereClause !== null) {
59
+            $query->where($whereClause);
60
+        }
61
+
62
+        $limit = min((int) ($queryParams['search_limit'] ?? $maxResultLimit), $maxResultLimit);
63
+        $query->limit($limit);
64
+
65
+        $offset = $queryParams['search_offset'] ?? 0;
66
+        $query->offset($offset);
67
+
68
+        $numPages = $query->getNumberOfPages();
69
+        $currentPage = $query->getCurrentPage();
70
+
71
+        // Fetch results
72
+        $results = $query->fetchAll();
73
+        $resultsArray = [];
74
+        foreach ($results as $result) {
75
+            $resultsArray[] = $result->toArray($fieldWhitelist);
76
+        }
77
+
78
+        return [
79
+            'search_offset' => $offset,
80
+            'search_limit' => $limit,
81
+            'search_order_by' => $orderColumn,
82
+            'search_order_direction' => $orderDirection,
83
+            'search_pages' => $numPages,
84
+            'search_current' => $currentPage,
85
+            'data' => $resultsArray
86
+        ];
87
+    }
88
+
89
+    /**
90
+     * Performs a read call on the entity (modifying the current object) 
91
+     * 	and returns a result array ($error, $data), with an optional error, and the results array (as filtered by the whitelist)
92
+     * 	containing the loaded data.
93
+     * 
94
+     * @param string|int $id the id of the current entity
95
+     * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
96
+     * 
97
+     * @param Array [$error, $result]
98
+     * 				Where $error contains the error message (@TODO: & Error type?)
99
+     * 				Where result is an associative array containing the data for this record, and the keys are a subset of $fieldWhitelist
100
+     * 				
101
+     */
102
+    public function apiRead($id, Array $fieldWhitelist): Array
103
+    {
104
+        try {
105
+            $this->read($id);	
106
+        } catch (ActiveRecordException $e) {
107
+            if ($e->getCode() === ActiveRecordException::NOT_FOUND) {
108
+                $err = [
109
+                    'type' => 'invalid',
110
+                    'message' => $e->getMessage()
111
+                ];
112
+                return [$err, null];
113
+            }
114
+            throw $e;
115
+        }
116
+        return [null, $this->toArray($fieldWhitelist)];
117
+    }
118
+
119
+    /* =============================================================
120 120
 	 * ===================== Constraint validation =================
121 121
 	 * ============================================================= */
122 122
 
123
-	/**
124
-	 * Copy all table variables between two instances
125
-	 */
126
-	public function syncInstanceFrom($from)
127
-	{
128
-		foreach ($this->tableDefinition as $colName => $definition) {
129
-			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
130
-		}
131
-	}
132
-
133
-	private function filterInputColumns($input, $whitelist)
134
-	{
135
-		$filteredInput = $input;
136
-		foreach ($input as $colName => $value) {
137
-			if (!in_array($colName, $whitelist)) {
138
-				unset($filteredInput[$colName]);
139
-			}
140
-		}
141
-		return $filteredInput;
142
-	}
143
-
144
-	private function validateExcessKeys($input)
145
-	{
146
-		$errors = [];
147
-		foreach ($input as $colName => $value) {
148
-			if (!array_key_exists($colName, $this->tableDefinition)) {
149
-				$errors[$colName] = [
150
-					'type' => 'unknown_field',
151
-					'message' => 'Unknown input field'
152
-				];
153
-				continue;
154
-			}
155
-		}
156
-		return $errors;
157
-	}
158
-
159
-	private function validateImmutableColumns($input)
160
-	{
161
-		$errors = [];
162
-		foreach ($this->tableDefinition as $colName => $definition) {
163
-			$property = $definition['properties'] ?? null;
164
-			if (array_key_exists($colName, $input)
165
-				&& $property & ColumnProperty::IMMUTABLE) {
166
-				$errors[$colName] = [
167
-					'type' => 'immutable',
168
-					'message' => 'Value cannot be changed'
169
-				];
170
-			}
171
-		}
172
-		return $errors;
173
-	}
174
-
175
-	/**
176
-	 * Checks whether input values are correct:
177
-	 * 1. Checks whether a value passes the validation function for that column
178
-	 * 2. Checks whether a value supplied to a relationship column is a valid value
179
-	 */
180
-	private function validateInputValues($input)
181
-	{
182
-		$errors = [];
183
-		foreach ($this->tableDefinition as $colName => $definition) {
184
-			// Validation check 1: If validate function is present
185
-			if (array_key_exists($colName, $input) 
186
-				&& is_callable($definition['validate'] ?? null)) {
187
-				$inputValue = $input[$colName];
188
-
189
-				// If validation function fails
190
-				[$status, $message] = $definition['validate']($inputValue);
191
-				if (!$status) {
192
-					$errors[$colName] = [
193
-						'type' => 'invalid',
194
-						'message' => $message
195
-					];
196
-				}	
197
-			}
198
-
199
-			// Validation check 2: If relation column, check whether entity exists
200
-			$properties = $definition['properties'] ?? null;
201
-			if (isset($definition['relation'])
202
-				&& ($properties & ColumnProperty::NOT_NULL)) {
203
-				$instance = clone $definition['relation'];
204
-				try {
205
-					$instance->read($input[$colName] ?? $definition['value'] ?? null);
206
-				} catch (ActiveRecordException $e) {
207
-					$errors[$colName] = [
208
-						'type' => 'invalid',
209
-						'message' => 'Entry for this value does not exist'
210
-					];
211
-				}
212
-			}
213
-		}
214
-		return $errors;
215
-	}
216
-
217
-	/**
218
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
219
-	 * Determines whether there are required columns for which no data is provided
220
-	 */
221
-	private function validateMissingKeys($input)
222
-	{
223
-		$errors = [];
224
-
225
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
226
-			$default = $colDefinition['default'] ?? null;
227
-			$properties = $colDefinition['properties'] ?? null;
228
-			$value = $colDefinition['value'];
229
-
230
-			// If nullable and default not set => null
231
-			// If nullable and default null => default (null)
232
-			// If nullable and default set => default (value)
233
-
234
-			// if not nullable and default not set => error
235
-			// if not nullable and default null => error
236
-			// if not nullable and default st => default (value)
237
-			// => if not nullable and default null and value not set (or null) => error message in this method
238
-			if ($properties & ColumnProperty::NOT_NULL
239
-				&& $default === null
240
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
241
-				&& (!array_key_exists($colName, $input) 
242
-					|| $input[$colName] === null 
243
-					|| (is_string($input[$colName]) && $input[$colName] === '') )
244
-				&& ($value === null
245
-					|| (is_string($value) && $value === ''))) {
246
-				$errors[$colName] = [
247
-					'type' => 'missing',
248
-					'message' => sprintf("The required field \"%s\" is missing", $colName)
249
-				];
250
-			} 
251
-		}
252
-
253
-		return $errors;
254
-	}
255
-
256
-	/**
257
-	 * Copies the values for entries in the input with matching variable names in the record definition
258
-	 * @param Array $input The input data to be loaded into $this record
259
-	 */
260
-	private function loadData($input)
261
-	{
262
-		foreach ($this->tableDefinition as $colName => $definition) {
263
-			// Skip if this table column does not appear in the input
264
-			if (!array_key_exists($colName, $input)) {
265
-				continue;
266
-			}
267
-
268
-			// Use setter if known, otherwise set value directly
269
-			$fn = $definition['setter'] ?? null;
270
-			if (is_callable($fn)) {
271
-				$fn($input[$colName]);
272
-			} else {
273
-				$definition['value'] = $input[$colName];
274
-			}
275
-		}
276
-	}
277
-
278
-	/**
279
-	 * @param Array $input Associative array of input values
280
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
281
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
282
-	 * 					of the modified data.
283
-	 */
284
-	public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
285
-	{
286
-		// Clone $this to new instance (for restoring if validation goes wrong)
287
-		$transaction = $this->newInstance();
288
-		$errors = [];
289
-
290
-		// Filter out all non-whitelisted input values
291
-		$input = $this->filterInputColumns($input, $createWhitelist);
292
-
293
-		// Validate excess keys
294
-		$errors += $transaction->validateExcessKeys($input);
295
-
296
-		// Validate input values (using validation function)
297
-		$errors += $transaction->validateInputValues($input);
298
-
299
-		// "Copy" data into transaction
300
-		$transaction->loadData($input);
301
-
302
-		// Run create hooks
303
-		foreach ($transaction->createHooks as $colName => $fn) {
304
-			$fn();
305
-		}
306
-
307
-		// Validate missing keys
308
-		$errors += $transaction->validateMissingKeys($input);
309
-
310
-		// If no errors, commit the pending data
311
-		if (empty($errors)) {
312
-			$this->syncInstanceFrom($transaction);
313
-
314
-			// Insert default values for not-null fields
315
-			$this->insertDefaults();
316
-
317
-			try {
318
-				(new Query($this->getPdo(), $this->getTableName()))
319
-					->insert($this->getActiveRecordColumns())
320
-					->execute();
321
-
322
-				$this->setId(intval($this->getPdo()->lastInsertId()));
323
-			} catch (\PDOException $e) {
324
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
325
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
326
-			}
327
-
328
-			return [null, $this->toArray($readWhitelist)];
329
-		} else {
330
-			return [$errors, null];
331
-		}
332
-	}
333
-
334
-	/**
335
-	 * @param Array $input Associative array of input values
336
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
337
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
338
-	 * 					of the modified data.
339
-	 */
340
-	public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
341
-	{
342
-		$transaction = $this->newInstance();
343
-		$transaction->syncInstanceFrom($this);
344
-		$errors = [];
345
-
346
-		// Filter out all non-whitelisted input values
347
-		$input = $this->filterInputColumns($input, $updateWhitelist);
348
-
349
-		// Check for excess keys
350
-		$errors += $transaction->validateExcessKeys($input);
351
-
352
-		// Check for immutable keys
353
-		$errors += $transaction->validateImmutableColumns($input);
354
-
355
-		// Validate input values (using validation function)
356
-		$errors += $transaction->validateInputValues($input);
357
-
358
-		// "Copy" data into transaction
359
-		$transaction->loadData($input);
360
-
361
-		// Run create hooks
362
-		foreach ($transaction->updateHooks as $colName => $fn) {
363
-			$fn();
364
-		}
365
-
366
-		// Validate missing keys
367
-		$errors += $transaction->validateMissingKeys($input);
368
-
369
-		// Update database
370
-		if (empty($errors)) {
371
-			$this->syncInstanceFrom($transaction);
372
-
373
-			try {
374
-				(new Query($this->getPdo(), $this->getTableName()))
375
-					->update($this->getActiveRecordColumns())
376
-					->where(Query::Equal('id', $this->getId()))
377
-					->execute();
378
-			} catch (\PDOException $e) {
379
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
380
-			}
381
-
382
-			return [null, $this->toArray($readWhitelist)];
383
-		} else {
384
-			return [$errors, null];
385
-		}
386
-	}
387
-
388
-	/**
389
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
390
-	 *
391
-	 * @param mixed $id
392
-	 * @return $this
393
-	 * @throws ActiveRecordException on failure.
394
-	 */
395
-	abstract public function read($id);
396
-
397
-	/**
398
-	 * Returns the PDO.
399
-	 *
400
-	 * @return \PDO the PDO.
401
-	 */
402
-	abstract public function getPdo();
403
-
404
-	/**
405
-	 * Set the ID.
406
-	 *
407
-	 * @param int $id
408
-	 * @return $this
409
-	 */
410
-	abstract protected function setId($id);
411
-
412
-	/**
413
-	 * Returns the ID.
414
-	 *
415
-	 * @return null|int The ID.
416
-	 */
417
-	abstract protected function getId();
418
-
419
-
420
-	/**
421
-	 * Returns the serialized form of the specified columns
422
-	 * 
423
-	 * @return Array
424
-	 */
425
-	abstract public function toArray(Array $fieldWhitelist);
426
-
427
-	/**
428
-	 * Returns the active record table.
429
-	 *
430
-	 * @return string the active record table name.
431
-	 */
432
-	abstract public function getTableName();
433
-
434
-	/**
435
-	 * Returns the name -> variable mapping for the table definition.
436
-	 * @return Array The mapping
437
-	 */
438
-	abstract protected function getActiveRecordColumns();
123
+    /**
124
+     * Copy all table variables between two instances
125
+     */
126
+    public function syncInstanceFrom($from)
127
+    {
128
+        foreach ($this->tableDefinition as $colName => $definition) {
129
+            $this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
130
+        }
131
+    }
132
+
133
+    private function filterInputColumns($input, $whitelist)
134
+    {
135
+        $filteredInput = $input;
136
+        foreach ($input as $colName => $value) {
137
+            if (!in_array($colName, $whitelist)) {
138
+                unset($filteredInput[$colName]);
139
+            }
140
+        }
141
+        return $filteredInput;
142
+    }
143
+
144
+    private function validateExcessKeys($input)
145
+    {
146
+        $errors = [];
147
+        foreach ($input as $colName => $value) {
148
+            if (!array_key_exists($colName, $this->tableDefinition)) {
149
+                $errors[$colName] = [
150
+                    'type' => 'unknown_field',
151
+                    'message' => 'Unknown input field'
152
+                ];
153
+                continue;
154
+            }
155
+        }
156
+        return $errors;
157
+    }
158
+
159
+    private function validateImmutableColumns($input)
160
+    {
161
+        $errors = [];
162
+        foreach ($this->tableDefinition as $colName => $definition) {
163
+            $property = $definition['properties'] ?? null;
164
+            if (array_key_exists($colName, $input)
165
+                && $property & ColumnProperty::IMMUTABLE) {
166
+                $errors[$colName] = [
167
+                    'type' => 'immutable',
168
+                    'message' => 'Value cannot be changed'
169
+                ];
170
+            }
171
+        }
172
+        return $errors;
173
+    }
174
+
175
+    /**
176
+     * Checks whether input values are correct:
177
+     * 1. Checks whether a value passes the validation function for that column
178
+     * 2. Checks whether a value supplied to a relationship column is a valid value
179
+     */
180
+    private function validateInputValues($input)
181
+    {
182
+        $errors = [];
183
+        foreach ($this->tableDefinition as $colName => $definition) {
184
+            // Validation check 1: If validate function is present
185
+            if (array_key_exists($colName, $input) 
186
+                && is_callable($definition['validate'] ?? null)) {
187
+                $inputValue = $input[$colName];
188
+
189
+                // If validation function fails
190
+                [$status, $message] = $definition['validate']($inputValue);
191
+                if (!$status) {
192
+                    $errors[$colName] = [
193
+                        'type' => 'invalid',
194
+                        'message' => $message
195
+                    ];
196
+                }	
197
+            }
198
+
199
+            // Validation check 2: If relation column, check whether entity exists
200
+            $properties = $definition['properties'] ?? null;
201
+            if (isset($definition['relation'])
202
+                && ($properties & ColumnProperty::NOT_NULL)) {
203
+                $instance = clone $definition['relation'];
204
+                try {
205
+                    $instance->read($input[$colName] ?? $definition['value'] ?? null);
206
+                } catch (ActiveRecordException $e) {
207
+                    $errors[$colName] = [
208
+                        'type' => 'invalid',
209
+                        'message' => 'Entry for this value does not exist'
210
+                    ];
211
+                }
212
+            }
213
+        }
214
+        return $errors;
215
+    }
216
+
217
+    /**
218
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
219
+     * Determines whether there are required columns for which no data is provided
220
+     */
221
+    private function validateMissingKeys($input)
222
+    {
223
+        $errors = [];
224
+
225
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
226
+            $default = $colDefinition['default'] ?? null;
227
+            $properties = $colDefinition['properties'] ?? null;
228
+            $value = $colDefinition['value'];
229
+
230
+            // If nullable and default not set => null
231
+            // If nullable and default null => default (null)
232
+            // If nullable and default set => default (value)
233
+
234
+            // if not nullable and default not set => error
235
+            // if not nullable and default null => error
236
+            // if not nullable and default st => default (value)
237
+            // => if not nullable and default null and value not set (or null) => error message in this method
238
+            if ($properties & ColumnProperty::NOT_NULL
239
+                && $default === null
240
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
241
+                && (!array_key_exists($colName, $input) 
242
+                    || $input[$colName] === null 
243
+                    || (is_string($input[$colName]) && $input[$colName] === '') )
244
+                && ($value === null
245
+                    || (is_string($value) && $value === ''))) {
246
+                $errors[$colName] = [
247
+                    'type' => 'missing',
248
+                    'message' => sprintf("The required field \"%s\" is missing", $colName)
249
+                ];
250
+            } 
251
+        }
252
+
253
+        return $errors;
254
+    }
255
+
256
+    /**
257
+     * Copies the values for entries in the input with matching variable names in the record definition
258
+     * @param Array $input The input data to be loaded into $this record
259
+     */
260
+    private function loadData($input)
261
+    {
262
+        foreach ($this->tableDefinition as $colName => $definition) {
263
+            // Skip if this table column does not appear in the input
264
+            if (!array_key_exists($colName, $input)) {
265
+                continue;
266
+            }
267
+
268
+            // Use setter if known, otherwise set value directly
269
+            $fn = $definition['setter'] ?? null;
270
+            if (is_callable($fn)) {
271
+                $fn($input[$colName]);
272
+            } else {
273
+                $definition['value'] = $input[$colName];
274
+            }
275
+        }
276
+    }
277
+
278
+    /**
279
+     * @param Array $input Associative array of input values
280
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
281
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
282
+     * 					of the modified data.
283
+     */
284
+    public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
285
+    {
286
+        // Clone $this to new instance (for restoring if validation goes wrong)
287
+        $transaction = $this->newInstance();
288
+        $errors = [];
289
+
290
+        // Filter out all non-whitelisted input values
291
+        $input = $this->filterInputColumns($input, $createWhitelist);
292
+
293
+        // Validate excess keys
294
+        $errors += $transaction->validateExcessKeys($input);
295
+
296
+        // Validate input values (using validation function)
297
+        $errors += $transaction->validateInputValues($input);
298
+
299
+        // "Copy" data into transaction
300
+        $transaction->loadData($input);
301
+
302
+        // Run create hooks
303
+        foreach ($transaction->createHooks as $colName => $fn) {
304
+            $fn();
305
+        }
306
+
307
+        // Validate missing keys
308
+        $errors += $transaction->validateMissingKeys($input);
309
+
310
+        // If no errors, commit the pending data
311
+        if (empty($errors)) {
312
+            $this->syncInstanceFrom($transaction);
313
+
314
+            // Insert default values for not-null fields
315
+            $this->insertDefaults();
316
+
317
+            try {
318
+                (new Query($this->getPdo(), $this->getTableName()))
319
+                    ->insert($this->getActiveRecordColumns())
320
+                    ->execute();
321
+
322
+                $this->setId(intval($this->getPdo()->lastInsertId()));
323
+            } catch (\PDOException $e) {
324
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
325
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
326
+            }
327
+
328
+            return [null, $this->toArray($readWhitelist)];
329
+        } else {
330
+            return [$errors, null];
331
+        }
332
+    }
333
+
334
+    /**
335
+     * @param Array $input Associative array of input values
336
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
337
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
338
+     * 					of the modified data.
339
+     */
340
+    public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
341
+    {
342
+        $transaction = $this->newInstance();
343
+        $transaction->syncInstanceFrom($this);
344
+        $errors = [];
345
+
346
+        // Filter out all non-whitelisted input values
347
+        $input = $this->filterInputColumns($input, $updateWhitelist);
348
+
349
+        // Check for excess keys
350
+        $errors += $transaction->validateExcessKeys($input);
351
+
352
+        // Check for immutable keys
353
+        $errors += $transaction->validateImmutableColumns($input);
354
+
355
+        // Validate input values (using validation function)
356
+        $errors += $transaction->validateInputValues($input);
357
+
358
+        // "Copy" data into transaction
359
+        $transaction->loadData($input);
360
+
361
+        // Run create hooks
362
+        foreach ($transaction->updateHooks as $colName => $fn) {
363
+            $fn();
364
+        }
365
+
366
+        // Validate missing keys
367
+        $errors += $transaction->validateMissingKeys($input);
368
+
369
+        // Update database
370
+        if (empty($errors)) {
371
+            $this->syncInstanceFrom($transaction);
372
+
373
+            try {
374
+                (new Query($this->getPdo(), $this->getTableName()))
375
+                    ->update($this->getActiveRecordColumns())
376
+                    ->where(Query::Equal('id', $this->getId()))
377
+                    ->execute();
378
+            } catch (\PDOException $e) {
379
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
380
+            }
381
+
382
+            return [null, $this->toArray($readWhitelist)];
383
+        } else {
384
+            return [$errors, null];
385
+        }
386
+    }
387
+
388
+    /**
389
+     * Returns this active record after reading the attributes from the entry with the given identifier.
390
+     *
391
+     * @param mixed $id
392
+     * @return $this
393
+     * @throws ActiveRecordException on failure.
394
+     */
395
+    abstract public function read($id);
396
+
397
+    /**
398
+     * Returns the PDO.
399
+     *
400
+     * @return \PDO the PDO.
401
+     */
402
+    abstract public function getPdo();
403
+
404
+    /**
405
+     * Set the ID.
406
+     *
407
+     * @param int $id
408
+     * @return $this
409
+     */
410
+    abstract protected function setId($id);
411
+
412
+    /**
413
+     * Returns the ID.
414
+     *
415
+     * @return null|int The ID.
416
+     */
417
+    abstract protected function getId();
418
+
419
+
420
+    /**
421
+     * Returns the serialized form of the specified columns
422
+     * 
423
+     * @return Array
424
+     */
425
+    abstract public function toArray(Array $fieldWhitelist);
426
+
427
+    /**
428
+     * Returns the active record table.
429
+     *
430
+     * @return string the active record table name.
431
+     */
432
+    abstract public function getTableName();
433
+
434
+    /**
435
+     * Returns the name -> variable mapping for the table definition.
436
+     * @return Array The mapping
437
+     */
438
+    abstract protected function getActiveRecordColumns();
439 439
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -240,7 +240,7 @@
 block discarded – undo
240 240
 				&& !($properties & ColumnProperty::AUTO_INCREMENT)
241 241
 				&& (!array_key_exists($colName, $input) 
242 242
 					|| $input[$colName] === null 
243
-					|| (is_string($input[$colName]) && $input[$colName] === '') )
243
+					|| (is_string($input[$colName]) && $input[$colName] === ''))
244 244
 				&& ($value === null
245 245
 					|| (is_string($value) && $value === ''))) {
246 246
 				$errors[$colName] = [
Please login to merge, or discard this patch.
src/Traits/Password.php 1 patch
Indentation   +180 added lines, -180 removed lines patch added patch discarded remove patch
@@ -14,187 +14,187 @@
 block discarded – undo
14 14
 
15 15
 trait Password
16 16
 {
17
-	/** @var string The password hash. */
18
-	protected $password;
19
-
20
-	/** @var string|null The password reset token. */
21
-	protected $passwordResetToken;
22
-
23
-	/** @var string|null The password expiry date */
24
-	protected $passwordExpiryDate;
25
-
26
-	/**
27
-	 * this method is required to be called in the constructor for each class that uses this trait. 
28
-	 * It adds the fields necessary for the passwords struct to the table definition
29
-	 */
30
-	protected function initPassword()
31
-	{
32
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_PASSWORD, [
33
-			'value' => &$this->password,
34
-			'validate' => [$this, 'validatePassword'],
35
-			'setter' => [$this, 'setPassword'],
36
-			'type' => 'VARCHAR',
37
-			'length' => 1024,
38
-			'properties' => null
39
-		]);
40
-
41
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN, [
42
-			'value' => &$this->passwordResetToken,
43
-			'validate' => null,
44
-			'default' => 0,
45
-			'type' => 'VARCHAR',
46
-			'length' => 1024
47
-		]);
48
-
49
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN_EXPIRY, [
50
-			'value' => &$this->passwordExpiryDate,
51
-			'validate' => null,
52
-			'type' => 'DATETIME',
53
-		]);
54
-	}
55
-
56
-
57
-	/**
58
-	 * Returns whether the users password has been set
59
-	 * @return boolean true if the user has a password
60
-	 */
61
-	public function hasPasswordBeenSet()
62
-	{
63
-		return $this->password !== null;
64
-	}
65
-
66
-	/**
67
-	 * Returns true if the credentials are correct.
68
-	 *
69
-	 * @param string $password
70
-	 * @return boolean true if the credentials are correct
71
-	 */
72
-	public function isPassword($password)
73
-	{ 
74
-		if (!$this->hasPasswordBeenSet())
75
-		{
76
-			throw new ActiveRecordTraitException("Password field has not been set");
77
-		}
78
-
79
-		if (!password_verify($password, $this->password)) {
80
-			return false;
81
-		}
82
-
83
-		if (password_needs_rehash($this->password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH])) {
84
-			$this->setPassword($password)->sync();
85
-		}
86
-
87
-		return true;
88
-	}
89
-
90
-	public function validatePassword($password) {
91
-		if (strlen($password) < TRAIT_PASSWORD_MIN_LENGTH) {
92
-			$message = sprintf('\'Password\' must be atleast %s characters long. %s characters provided.', TRAIT_PASSWORD_MIN_LENGTH, strlen($password));
93
-			return [false, $message];
94
-		}
95
-		return [true, ''];
96
-	}
97
-
98
-	/**
99
-	 * Set the password.
100
-	 *
101
-	 * @param string $password
102
-	 * @return $this
103
-	 * @throws \Exception
104
-	 */
105
-	public function setPassword($password)
106
-	{
107
-		[$status, $error] = $this->validatePassword($password);
108
-		if (!$status) {
109
-			throw new ActiveRecordTraitException($error);
110
-		}
111
-
112
-		$passwordHash = \password_hash($password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH]);
113
-
114
-		if ($passwordHash === false) {
115
-			throw new ActiveRecordTraitException('\'Password\' hash failed.');
116
-		}
117
-
118
-		$this->password = $passwordHash;
119
-
120
-		return $this;
121
-	}
122
-
123
-	/**
124
-	 * @return string The Hash of the password
125
-	 */
126
-	public function getPasswordHash()
127
-	{
128
-		return $this->password;
129
-	}
130
-
131
-	/**
132
-	 * Returns the currently set password token for the entity, or null if not set
133
-	 * @return string|null The password reset token
134
-	 */
135
-	public function getPasswordResetToken()
136
-	{
137
-		return $this->passwordResetToken;
138
-	}
139
-
140
-	/**
141
-	 * Generates a new password reset token for the user
142
-	 */
143
-	public function generatePasswordResetToken()
144
-	{
145
-		$this->passwordResetToken = md5(uniqid(mt_rand(), true));
146
-
147
-		$validityDuration = new \DateInterval('PT24H');
148
-
149
-		$this->passwordExpiryDate = (new \DateTime('now'))->add($validityDuration)->format('Y-m-d H:i:s');
150
-		return $this;
151
-	}
152
-
153
-	/**
154
-	 * Clears the current password reset token
155
-	 */
156
-	public function clearPasswordResetToken()
157
-	{
158
-		$this->passwordResetToken = null;
159
-		$this->passwordExpiryDate = null;
160
-		return $this;
161
-	}
162
-
163
-	public function validatePasswordResetToken(string $token)
164
-	{
165
-		return $this->passwordResetToken !== null
166
-			&& $token === $this->passwordResetToken
167
-			&& (new \DateTime('now')) < (new \DateTime($this->passwordExpiryDate));
168
-	}
17
+    /** @var string The password hash. */
18
+    protected $password;
19
+
20
+    /** @var string|null The password reset token. */
21
+    protected $passwordResetToken;
22
+
23
+    /** @var string|null The password expiry date */
24
+    protected $passwordExpiryDate;
25
+
26
+    /**
27
+     * this method is required to be called in the constructor for each class that uses this trait. 
28
+     * It adds the fields necessary for the passwords struct to the table definition
29
+     */
30
+    protected function initPassword()
31
+    {
32
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_PASSWORD, [
33
+            'value' => &$this->password,
34
+            'validate' => [$this, 'validatePassword'],
35
+            'setter' => [$this, 'setPassword'],
36
+            'type' => 'VARCHAR',
37
+            'length' => 1024,
38
+            'properties' => null
39
+        ]);
40
+
41
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN, [
42
+            'value' => &$this->passwordResetToken,
43
+            'validate' => null,
44
+            'default' => 0,
45
+            'type' => 'VARCHAR',
46
+            'length' => 1024
47
+        ]);
48
+
49
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN_EXPIRY, [
50
+            'value' => &$this->passwordExpiryDate,
51
+            'validate' => null,
52
+            'type' => 'DATETIME',
53
+        ]);
54
+    }
55
+
56
+
57
+    /**
58
+     * Returns whether the users password has been set
59
+     * @return boolean true if the user has a password
60
+     */
61
+    public function hasPasswordBeenSet()
62
+    {
63
+        return $this->password !== null;
64
+    }
65
+
66
+    /**
67
+     * Returns true if the credentials are correct.
68
+     *
69
+     * @param string $password
70
+     * @return boolean true if the credentials are correct
71
+     */
72
+    public function isPassword($password)
73
+    { 
74
+        if (!$this->hasPasswordBeenSet())
75
+        {
76
+            throw new ActiveRecordTraitException("Password field has not been set");
77
+        }
78
+
79
+        if (!password_verify($password, $this->password)) {
80
+            return false;
81
+        }
82
+
83
+        if (password_needs_rehash($this->password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH])) {
84
+            $this->setPassword($password)->sync();
85
+        }
86
+
87
+        return true;
88
+    }
89
+
90
+    public function validatePassword($password) {
91
+        if (strlen($password) < TRAIT_PASSWORD_MIN_LENGTH) {
92
+            $message = sprintf('\'Password\' must be atleast %s characters long. %s characters provided.', TRAIT_PASSWORD_MIN_LENGTH, strlen($password));
93
+            return [false, $message];
94
+        }
95
+        return [true, ''];
96
+    }
97
+
98
+    /**
99
+     * Set the password.
100
+     *
101
+     * @param string $password
102
+     * @return $this
103
+     * @throws \Exception
104
+     */
105
+    public function setPassword($password)
106
+    {
107
+        [$status, $error] = $this->validatePassword($password);
108
+        if (!$status) {
109
+            throw new ActiveRecordTraitException($error);
110
+        }
111
+
112
+        $passwordHash = \password_hash($password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH]);
113
+
114
+        if ($passwordHash === false) {
115
+            throw new ActiveRecordTraitException('\'Password\' hash failed.');
116
+        }
117
+
118
+        $this->password = $passwordHash;
119
+
120
+        return $this;
121
+    }
122
+
123
+    /**
124
+     * @return string The Hash of the password
125
+     */
126
+    public function getPasswordHash()
127
+    {
128
+        return $this->password;
129
+    }
130
+
131
+    /**
132
+     * Returns the currently set password token for the entity, or null if not set
133
+     * @return string|null The password reset token
134
+     */
135
+    public function getPasswordResetToken()
136
+    {
137
+        return $this->passwordResetToken;
138
+    }
139
+
140
+    /**
141
+     * Generates a new password reset token for the user
142
+     */
143
+    public function generatePasswordResetToken()
144
+    {
145
+        $this->passwordResetToken = md5(uniqid(mt_rand(), true));
146
+
147
+        $validityDuration = new \DateInterval('PT24H');
148
+
149
+        $this->passwordExpiryDate = (new \DateTime('now'))->add($validityDuration)->format('Y-m-d H:i:s');
150
+        return $this;
151
+    }
152
+
153
+    /**
154
+     * Clears the current password reset token
155
+     */
156
+    public function clearPasswordResetToken()
157
+    {
158
+        $this->passwordResetToken = null;
159
+        $this->passwordExpiryDate = null;
160
+        return $this;
161
+    }
162
+
163
+    public function validatePasswordResetToken(string $token)
164
+    {
165
+        return $this->passwordResetToken !== null
166
+            && $token === $this->passwordResetToken
167
+            && (new \DateTime('now')) < (new \DateTime($this->passwordExpiryDate));
168
+    }
169 169
 	
170
-	/**
171
-	 * @return void
172
-	 */
173
-	abstract protected function extendTableDefinition($columnName, $definition);
170
+    /**
171
+     * @return void
172
+     */
173
+    abstract protected function extendTableDefinition($columnName, $definition);
174 174
 	
175
-	/**
176
-	 * @return void
177
-	 */
178
-	abstract protected function registerSearchHook($columnName, $fn);
179
-
180
-	/**
181
-	 * @return void
182
-	 */
183
-	abstract protected function registerDeleteHook($columnName, $fn);
184
-
185
-	/**
186
-	 * @return void
187
-	 */
188
-	abstract protected function registerUpdateHook($columnName, $fn);
189
-
190
-	/**
191
-	 * @return void
192
-	 */
193
-	abstract protected function registerReadHook($columnName, $fn);
194
-
195
-	/**
196
-	 * @return void
197
-	 */
198
-	abstract protected function registerCreateHook($columnName, $fn);
175
+    /**
176
+     * @return void
177
+     */
178
+    abstract protected function registerSearchHook($columnName, $fn);
179
+
180
+    /**
181
+     * @return void
182
+     */
183
+    abstract protected function registerDeleteHook($columnName, $fn);
184
+
185
+    /**
186
+     * @return void
187
+     */
188
+    abstract protected function registerUpdateHook($columnName, $fn);
189
+
190
+    /**
191
+     * @return void
192
+     */
193
+    abstract protected function registerReadHook($columnName, $fn);
194
+
195
+    /**
196
+     * @return void
197
+     */
198
+    abstract protected function registerCreateHook($columnName, $fn);
199 199
 
200 200
 }
201 201
\ No newline at end of file
Please login to merge, or discard this patch.
src/AbstractActiveRecord.php 1 patch
Indentation   +508 added lines, -508 removed lines patch added patch discarded remove patch
@@ -18,513 +18,513 @@
 block discarded – undo
18 18
  */
19 19
 abstract class AbstractActiveRecord implements ActiveRecordInterface
20 20
 {
21
-	const COLUMN_NAME_ID = 'id';
22
-	const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
-
24
-	const CREATE = 'CREATE';
25
-	const READ = 'READ';
26
-	const UPDATE = 'UPDATE';
27
-	const DELETE = 'DELETE';
28
-	const SEARCH = 'SEARCH';
29
-
30
-	/** @var \PDO The PDO object. */
31
-	protected $pdo;
32
-
33
-	/** @var null|int The ID. */
34
-	private $id;
35
-
36
-	/** @var array A map of column name to functions that hook the insert function */
37
-	protected $createHooks;
38
-
39
-	/** @var array A map of column name to functions that hook the read function */
40
-	protected $readHooks;
41
-
42
-	/** @var array A map of column name to functions that hook the update function */
43
-	protected $updateHooks;
44
-
45
-	/** @var array A map of column name to functions that hook the update function */
46
-	protected $deleteHooks;	
47
-
48
-	/** @var array A map of column name to functions that hook the search function */
49
-	protected $searchHooks;
50
-
51
-	/** @var array A list of table column definitions */
52
-	protected $tableDefinition;
53
-
54
-	/**
55
-	 * Construct an abstract active record with the given PDO.
56
-	 *
57
-	 * @param \PDO $pdo
58
-	 */
59
-	public function __construct(\PDO $pdo)
60
-	{
61
-		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
62
-		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
63
-
64
-		$this->setPdo($pdo);
65
-
66
-		$this->createHooks = [];
67
-		$this->readHooks = [];
68
-		$this->updateHooks = [];
69
-		$this->deleteHooks = [];
70
-		$this->searchHooks = [];
71
-		$this->tableDefinition = $this->getTableDefinition();
72
-
73
-		// Extend table definition with default ID field, throw exception if field already exists
74
-		if (array_key_exists('id', $this->tableDefinition)) {
75
-			$message = "Table definition in record contains a field with name \"id\"";
76
-			$message .= ", which is a reserved name by ActiveRecord";
77
-			throw new ActiveRecordException($message, 0);
78
-		}
79
-
80
-		$this->tableDefinition[self::COLUMN_NAME_ID] =
81
-		[
82
-			'value' => &$this->id,
83
-			'validate' => null,
84
-			'type' => self::COLUMN_TYPE_ID,
85
-			'properties' =>
86
-				ColumnProperty::NOT_NULL
87
-				| ColumnProperty::IMMUTABLE
88
-				| ColumnProperty::AUTO_INCREMENT
89
-				| ColumnProperty::PRIMARY_KEY
90
-		];
91
-	}
92
-
93
-	private function checkHookConstraints($columnName, $hookMap)
94
-	{
95
-		// Check whether column exists
96
-		if (!array_key_exists($columnName, $this->tableDefinition)) 
97
-		{
98
-			throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
99
-		}
100
-
101
-		// Enforcing 1 hook per table column
102
-		if (array_key_exists($columnName, $hookMap)) {
103
-			$message = "Hook is trying to register on an already registered column \"$columnName\", ";
104
-			$message .= "do you have conflicting traits?";
105
-			throw new ActiveRecordException($message, 0);
106
-		}
107
-	}
108
-
109
-	public function registerHookOnAction($actionName, $columnName, $fn)
110
-	{
111
-		if (is_string($fn) && is_callable([$this, $fn])) {
112
-			$fn = [$this, $fn];
113
-		}
114
-
115
-		if (!is_callable($fn)) { 
116
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
117
-		}
118
-
119
-		switch ($actionName) {
120
-			case self::CREATE:
121
-				$this->checkHookConstraints($columnName, $this->createHooks);
122
-				$this->createHooks[$columnName] = $fn;
123
-				break;
124
-			case self::READ:
125
-				$this->checkHookConstraints($columnName, $this->readHooks);
126
-				$this->readHooks[$columnName] = $fn;
127
-				break;
128
-			case self::UPDATE:
129
-				$this->checkHookConstraints($columnName, $this->updateHooks);
130
-				$this->updateHooks[$columnName] = $fn;
131
-				break;
132
-			case self::DELETE:
133
-				$this->checkHookConstraints($columnName, $this->deleteHooks);
134
-				$this->deleteHooks[$columnName] = $fn;
135
-				break;
136
-			case self::SEARCH:
137
-				$this->checkHookConstraints($columnName, $this->searchHooks);
138
-				$this->searchHooks[$columnName] = $fn;
139
-				break;
140
-			default:
141
-				throw new ActiveRecordException("Invalid action: Can not register hook on non-existing action");
142
-		}
143
-	}
144
-
145
-	/**
146
-	 * Register a new hook for a specific column that gets called before execution of the create() method
147
-	 * Only one hook per column can be registered at a time
148
-	 * @param string $columnName The name of the column that is registered.
149
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
150
-	 */
151
-	public function registerCreateHook($columnName, $fn)
152
-	{
153
-		$this->registerHookOnAction(self::CREATE, $columnName, $fn);
154
-	}
155
-
156
-	/**
157
-	 * Register a new hook for a specific column that gets called before execution of the read() method
158
-	 * Only one hook per column can be registered at a time
159
-	 * @param string $columnName The name of the column that is registered.
160
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
-	 */
162
-	public function registerReadHook($columnName, $fn)
163
-	{
164
-		$this->registerHookOnAction(self::READ, $columnName, $fn);
165
-	}
166
-
167
-	/**
168
-	 * Register a new hook for a specific column that gets called before execution of the update() method
169
-	 * Only one hook per column can be registered at a time
170
-	 * @param string $columnName The name of the column that is registered.
171
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
172
-	 */
173
-	public function registerUpdateHook($columnName, $fn)
174
-	{
175
-		$this->registerHookOnAction(self::UPDATE, $columnName, $fn);
176
-	}
177
-
178
-	/**
179
-	 * Register a new hook for a specific column that gets called before execution of the delete() method
180
-	 * Only one hook per column can be registered at a time
181
-	 * @param string $columnName The name of the column that is registered.
182
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
183
-	 */
184
-	public function registerDeleteHook($columnName, $fn)
185
-	{
186
-		$this->registerHookOnAction(self::DELETE, $columnName, $fn);
187
-	}
188
-
189
-	/**
190
-	 * Register a new hook for a specific column that gets called before execution of the search() method
191
-	 * Only one hook per column can be registered at a time
192
-	 * @param string $columnName The name of the column that is registered.
193
-	 * @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; 
194
-	 */
195
-	public function registerSearchHook($columnName, $fn)
196
-	{
197
-		$this->registerHookOnAction(self::SEARCH, $columnName, $fn);
198
-	}
199
-
200
-	/**
201
-	 * Adds a new column definition to the table.
202
-	 * @param string $columnName The name of the column that is registered.
203
-	 * @param Array $definition The definition of that column.
204
-	 */
205
-	protected function extendTableDefinition($columnName, $definition)
206
-	{
207
-		if ($this->tableDefinition === null) {
208
-			throw new ActiveRecordException("tableDefinition is null, has parent been initialized in constructor?");
209
-		}
210
-
211
-		// Enforcing table can only be extended with new columns
212
-		if (array_key_exists($columnName, $this->tableDefinition)) {
213
-			$message = "Table is being extended with a column that already exists, ";
214
-			$message .= "\"$columnName\" conflicts with your table definition";
215
-			throw new ActiveRecordException($message, 0);
216
-		}
217
-
218
-		$this->tableDefinition[$columnName] = $definition;
219
-	}
220
-
221
-	/**
222
-	 * Creates the entity as a table in the database
223
-	 */
224
-	public function createTable()
225
-	{
226
-		$this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
227
-	}
228
-
229
-	/**
230
-	 * Iterates over the specified constraints in the table definition, 
231
-	 * 		and applies these to the database.
232
-	 */
233
-	public function createTableConstraints()
234
-	{
235
-		// Iterate over columns, check whether "relation" field exists, if so create constraint
236
-		foreach ($this->tableDefinition as $colName => $definition) {
237
-			if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
238
-				// Forge new relation
239
-				$target = $definition['relation'];
240
-				$properties = $definition['properties'] ?? 0;
241
-
242
-				if ($properties & ColumnProperty::NOT_NULL) {
243
-					$constraintSql = SchemaBuilder::buildConstraintOnDeleteCascade($target->getTableName(), 'id', $this->getTableName(), $colName);
244
-				} else {
245
-					$constraintSql = SchemaBuilder::buildConstraintOnDeleteSetNull($target->getTableName(), 'id', $this->getTableName(), $colName);
246
-				}
247
-
248
-				$this->pdo->query($constraintSql);
249
-			} else if (isset($definition['relation'])) {
250
-				$msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
251
-					$colName,
252
-					$this->getTableName());
253
-				throw new ActiveRecordException($msg);
254
-			}
255
-		}
256
-	}
257
-
258
-	/**
259
-	 * Returns the name -> variable mapping for the table definition.
260
-	 * @return Array The mapping
261
-	 */
262
-	protected function getActiveRecordColumns()
263
-	{
264
-		$bindings = [];
265
-		foreach ($this->tableDefinition as $colName => $definition) {
266
-
267
-			// Ignore the id column (key) when inserting or updating
268
-			if ($colName == self::COLUMN_NAME_ID) {
269
-				continue;
270
-			}
271
-
272
-			$bindings[$colName] = &$definition['value'];
273
-		}
274
-		return $bindings;
275
-	}
276
-
277
-	protected function insertDefaults()
278
-	{
279
-		// Insert default values for not-null fields
280
-		foreach ($this->tableDefinition as $colName => $colDef) {
281
-			if ($colDef['value'] === null
282
-				&& ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
283
-				&& isset($colDef['default'])) {
284
-				$this->tableDefinition[$colName]['value'] = $colDef['default'];
285
-			}
286
-		}		
287
-	}
288
-
289
-	/**
290
-	 * {@inheritdoc}
291
-	 */
292
-	public function create()
293
-	{
294
-		foreach ($this->createHooks as $colName => $fn) {
295
-			$fn();
296
-		}
297
-
298
-		$this->insertDefaults();
299
-
300
-		try {
301
-			(new Query($this->getPdo(), $this->getTableName()))
302
-				->insert($this->getActiveRecordColumns())
303
-				->execute();
304
-
305
-			$this->setId(intval($this->getPdo()->lastInsertId()));
306
-		} catch (\PDOException $e) {
307
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
308
-		}
309
-
310
-		return $this;
311
-	}
312
-
313
-	/**
314
-	 * {@inheritdoc}
315
-	 */
316
-	public function read($id)
317
-	{
318
-		$whereConditions = [
319
-			Query::Equal('id', $id)
320
-		];
321
-		foreach ($this->readHooks as $colName => $fn) {
322
-			$cond = $fn();
323
-			if ($cond !== null) {
324
-				$whereConditions[] = $cond;
325
-			}
326
-		}
327
-
328
-		try {
329
-			$row = (new Query($this->getPdo(), $this->getTableName()))
330
-				->select()
331
-				->where(Query::AndArray($whereConditions))
332
-				->execute()
333
-				->fetch();
21
+    const COLUMN_NAME_ID = 'id';
22
+    const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
+
24
+    const CREATE = 'CREATE';
25
+    const READ = 'READ';
26
+    const UPDATE = 'UPDATE';
27
+    const DELETE = 'DELETE';
28
+    const SEARCH = 'SEARCH';
29
+
30
+    /** @var \PDO The PDO object. */
31
+    protected $pdo;
32
+
33
+    /** @var null|int The ID. */
34
+    private $id;
35
+
36
+    /** @var array A map of column name to functions that hook the insert function */
37
+    protected $createHooks;
38
+
39
+    /** @var array A map of column name to functions that hook the read function */
40
+    protected $readHooks;
41
+
42
+    /** @var array A map of column name to functions that hook the update function */
43
+    protected $updateHooks;
44
+
45
+    /** @var array A map of column name to functions that hook the update function */
46
+    protected $deleteHooks;	
47
+
48
+    /** @var array A map of column name to functions that hook the search function */
49
+    protected $searchHooks;
50
+
51
+    /** @var array A list of table column definitions */
52
+    protected $tableDefinition;
53
+
54
+    /**
55
+     * Construct an abstract active record with the given PDO.
56
+     *
57
+     * @param \PDO $pdo
58
+     */
59
+    public function __construct(\PDO $pdo)
60
+    {
61
+        $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
62
+        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
63
+
64
+        $this->setPdo($pdo);
65
+
66
+        $this->createHooks = [];
67
+        $this->readHooks = [];
68
+        $this->updateHooks = [];
69
+        $this->deleteHooks = [];
70
+        $this->searchHooks = [];
71
+        $this->tableDefinition = $this->getTableDefinition();
72
+
73
+        // Extend table definition with default ID field, throw exception if field already exists
74
+        if (array_key_exists('id', $this->tableDefinition)) {
75
+            $message = "Table definition in record contains a field with name \"id\"";
76
+            $message .= ", which is a reserved name by ActiveRecord";
77
+            throw new ActiveRecordException($message, 0);
78
+        }
79
+
80
+        $this->tableDefinition[self::COLUMN_NAME_ID] =
81
+        [
82
+            'value' => &$this->id,
83
+            'validate' => null,
84
+            'type' => self::COLUMN_TYPE_ID,
85
+            'properties' =>
86
+                ColumnProperty::NOT_NULL
87
+                | ColumnProperty::IMMUTABLE
88
+                | ColumnProperty::AUTO_INCREMENT
89
+                | ColumnProperty::PRIMARY_KEY
90
+        ];
91
+    }
92
+
93
+    private function checkHookConstraints($columnName, $hookMap)
94
+    {
95
+        // Check whether column exists
96
+        if (!array_key_exists($columnName, $this->tableDefinition)) 
97
+        {
98
+            throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
99
+        }
100
+
101
+        // Enforcing 1 hook per table column
102
+        if (array_key_exists($columnName, $hookMap)) {
103
+            $message = "Hook is trying to register on an already registered column \"$columnName\", ";
104
+            $message .= "do you have conflicting traits?";
105
+            throw new ActiveRecordException($message, 0);
106
+        }
107
+    }
108
+
109
+    public function registerHookOnAction($actionName, $columnName, $fn)
110
+    {
111
+        if (is_string($fn) && is_callable([$this, $fn])) {
112
+            $fn = [$this, $fn];
113
+        }
114
+
115
+        if (!is_callable($fn)) { 
116
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
117
+        }
118
+
119
+        switch ($actionName) {
120
+            case self::CREATE:
121
+                $this->checkHookConstraints($columnName, $this->createHooks);
122
+                $this->createHooks[$columnName] = $fn;
123
+                break;
124
+            case self::READ:
125
+                $this->checkHookConstraints($columnName, $this->readHooks);
126
+                $this->readHooks[$columnName] = $fn;
127
+                break;
128
+            case self::UPDATE:
129
+                $this->checkHookConstraints($columnName, $this->updateHooks);
130
+                $this->updateHooks[$columnName] = $fn;
131
+                break;
132
+            case self::DELETE:
133
+                $this->checkHookConstraints($columnName, $this->deleteHooks);
134
+                $this->deleteHooks[$columnName] = $fn;
135
+                break;
136
+            case self::SEARCH:
137
+                $this->checkHookConstraints($columnName, $this->searchHooks);
138
+                $this->searchHooks[$columnName] = $fn;
139
+                break;
140
+            default:
141
+                throw new ActiveRecordException("Invalid action: Can not register hook on non-existing action");
142
+        }
143
+    }
144
+
145
+    /**
146
+     * Register a new hook for a specific column that gets called before execution of the create() method
147
+     * Only one hook per column can be registered at a time
148
+     * @param string $columnName The name of the column that is registered.
149
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
150
+     */
151
+    public function registerCreateHook($columnName, $fn)
152
+    {
153
+        $this->registerHookOnAction(self::CREATE, $columnName, $fn);
154
+    }
155
+
156
+    /**
157
+     * Register a new hook for a specific column that gets called before execution of the read() method
158
+     * Only one hook per column can be registered at a time
159
+     * @param string $columnName The name of the column that is registered.
160
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
+     */
162
+    public function registerReadHook($columnName, $fn)
163
+    {
164
+        $this->registerHookOnAction(self::READ, $columnName, $fn);
165
+    }
166
+
167
+    /**
168
+     * Register a new hook for a specific column that gets called before execution of the update() method
169
+     * Only one hook per column can be registered at a time
170
+     * @param string $columnName The name of the column that is registered.
171
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
172
+     */
173
+    public function registerUpdateHook($columnName, $fn)
174
+    {
175
+        $this->registerHookOnAction(self::UPDATE, $columnName, $fn);
176
+    }
177
+
178
+    /**
179
+     * Register a new hook for a specific column that gets called before execution of the delete() method
180
+     * Only one hook per column can be registered at a time
181
+     * @param string $columnName The name of the column that is registered.
182
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
183
+     */
184
+    public function registerDeleteHook($columnName, $fn)
185
+    {
186
+        $this->registerHookOnAction(self::DELETE, $columnName, $fn);
187
+    }
188
+
189
+    /**
190
+     * Register a new hook for a specific column that gets called before execution of the search() method
191
+     * Only one hook per column can be registered at a time
192
+     * @param string $columnName The name of the column that is registered.
193
+     * @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; 
194
+     */
195
+    public function registerSearchHook($columnName, $fn)
196
+    {
197
+        $this->registerHookOnAction(self::SEARCH, $columnName, $fn);
198
+    }
199
+
200
+    /**
201
+     * Adds a new column definition to the table.
202
+     * @param string $columnName The name of the column that is registered.
203
+     * @param Array $definition The definition of that column.
204
+     */
205
+    protected function extendTableDefinition($columnName, $definition)
206
+    {
207
+        if ($this->tableDefinition === null) {
208
+            throw new ActiveRecordException("tableDefinition is null, has parent been initialized in constructor?");
209
+        }
210
+
211
+        // Enforcing table can only be extended with new columns
212
+        if (array_key_exists($columnName, $this->tableDefinition)) {
213
+            $message = "Table is being extended with a column that already exists, ";
214
+            $message .= "\"$columnName\" conflicts with your table definition";
215
+            throw new ActiveRecordException($message, 0);
216
+        }
217
+
218
+        $this->tableDefinition[$columnName] = $definition;
219
+    }
220
+
221
+    /**
222
+     * Creates the entity as a table in the database
223
+     */
224
+    public function createTable()
225
+    {
226
+        $this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
227
+    }
228
+
229
+    /**
230
+     * Iterates over the specified constraints in the table definition, 
231
+     * 		and applies these to the database.
232
+     */
233
+    public function createTableConstraints()
234
+    {
235
+        // Iterate over columns, check whether "relation" field exists, if so create constraint
236
+        foreach ($this->tableDefinition as $colName => $definition) {
237
+            if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
238
+                // Forge new relation
239
+                $target = $definition['relation'];
240
+                $properties = $definition['properties'] ?? 0;
241
+
242
+                if ($properties & ColumnProperty::NOT_NULL) {
243
+                    $constraintSql = SchemaBuilder::buildConstraintOnDeleteCascade($target->getTableName(), 'id', $this->getTableName(), $colName);
244
+                } else {
245
+                    $constraintSql = SchemaBuilder::buildConstraintOnDeleteSetNull($target->getTableName(), 'id', $this->getTableName(), $colName);
246
+                }
247
+
248
+                $this->pdo->query($constraintSql);
249
+            } else if (isset($definition['relation'])) {
250
+                $msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
251
+                    $colName,
252
+                    $this->getTableName());
253
+                throw new ActiveRecordException($msg);
254
+            }
255
+        }
256
+    }
257
+
258
+    /**
259
+     * Returns the name -> variable mapping for the table definition.
260
+     * @return Array The mapping
261
+     */
262
+    protected function getActiveRecordColumns()
263
+    {
264
+        $bindings = [];
265
+        foreach ($this->tableDefinition as $colName => $definition) {
266
+
267
+            // Ignore the id column (key) when inserting or updating
268
+            if ($colName == self::COLUMN_NAME_ID) {
269
+                continue;
270
+            }
271
+
272
+            $bindings[$colName] = &$definition['value'];
273
+        }
274
+        return $bindings;
275
+    }
276
+
277
+    protected function insertDefaults()
278
+    {
279
+        // Insert default values for not-null fields
280
+        foreach ($this->tableDefinition as $colName => $colDef) {
281
+            if ($colDef['value'] === null
282
+                && ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
283
+                && isset($colDef['default'])) {
284
+                $this->tableDefinition[$colName]['value'] = $colDef['default'];
285
+            }
286
+        }		
287
+    }
288
+
289
+    /**
290
+     * {@inheritdoc}
291
+     */
292
+    public function create()
293
+    {
294
+        foreach ($this->createHooks as $colName => $fn) {
295
+            $fn();
296
+        }
297
+
298
+        $this->insertDefaults();
299
+
300
+        try {
301
+            (new Query($this->getPdo(), $this->getTableName()))
302
+                ->insert($this->getActiveRecordColumns())
303
+                ->execute();
304
+
305
+            $this->setId(intval($this->getPdo()->lastInsertId()));
306
+        } catch (\PDOException $e) {
307
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
308
+        }
309
+
310
+        return $this;
311
+    }
312
+
313
+    /**
314
+     * {@inheritdoc}
315
+     */
316
+    public function read($id)
317
+    {
318
+        $whereConditions = [
319
+            Query::Equal('id', $id)
320
+        ];
321
+        foreach ($this->readHooks as $colName => $fn) {
322
+            $cond = $fn();
323
+            if ($cond !== null) {
324
+                $whereConditions[] = $cond;
325
+            }
326
+        }
327
+
328
+        try {
329
+            $row = (new Query($this->getPdo(), $this->getTableName()))
330
+                ->select()
331
+                ->where(Query::AndArray($whereConditions))
332
+                ->execute()
333
+                ->fetch();
334 334
 			
335
-			if ($row === false) {
336
-				$msg = sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName());
337
-				throw new ActiveRecordException($msg, ActiveRecordException::NOT_FOUND);
338
-			}
339
-
340
-			$this->fill($row)->setId($id);
341
-		} catch (\PDOException $e) {
342
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
343
-		}
344
-
345
-		return $this;
346
-	}
347
-
348
-	/**
349
-	 * {@inheritdoc}
350
-	 */
351
-	public function update()
352
-	{
353
-		foreach ($this->updateHooks as $colName => $fn) {
354
-			$fn();
355
-		}
356
-
357
-		try {
358
-			(new Query($this->getPdo(), $this->getTableName()))
359
-				->update($this->getActiveRecordColumns())
360
-				->where(Query::Equal('id', $this->getId()))
361
-				->execute();
362
-		} catch (\PDOException $e) {
363
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
364
-		}
365
-
366
-		return $this;
367
-	}
368
-
369
-	/**
370
-	 * {@inheritdoc}
371
-	 */
372
-	public function delete()
373
-	{
374
-		foreach ($this->deleteHooks as $colName => $fn) {
375
-			$fn();
376
-		}
377
-
378
-		try {
379
-			(new Query($this->getPdo(), $this->getTableName()))
380
-				->delete()
381
-				->where(Query::Equal('id', $this->getId()))
382
-				->execute();
383
-
384
-			$this->setId(null);
385
-		} catch (\PDOException $e) {
386
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
387
-		}
388
-
389
-		return $this;
390
-	}
391
-
392
-	/**
393
-	 * {@inheritdoc}
394
-	 */
395
-	public function sync()
396
-	{
397
-		if (!$this->exists()) {
398
-			return $this->create();
399
-		}
400
-
401
-		return $this->update();
402
-	}
403
-
404
-	/**
405
-	 * {@inheritdoc}
406
-	 */
407
-	public function exists()
408
-	{
409
-		return $this->getId() !== null;
410
-	}
411
-
412
-	/**
413
-	 * {@inheritdoc}
414
-	 */
415
-	public function fill(array $attributes)
416
-	{
417
-		$columns = $this->getActiveRecordColumns();
418
-		$columns['id'] = &$this->id;
419
-
420
-		foreach ($attributes as $key => $value) {
421
-			if (array_key_exists($key, $columns)) {
422
-				$columns[$key] = $value;
423
-			}
424
-		}
425
-
426
-		return $this;
427
-	}
428
-
429
-	/**
430
-	 * Returns the serialized form of the specified columns
431
-	 * 
432
-	 * @return Array
433
-	 */
434
-	public function toArray(Array $fieldWhitelist)
435
-	{
436
-		$output = [];
437
-		foreach ($this->tableDefinition as $colName => $definition) {
438
-			if (in_array($colName, $fieldWhitelist)) {
439
-				$output[$colName] = $definition['value'];
440
-			}
441
-		}
442
-
443
-		return $output;
444
-	}
445
-
446
-	/**
447
-	 * {@inheritdoc}
448
-	 */
449
-	public function search(array $ignoredTraits = [])
450
-	{
451
-		$clauses = [];
452
-		foreach ($this->searchHooks as $column => $fn) {
453
-			if (!in_array($column, $ignoredTraits)) {
454
-				$clauses[] = $fn();
455
-			}
456
-		}
457
-
458
-		return new ActiveRecordQuery($this, $clauses);
459
-	}
460
-
461
-	/**
462
-	 * Returns the PDO.
463
-	 *
464
-	 * @return \PDO the PDO.
465
-	 */
466
-	public function getPdo()
467
-	{
468
-		return $this->pdo;
469
-	}
470
-
471
-	/**
472
-	 * Set the PDO.
473
-	 *
474
-	 * @param \PDO $pdo
475
-	 * @return $this
476
-	 */
477
-	protected function setPdo($pdo)
478
-	{
479
-		$this->pdo = $pdo;
480
-
481
-		return $this;
482
-	}
483
-
484
-	/**
485
-	 * Returns the ID.
486
-	 *
487
-	 * @return null|int The ID.
488
-	 */
489
-	public function getId()
490
-	{
491
-		return $this->id;
492
-	}
493
-
494
-	/**
495
-	 * Set the ID.
496
-	 *
497
-	 * @param int $id
498
-	 * @return $this
499
-	 */
500
-	protected function setId($id)
501
-	{
502
-		$this->id = $id;
503
-
504
-		return $this;
505
-	}
506
-
507
-	public function getFinalTableDefinition()
508
-	{
509
-		return $this->tableDefinition;
510
-	}
511
-
512
-	public function newInstance()
513
-	{
514
-		return new static($this->pdo);
515
-	}
516
-
517
-	/**
518
-	 * Returns the active record table.
519
-	 *
520
-	 * @return string the active record table name.
521
-	 */
522
-	abstract public function getTableName(): string;
523
-
524
-	/**
525
-	 * Returns the active record columns.
526
-	 *
527
-	 * @return array the active record columns.
528
-	 */
529
-	abstract protected function getTableDefinition(): Array;
335
+            if ($row === false) {
336
+                $msg = sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName());
337
+                throw new ActiveRecordException($msg, ActiveRecordException::NOT_FOUND);
338
+            }
339
+
340
+            $this->fill($row)->setId($id);
341
+        } catch (\PDOException $e) {
342
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
343
+        }
344
+
345
+        return $this;
346
+    }
347
+
348
+    /**
349
+     * {@inheritdoc}
350
+     */
351
+    public function update()
352
+    {
353
+        foreach ($this->updateHooks as $colName => $fn) {
354
+            $fn();
355
+        }
356
+
357
+        try {
358
+            (new Query($this->getPdo(), $this->getTableName()))
359
+                ->update($this->getActiveRecordColumns())
360
+                ->where(Query::Equal('id', $this->getId()))
361
+                ->execute();
362
+        } catch (\PDOException $e) {
363
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
364
+        }
365
+
366
+        return $this;
367
+    }
368
+
369
+    /**
370
+     * {@inheritdoc}
371
+     */
372
+    public function delete()
373
+    {
374
+        foreach ($this->deleteHooks as $colName => $fn) {
375
+            $fn();
376
+        }
377
+
378
+        try {
379
+            (new Query($this->getPdo(), $this->getTableName()))
380
+                ->delete()
381
+                ->where(Query::Equal('id', $this->getId()))
382
+                ->execute();
383
+
384
+            $this->setId(null);
385
+        } catch (\PDOException $e) {
386
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
387
+        }
388
+
389
+        return $this;
390
+    }
391
+
392
+    /**
393
+     * {@inheritdoc}
394
+     */
395
+    public function sync()
396
+    {
397
+        if (!$this->exists()) {
398
+            return $this->create();
399
+        }
400
+
401
+        return $this->update();
402
+    }
403
+
404
+    /**
405
+     * {@inheritdoc}
406
+     */
407
+    public function exists()
408
+    {
409
+        return $this->getId() !== null;
410
+    }
411
+
412
+    /**
413
+     * {@inheritdoc}
414
+     */
415
+    public function fill(array $attributes)
416
+    {
417
+        $columns = $this->getActiveRecordColumns();
418
+        $columns['id'] = &$this->id;
419
+
420
+        foreach ($attributes as $key => $value) {
421
+            if (array_key_exists($key, $columns)) {
422
+                $columns[$key] = $value;
423
+            }
424
+        }
425
+
426
+        return $this;
427
+    }
428
+
429
+    /**
430
+     * Returns the serialized form of the specified columns
431
+     * 
432
+     * @return Array
433
+     */
434
+    public function toArray(Array $fieldWhitelist)
435
+    {
436
+        $output = [];
437
+        foreach ($this->tableDefinition as $colName => $definition) {
438
+            if (in_array($colName, $fieldWhitelist)) {
439
+                $output[$colName] = $definition['value'];
440
+            }
441
+        }
442
+
443
+        return $output;
444
+    }
445
+
446
+    /**
447
+     * {@inheritdoc}
448
+     */
449
+    public function search(array $ignoredTraits = [])
450
+    {
451
+        $clauses = [];
452
+        foreach ($this->searchHooks as $column => $fn) {
453
+            if (!in_array($column, $ignoredTraits)) {
454
+                $clauses[] = $fn();
455
+            }
456
+        }
457
+
458
+        return new ActiveRecordQuery($this, $clauses);
459
+    }
460
+
461
+    /**
462
+     * Returns the PDO.
463
+     *
464
+     * @return \PDO the PDO.
465
+     */
466
+    public function getPdo()
467
+    {
468
+        return $this->pdo;
469
+    }
470
+
471
+    /**
472
+     * Set the PDO.
473
+     *
474
+     * @param \PDO $pdo
475
+     * @return $this
476
+     */
477
+    protected function setPdo($pdo)
478
+    {
479
+        $this->pdo = $pdo;
480
+
481
+        return $this;
482
+    }
483
+
484
+    /**
485
+     * Returns the ID.
486
+     *
487
+     * @return null|int The ID.
488
+     */
489
+    public function getId()
490
+    {
491
+        return $this->id;
492
+    }
493
+
494
+    /**
495
+     * Set the ID.
496
+     *
497
+     * @param int $id
498
+     * @return $this
499
+     */
500
+    protected function setId($id)
501
+    {
502
+        $this->id = $id;
503
+
504
+        return $this;
505
+    }
506
+
507
+    public function getFinalTableDefinition()
508
+    {
509
+        return $this->tableDefinition;
510
+    }
511
+
512
+    public function newInstance()
513
+    {
514
+        return new static($this->pdo);
515
+    }
516
+
517
+    /**
518
+     * Returns the active record table.
519
+     *
520
+     * @return string the active record table name.
521
+     */
522
+    abstract public function getTableName(): string;
523
+
524
+    /**
525
+     * Returns the active record columns.
526
+     *
527
+     * @return array the active record columns.
528
+     */
529
+    abstract protected function getTableDefinition(): Array;
530 530
 }
Please login to merge, or discard this patch.
src/ActiveRecordException.php 1 patch
Indentation   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -16,6 +16,6 @@
 block discarded – undo
16 16
  */
17 17
 class ActiveRecordException extends \RuntimeException
18 18
 {
19
-	const NOT_FOUND = 1;
20
-	const DB_ERROR = 2;
19
+    const NOT_FOUND = 1;
20
+    const DB_ERROR = 2;
21 21
 }
Please login to merge, or discard this patch.