Passed
Push — v2 ( e57a7e...6c89a0 )
by Berend
04:02
created
src/Traits/AutoApi.php 1 patch
Indentation   +360 added lines, -360 removed lines patch added patch discarded remove patch
@@ -9,379 +9,379 @@
 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 $registeredCreateHooks;
16
+    /** @var array A map of column name to functions that hook the insert function */
17
+    protected $registeredCreateHooks;
18 18
 
19
-	/** @var array A map of column name to functions that hook the read function */
20
-	protected $registeredReadHooks;
19
+    /** @var array A map of column name to functions that hook the read function */
20
+    protected $registeredReadHooks;
21 21
 
22
-	/** @var array A map of column name to functions that hook the update function */
23
-	protected $registeredUpdateHooks;
22
+    /** @var array A map of column name to functions that hook the update function */
23
+    protected $registeredUpdateHooks;
24 24
 
25
-	/** @var array A map of column name to functions that hook the update function */
26
-	protected $registeredDeleteHooks;	
25
+    /** @var array A map of column name to functions that hook the update function */
26
+    protected $registeredDeleteHooks;	
27 27
 
28
-	/** @var array A map of column name to functions that hook the search function */
29
-	protected $registeredSearchHooks;
28
+    /** @var array A map of column name to functions that hook the search function */
29
+    protected $registeredSearchHooks;
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
-	public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null)
42
-	{
43
-		$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
+    public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null)
42
+    {
43
+        $query = $this->search();
44 44
 
45
-		// Build query
46
-		$orderColumn = $queryParams['search_order_by'] ?? null;
47
-		$orderDirection = $queryParams['search_order_direction'] ?? null;
48
-		if ($orderColumn !== null) {
49
-			$query->orderBy($orderColumn, $orderDirection);
50
-		}
45
+        // Build query
46
+        $orderColumn = $queryParams['search_order_by'] ?? null;
47
+        $orderDirection = $queryParams['search_order_direction'] ?? null;
48
+        if ($orderColumn !== null) {
49
+            $query->orderBy($orderColumn, $orderDirection);
50
+        }
51 51
 		
52
-		$limit = $queryParams['search_limit'] ?? null;
53
-		if ($limit !== null) {
54
-			$query->limit($limit);
55
-		}
56
-
57
-		$offset = $queryParams['search_offset'] ?? null;
58
-		if ($offset !== null) {
59
-			$query->offset($offset);
60
-		}
61
-
62
-		if ($whereClause !== null) {
63
-			$query->where($whereClause);
64
-		}
65
-
66
-		// Fetch results
67
-		$results = $query->fetchAll();
68
-
69
-		$resultsArray = [];
70
-		foreach ($results as $result) {
71
-			$resultsArray[] = $result->toArray($fieldWhitelist);
72
-		}
73
-
74
-		return $resultsArray;
75
-	}
76
-
77
-	public function toArray($fieldWhitelist)
78
-	{
79
-		$output = [];
80
-		foreach ($this->tableDefinition as $colName => $definition) {
81
-			if (in_array($colName, $fieldWhitelist)) {
82
-				$output[$colName] = $definition['value'];
83
-			}
84
-		}
85
-
86
-		return $output;
87
-	}
88
-
89
-	public function apiRead($id, Array $fieldWhitelist)
90
-	{
91
-		$this->read($id);
92
-		return $this->toArray($fieldWhitelist);
93
-	}
94
-
95
-	/* =============================================================
52
+        $limit = $queryParams['search_limit'] ?? null;
53
+        if ($limit !== null) {
54
+            $query->limit($limit);
55
+        }
56
+
57
+        $offset = $queryParams['search_offset'] ?? null;
58
+        if ($offset !== null) {
59
+            $query->offset($offset);
60
+        }
61
+
62
+        if ($whereClause !== null) {
63
+            $query->where($whereClause);
64
+        }
65
+
66
+        // Fetch results
67
+        $results = $query->fetchAll();
68
+
69
+        $resultsArray = [];
70
+        foreach ($results as $result) {
71
+            $resultsArray[] = $result->toArray($fieldWhitelist);
72
+        }
73
+
74
+        return $resultsArray;
75
+    }
76
+
77
+    public function toArray($fieldWhitelist)
78
+    {
79
+        $output = [];
80
+        foreach ($this->tableDefinition as $colName => $definition) {
81
+            if (in_array($colName, $fieldWhitelist)) {
82
+                $output[$colName] = $definition['value'];
83
+            }
84
+        }
85
+
86
+        return $output;
87
+    }
88
+
89
+    public function apiRead($id, Array $fieldWhitelist)
90
+    {
91
+        $this->read($id);
92
+        return $this->toArray($fieldWhitelist);
93
+    }
94
+
95
+    /* =============================================================
96 96
 	 * ===================== Constraint validation =================
97 97
 	 * ============================================================= */
98 98
 
99
-	/**
100
-	 * Copy all table variables between two instances
101
-	 */
102
-	public function syncInstanceFrom($from)
103
-	{
104
-		foreach ($this->tableDefinition as $colName => $definition) {
105
-			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
106
-		}
107
-	}
108
-
109
-	private function filterInputColumns($input, $whitelist)
110
-	{
111
-		$filteredInput = $input;
112
-		foreach ($input as $colName => $value) {
113
-			if (!in_array($colName, $whitelist)) {
114
-				unset($filteredInput[$colName]);
115
-			}
116
-		}
117
-		return $filteredInput;
118
-	}
119
-
120
-	private function validateExcessKeys($input)
121
-	{
122
-		$errors = [];
123
-		foreach ($input as $colName => $value) {
124
-			if (!array_key_exists($colName, $this->tableDefinition)) {
125
-				$errors[$colName] = "Unknown input field";
126
-				continue;
127
-			}
128
-		}
129
-		return $errors;
130
-	}
131
-
132
-	private function validateImmutableColumns($input)
133
-	{
134
-		$errors = [];
135
-		foreach ($this->tableDefinition as $colName => $definition) {
136
-			$property = $definition['properties'] ?? null;
137
-			if (array_key_exists($colName, $input)
138
-				&& $property & ColumnProperty::IMMUTABLE) {
139
-				$errors[$colName] = "Field cannot be changed";
140
-			}
141
-		}
142
-		return $errors;
143
-	}
144
-
145
-	/**
146
-	 * Checks whether input values are correct:
147
-	 * 1. Checks whether a value passes the validation function for that column
148
-	 * 2. Checks whether a value supplied to a relationship column is a valid value
149
-	 */
150
-	private function validateInputValues($input)
151
-	{
152
-		$errors = [];
153
-		foreach ($this->tableDefinition as $colName => $definition) {
154
-			// Validation check 1: If validate function is present
155
-			if (array_key_exists($colName, $input) 
156
-				&& is_callable($definition['validate'] ?? null)) {
157
-				$inputValue = $input[$colName];
158
-
159
-				// If validation function fails
160
-				[$status, $message] = $definition['validate']($inputValue);
161
-				if (!$status) {
162
-					$errors[$colName] = $message;
163
-				}	
164
-			}
165
-
166
-			// Validation check 2: If relation column, check whether entity exists
167
-			$properties = $definition['properties'] ?? null;
168
-			if (isset($definition['relation'])
169
-				&& ($properties & ColumnProperty::NOT_NULL)) {
170
-				$instance = clone $definition['relation'];
171
-				try {
172
-					$instance->read($input[$colName] ?? null);
173
-				} catch (ActiveRecordException $e) {
174
-					$errors[$colName] = "Entity for this value doesn't exist";
175
-				}
176
-			}
177
-		}
178
-		return $errors;
179
-	}
180
-
181
-	/**
182
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
183
-	 * Determines whether there are required columns for which no data is provided
184
-	 */
185
-	private function validateMissingKeys($input)
186
-	{
187
-		$errors = [];
188
-
189
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
190
-			$default = $colDefinition['default'] ?? null;
191
-			$properties = $colDefinition['properties'] ?? null;
192
-			$value = $colDefinition['value'];
193
-
194
-			// If nullable and default not set => null
195
-			// If nullable and default null => default (null)
196
-			// If nullable and default set => default (value)
197
-
198
-			// if not nullable and default not set => error
199
-			// if not nullable and default null => error
200
-			// if not nullable and default st => default (value)
201
-			// => if not nullable and default null and value not set (or null) => error message in this method
202
-			if ($properties & ColumnProperty::NOT_NULL
203
-				&& $default === null
204
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
205
-				&& (!array_key_exists($colName, $input) || $input[$colName] === null)
206
-				&& $value === null) {
207
-				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
208
-			}
209
-		}
210
-
211
-		return $errors;
212
-	}
213
-
214
-	/**
215
-	 * Copies the values for entries in the input with matching variable names in the record definition
216
-	 * @param Array $input The input data to be loaded into $this record
217
-	 */
218
-	private function loadData($input)
219
-	{
220
-		foreach ($this->tableDefinition as $colName => $definition) {
221
-			if (array_key_exists($colName, $input)) {
222
-				$definition['value'] = $input[$colName];
223
-			}
224
-		}
225
-	}
226
-
227
-	/**
228
-	 * @param Array $input Associative array of input values
229
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
230
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
231
-	 * 					of the modified data.
232
-	 */
233
-	public function apiCreate($input, Array $createWhitelist, Array $readWhitelist)
234
-	{
235
-		// Clone $this to new instance (for restoring if validation goes wrong)
236
-		$transaction = $this->newInstance();
237
-		$errors = [];
238
-
239
-		// Filter out all non-whitelisted input values
240
-		$input = $this->filterInputColumns($input, $createWhitelist);
241
-
242
-		// Validate excess keys
243
-		$errors += $transaction->validateExcessKeys($input);
244
-
245
-		// Validate input values (using validation function)
246
-		$errors += $transaction->validateInputValues($input);
247
-
248
-		// "Copy" data into transaction
249
-		$transaction->loadData($input);
250
-
251
-		// Run create hooks
252
-		foreach ($transaction->registeredCreateHooks as $colName => $fn) {
253
-			$fn();
254
-		}
255
-
256
-		// Validate missing keys
257
-		$errors += $transaction->validateMissingKeys($input);
258
-
259
-		// If no errors, commit the pending data
260
-		if (empty($errors)) {
261
-			$this->syncInstanceFrom($transaction);
262
-
263
-			// Insert default values for not-null fields
264
-			foreach ($this->tableDefinition as $colName => $colDef) {
265
-				if ($this->tableDefinition[$colName]['value'] === null
266
-					&& isset($this->tableDefinition[$colName]['properties'])
267
-					&& $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
268
-					&& isset($this->tableDefinition[$colName]['default'])) {
269
-					$this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
270
-				}
271
-			}
272
-
273
-			try {
274
-				(new Query($this->getPdo(), $this->getTableName()))
275
-					->insert($this->getActiveRecordColumns())
276
-					->execute();
277
-
278
-				$this->setId(intval($this->getPdo()->lastInsertId()));
279
-			} catch (\PDOException $e) {
280
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
281
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
282
-			}
283
-
284
-			return [null, $this->toArray($readWhitelist)];
285
-		} else {
286
-			return [$errors, null];
287
-		}
288
-	}
289
-
290
-	/**
291
-	 * @param Array $input Associative array of input values
292
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
293
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
294
-	 * 					of the modified data.
295
-	 */
296
-	public function apiUpdate($input, Array $updateWhitelist, Array $readWhitelist)
297
-	{
298
-		$transaction = $this->newInstance();
299
-		$transaction->syncInstanceFrom($this);
300
-		$errors = [];
301
-
302
-		// Filter out all non-whitelisted input values
303
-		$input = $this->filterInputColumns($input, $updateWhitelist);
304
-
305
-		// Check for excess keys
306
-		$errors += $transaction->validateExcessKeys($input);
307
-
308
-		// Check for immutable keys
309
-		$errors += $transaction->validateImmutableColumns($input);
310
-
311
-		// Validate input values (using validation function)
312
-		$errors += $transaction->validateInputValues($input);
313
-
314
-		// "Copy" data into transaction
315
-		$transaction->loadData($input);
316
-
317
-		// Run create hooks
318
-		foreach ($transaction->registeredUpdateHooks as $colName => $fn) {
319
-			$fn();
320
-		}
321
-
322
-		// Validate missing keys
323
-		$errors += $transaction->validateMissingKeys($input);
324
-
325
-		// Update database
326
-		if (empty($errors)) {
327
-			$this->syncInstanceFrom($transaction);
328
-
329
-			try {
330
-				(new Query($this->getPdo(), $this->getTableName()))
331
-					->update($this->getActiveRecordColumns())
332
-					->where(Query::Equal('id', $this->getId()))
333
-					->execute();
334
-			} catch (\PDOException $e) {
335
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
336
-			}
337
-
338
-			return [null, $this->toArray($readWhitelist)];
339
-		} else {
340
-			return [$errors, null];
341
-		}
342
-	}
343
-
344
-	/**
345
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
346
-	 *
347
-	 * @param mixed $id
348
-	 * @return $this
349
-	 * @throws ActiveRecordException on failure.
350
-	 */
351
-	abstract public function read($id);
352
-
353
-	/**
354
-	 * Returns the PDO.
355
-	 *
356
-	 * @return \PDO the PDO.
357
-	 */
358
-	abstract public function getPdo();
359
-
360
-	/**
361
-	 * Set the ID.
362
-	 *
363
-	 * @param int $id
364
-	 * @return $this
365
-	 */
366
-	abstract protected function setId($id);
367
-
368
-	/**
369
-	 * Returns the ID.
370
-	 *
371
-	 * @return null|int The ID.
372
-	 */
373
-	abstract protected function getId();
374
-
375
-	/**
376
-	 * Returns the active record table.
377
-	 *
378
-	 * @return string the active record table name.
379
-	 */
380
-	abstract protected function getTableName();
381
-
382
-	/**
383
-	 * Returns the name -> variable mapping for the table definition.
384
-	 * @return Array The mapping
385
-	 */
386
-	abstract protected function getActiveRecordColumns();
99
+    /**
100
+     * Copy all table variables between two instances
101
+     */
102
+    public function syncInstanceFrom($from)
103
+    {
104
+        foreach ($this->tableDefinition as $colName => $definition) {
105
+            $this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
106
+        }
107
+    }
108
+
109
+    private function filterInputColumns($input, $whitelist)
110
+    {
111
+        $filteredInput = $input;
112
+        foreach ($input as $colName => $value) {
113
+            if (!in_array($colName, $whitelist)) {
114
+                unset($filteredInput[$colName]);
115
+            }
116
+        }
117
+        return $filteredInput;
118
+    }
119
+
120
+    private function validateExcessKeys($input)
121
+    {
122
+        $errors = [];
123
+        foreach ($input as $colName => $value) {
124
+            if (!array_key_exists($colName, $this->tableDefinition)) {
125
+                $errors[$colName] = "Unknown input field";
126
+                continue;
127
+            }
128
+        }
129
+        return $errors;
130
+    }
131
+
132
+    private function validateImmutableColumns($input)
133
+    {
134
+        $errors = [];
135
+        foreach ($this->tableDefinition as $colName => $definition) {
136
+            $property = $definition['properties'] ?? null;
137
+            if (array_key_exists($colName, $input)
138
+                && $property & ColumnProperty::IMMUTABLE) {
139
+                $errors[$colName] = "Field cannot be changed";
140
+            }
141
+        }
142
+        return $errors;
143
+    }
144
+
145
+    /**
146
+     * Checks whether input values are correct:
147
+     * 1. Checks whether a value passes the validation function for that column
148
+     * 2. Checks whether a value supplied to a relationship column is a valid value
149
+     */
150
+    private function validateInputValues($input)
151
+    {
152
+        $errors = [];
153
+        foreach ($this->tableDefinition as $colName => $definition) {
154
+            // Validation check 1: If validate function is present
155
+            if (array_key_exists($colName, $input) 
156
+                && is_callable($definition['validate'] ?? null)) {
157
+                $inputValue = $input[$colName];
158
+
159
+                // If validation function fails
160
+                [$status, $message] = $definition['validate']($inputValue);
161
+                if (!$status) {
162
+                    $errors[$colName] = $message;
163
+                }	
164
+            }
165
+
166
+            // Validation check 2: If relation column, check whether entity exists
167
+            $properties = $definition['properties'] ?? null;
168
+            if (isset($definition['relation'])
169
+                && ($properties & ColumnProperty::NOT_NULL)) {
170
+                $instance = clone $definition['relation'];
171
+                try {
172
+                    $instance->read($input[$colName] ?? null);
173
+                } catch (ActiveRecordException $e) {
174
+                    $errors[$colName] = "Entity for this value doesn't exist";
175
+                }
176
+            }
177
+        }
178
+        return $errors;
179
+    }
180
+
181
+    /**
182
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
183
+     * Determines whether there are required columns for which no data is provided
184
+     */
185
+    private function validateMissingKeys($input)
186
+    {
187
+        $errors = [];
188
+
189
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
190
+            $default = $colDefinition['default'] ?? null;
191
+            $properties = $colDefinition['properties'] ?? null;
192
+            $value = $colDefinition['value'];
193
+
194
+            // If nullable and default not set => null
195
+            // If nullable and default null => default (null)
196
+            // If nullable and default set => default (value)
197
+
198
+            // if not nullable and default not set => error
199
+            // if not nullable and default null => error
200
+            // if not nullable and default st => default (value)
201
+            // => if not nullable and default null and value not set (or null) => error message in this method
202
+            if ($properties & ColumnProperty::NOT_NULL
203
+                && $default === null
204
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
205
+                && (!array_key_exists($colName, $input) || $input[$colName] === null)
206
+                && $value === null) {
207
+                $errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
208
+            }
209
+        }
210
+
211
+        return $errors;
212
+    }
213
+
214
+    /**
215
+     * Copies the values for entries in the input with matching variable names in the record definition
216
+     * @param Array $input The input data to be loaded into $this record
217
+     */
218
+    private function loadData($input)
219
+    {
220
+        foreach ($this->tableDefinition as $colName => $definition) {
221
+            if (array_key_exists($colName, $input)) {
222
+                $definition['value'] = $input[$colName];
223
+            }
224
+        }
225
+    }
226
+
227
+    /**
228
+     * @param Array $input Associative array of input values
229
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
230
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
231
+     * 					of the modified data.
232
+     */
233
+    public function apiCreate($input, Array $createWhitelist, Array $readWhitelist)
234
+    {
235
+        // Clone $this to new instance (for restoring if validation goes wrong)
236
+        $transaction = $this->newInstance();
237
+        $errors = [];
238
+
239
+        // Filter out all non-whitelisted input values
240
+        $input = $this->filterInputColumns($input, $createWhitelist);
241
+
242
+        // Validate excess keys
243
+        $errors += $transaction->validateExcessKeys($input);
244
+
245
+        // Validate input values (using validation function)
246
+        $errors += $transaction->validateInputValues($input);
247
+
248
+        // "Copy" data into transaction
249
+        $transaction->loadData($input);
250
+
251
+        // Run create hooks
252
+        foreach ($transaction->registeredCreateHooks as $colName => $fn) {
253
+            $fn();
254
+        }
255
+
256
+        // Validate missing keys
257
+        $errors += $transaction->validateMissingKeys($input);
258
+
259
+        // If no errors, commit the pending data
260
+        if (empty($errors)) {
261
+            $this->syncInstanceFrom($transaction);
262
+
263
+            // Insert default values for not-null fields
264
+            foreach ($this->tableDefinition as $colName => $colDef) {
265
+                if ($this->tableDefinition[$colName]['value'] === null
266
+                    && isset($this->tableDefinition[$colName]['properties'])
267
+                    && $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
268
+                    && isset($this->tableDefinition[$colName]['default'])) {
269
+                    $this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
270
+                }
271
+            }
272
+
273
+            try {
274
+                (new Query($this->getPdo(), $this->getTableName()))
275
+                    ->insert($this->getActiveRecordColumns())
276
+                    ->execute();
277
+
278
+                $this->setId(intval($this->getPdo()->lastInsertId()));
279
+            } catch (\PDOException $e) {
280
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
281
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
282
+            }
283
+
284
+            return [null, $this->toArray($readWhitelist)];
285
+        } else {
286
+            return [$errors, null];
287
+        }
288
+    }
289
+
290
+    /**
291
+     * @param Array $input Associative array of input values
292
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
293
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
294
+     * 					of the modified data.
295
+     */
296
+    public function apiUpdate($input, Array $updateWhitelist, Array $readWhitelist)
297
+    {
298
+        $transaction = $this->newInstance();
299
+        $transaction->syncInstanceFrom($this);
300
+        $errors = [];
301
+
302
+        // Filter out all non-whitelisted input values
303
+        $input = $this->filterInputColumns($input, $updateWhitelist);
304
+
305
+        // Check for excess keys
306
+        $errors += $transaction->validateExcessKeys($input);
307
+
308
+        // Check for immutable keys
309
+        $errors += $transaction->validateImmutableColumns($input);
310
+
311
+        // Validate input values (using validation function)
312
+        $errors += $transaction->validateInputValues($input);
313
+
314
+        // "Copy" data into transaction
315
+        $transaction->loadData($input);
316
+
317
+        // Run create hooks
318
+        foreach ($transaction->registeredUpdateHooks as $colName => $fn) {
319
+            $fn();
320
+        }
321
+
322
+        // Validate missing keys
323
+        $errors += $transaction->validateMissingKeys($input);
324
+
325
+        // Update database
326
+        if (empty($errors)) {
327
+            $this->syncInstanceFrom($transaction);
328
+
329
+            try {
330
+                (new Query($this->getPdo(), $this->getTableName()))
331
+                    ->update($this->getActiveRecordColumns())
332
+                    ->where(Query::Equal('id', $this->getId()))
333
+                    ->execute();
334
+            } catch (\PDOException $e) {
335
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
336
+            }
337
+
338
+            return [null, $this->toArray($readWhitelist)];
339
+        } else {
340
+            return [$errors, null];
341
+        }
342
+    }
343
+
344
+    /**
345
+     * Returns this active record after reading the attributes from the entry with the given identifier.
346
+     *
347
+     * @param mixed $id
348
+     * @return $this
349
+     * @throws ActiveRecordException on failure.
350
+     */
351
+    abstract public function read($id);
352
+
353
+    /**
354
+     * Returns the PDO.
355
+     *
356
+     * @return \PDO the PDO.
357
+     */
358
+    abstract public function getPdo();
359
+
360
+    /**
361
+     * Set the ID.
362
+     *
363
+     * @param int $id
364
+     * @return $this
365
+     */
366
+    abstract protected function setId($id);
367
+
368
+    /**
369
+     * Returns the ID.
370
+     *
371
+     * @return null|int The ID.
372
+     */
373
+    abstract protected function getId();
374
+
375
+    /**
376
+     * Returns the active record table.
377
+     *
378
+     * @return string the active record table name.
379
+     */
380
+    abstract protected function getTableName();
381
+
382
+    /**
383
+     * Returns the name -> variable mapping for the table definition.
384
+     * @return Array The mapping
385
+     */
386
+    abstract protected function getActiveRecordColumns();
387 387
 }
Please login to merge, or discard this patch.
src/Traits/SoftDelete.php 1 patch
Indentation   +98 added lines, -98 removed lines patch added patch discarded remove patch
@@ -9,105 +9,105 @@
 block discarded – undo
9 9
 
10 10
 trait SoftDelete
11 11
 {
12
-	/** @var boolean the soft delete status for the entity this trait is embedded into. */
13
-	protected $softDelete;
14
-
15
-	/**
16
-	 * this method is required to be called in the constructor for each class that uses this trait. 
17
-	 * It adds the required fields to the table definition and registers hooks
18
-	 */
19
-	protected function initSoftDelete()
20
-	{
21
-		$this->softDelete = false;
22
-
23
-		$this->extendTableDefinition(TRAIT_SOFT_DELETE_FIELD_KEY, [
24
-			'value' => &$this->softDelete,
25
-			'validate' => null,
26
-			'default' => 0,
27
-			'type' => 'INT',
28
-			'length' => 1,
29
-			'properties' => ColumnProperty::NOT_NULL
30
-		]);
31
-
32
-		$this->registerSearchHook(TRAIT_SOFT_DELETE_FIELD_KEY, 'softDeleteSearchHook');
33
-	}
34
-
35
-	/**
36
-	 * The hook that gets called whenever a query is made
37
-	 */
38
-	protected function softDeleteSearchHook()
39
-	{
40
-		return Query::Equal(TRAIT_SOFT_DELETE_FIELD_KEY, 0);
41
-	}
42
-
43
-	/**
44
-	 * returns the name for the soft delete field in the database
45
-	 * @return string
46
-	 */
47
-	public function getSoftDeleteFieldName()
48
-	{
49
-		return TRAIT_SOFT_DELETE_FIELD_KEY;
50
-	}
12
+    /** @var boolean the soft delete status for the entity this trait is embedded into. */
13
+    protected $softDelete;
14
+
15
+    /**
16
+     * this method is required to be called in the constructor for each class that uses this trait. 
17
+     * It adds the required fields to the table definition and registers hooks
18
+     */
19
+    protected function initSoftDelete()
20
+    {
21
+        $this->softDelete = false;
22
+
23
+        $this->extendTableDefinition(TRAIT_SOFT_DELETE_FIELD_KEY, [
24
+            'value' => &$this->softDelete,
25
+            'validate' => null,
26
+            'default' => 0,
27
+            'type' => 'INT',
28
+            'length' => 1,
29
+            'properties' => ColumnProperty::NOT_NULL
30
+        ]);
31
+
32
+        $this->registerSearchHook(TRAIT_SOFT_DELETE_FIELD_KEY, 'softDeleteSearchHook');
33
+    }
34
+
35
+    /**
36
+     * The hook that gets called whenever a query is made
37
+     */
38
+    protected function softDeleteSearchHook()
39
+    {
40
+        return Query::Equal(TRAIT_SOFT_DELETE_FIELD_KEY, 0);
41
+    }
42
+
43
+    /**
44
+     * returns the name for the soft delete field in the database
45
+     * @return string
46
+     */
47
+    public function getSoftDeleteFieldName()
48
+    {
49
+        return TRAIT_SOFT_DELETE_FIELD_KEY;
50
+    }
51 51
 	
52
-	/**
53
-	 * Mark the current record as soft deleted
54
-	 * @return $this
55
-	 */
56
-	public function softDelete()
57
-	{
58
-		$this->softDelete = true;
59
-		$this->update();
60
-		return $this;
61
-	}
62
-
63
-	/**
64
-	 * Undo the current soft deletion status (mark it as non-soft deleted)
65
-	 * @return $this
66
-	 */
67
-	public function softRestore()
68
-	{
69
-		$this->softDelete = false;
70
-		$this->update();
71
-		return $this;
72
-	}
73
-
74
-	/**
75
-	 * returns the current soft deletion status
76
-	 * @return $this
77
-	 */
78
-	public function getDeletionStatus() 
79
-	{
80
-		return $this->softDelete;
81
-	}
82
-
83
-	/**
84
-	 * @return void
85
-	 */
86
-	abstract protected function extendTableDefinition($columnName, $definition);
52
+    /**
53
+     * Mark the current record as soft deleted
54
+     * @return $this
55
+     */
56
+    public function softDelete()
57
+    {
58
+        $this->softDelete = true;
59
+        $this->update();
60
+        return $this;
61
+    }
62
+
63
+    /**
64
+     * Undo the current soft deletion status (mark it as non-soft deleted)
65
+     * @return $this
66
+     */
67
+    public function softRestore()
68
+    {
69
+        $this->softDelete = false;
70
+        $this->update();
71
+        return $this;
72
+    }
73
+
74
+    /**
75
+     * returns the current soft deletion status
76
+     * @return $this
77
+     */
78
+    public function getDeletionStatus() 
79
+    {
80
+        return $this->softDelete;
81
+    }
82
+
83
+    /**
84
+     * @return void
85
+     */
86
+    abstract protected function extendTableDefinition($columnName, $definition);
87 87
 	
88
-	/**
89
-	 * @return void
90
-	 */
91
-	abstract protected function registerSearchHook($columnName, $fn);
92
-
93
-	/**
94
-	 * @return void
95
-	 */
96
-	abstract protected function registerDeleteHook($columnName, $fn);
97
-
98
-	/**
99
-	 * @return void
100
-	 */
101
-	abstract protected function registerUpdateHook($columnName, $fn);
102
-
103
-	/**
104
-	 * @return void
105
-	 */
106
-	abstract protected function registerReadHook($columnName, $fn);
107
-
108
-	/**
109
-	 * @return void
110
-	 */
111
-	abstract protected function registerCreateHook($columnName, $fn);
88
+    /**
89
+     * @return void
90
+     */
91
+    abstract protected function registerSearchHook($columnName, $fn);
92
+
93
+    /**
94
+     * @return void
95
+     */
96
+    abstract protected function registerDeleteHook($columnName, $fn);
97
+
98
+    /**
99
+     * @return void
100
+     */
101
+    abstract protected function registerUpdateHook($columnName, $fn);
102
+
103
+    /**
104
+     * @return void
105
+     */
106
+    abstract protected function registerReadHook($columnName, $fn);
107
+
108
+    /**
109
+     * @return void
110
+     */
111
+    abstract protected function registerCreateHook($columnName, $fn);
112 112
 	
113 113
 }
114 114
\ No newline at end of file
Please login to merge, or discard this patch.
src/Traits/Datefields.php 1 patch
Indentation   +97 added lines, -97 removed lines patch added patch discarded remove patch
@@ -10,104 +10,104 @@
 block discarded – undo
10 10
 
11 11
 trait Datefields
12 12
 {
13
-	/** @var string The timestamp representing the moment this record was created */
14
-	protected $created;
15
-
16
-	/** @var string The timestamp representing the moment this record was last updated */
17
-	protected $lastModified;
18
-
19
-	/**
20
-	 * this method is required to be called in the constructor for each class that uses this trait. 
21
-	 * It adds the datefields to the table definition and registers the callback hooks
22
-	 */
23
-	protected function initDatefields()
24
-	{
25
-		$this->extendTableDefinition(TRAIT_DATEFIELDS_CREATED, [
26
-			'value' => &$this->created,
27
-			'validate' => null,
28
-			'type' => 'DATETIME',
29
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
30
-		]);
31
-
32
-		$this->extendTableDefinition(TRAIT_DATEFIELDS_LAST_MODIFIED, [
33
-			'value' => &$this->lastModified,
34
-			'validate' => null,
35
-			'type' => 'DATETIME',
36
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
37
-		]);
13
+    /** @var string The timestamp representing the moment this record was created */
14
+    protected $created;
15
+
16
+    /** @var string The timestamp representing the moment this record was last updated */
17
+    protected $lastModified;
18
+
19
+    /**
20
+     * this method is required to be called in the constructor for each class that uses this trait. 
21
+     * It adds the datefields to the table definition and registers the callback hooks
22
+     */
23
+    protected function initDatefields()
24
+    {
25
+        $this->extendTableDefinition(TRAIT_DATEFIELDS_CREATED, [
26
+            'value' => &$this->created,
27
+            'validate' => null,
28
+            'type' => 'DATETIME',
29
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
30
+        ]);
31
+
32
+        $this->extendTableDefinition(TRAIT_DATEFIELDS_LAST_MODIFIED, [
33
+            'value' => &$this->lastModified,
34
+            'validate' => null,
35
+            'type' => 'DATETIME',
36
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
37
+        ]);
38 38
 		
39
-		$this->registerUpdateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsUpdateHook');
40
-		$this->registerCreateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsCreateHook');
41
-
42
-		$this->created = null;
43
-		$this->lastModified = null;
44
-	}
45
-
46
-	/**
47
-	 * The hook that gets called to set the timestamp whenever a new record is created
48
-	 */
49
-	protected function DatefieldsCreateHook()
50
-	{
51
-		// @TODO: Should this be split up to seperate hooks for "last_modified" and "created" for consistency?
52
-		$this->created = (new \DateTime('now'))->format('Y-m-d H:i:s');
53
-		$this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
54
-	}
55
-
56
-	/**
57
-	 * The hook that gets called to set the timestamp whenever a record gets updated
58
-	 */
59
-	protected function DatefieldsUpdateHook()
60
-	{
61
-		$this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
62
-	}
63
-
64
-	/**
65
-	 * Returns the timestamp of last update for this record
66
-	 * @return \DateTime
67
-	 */
68
-	public function getLastModifiedDate()
69
-	{
70
-		return new \DateTime($this->lastModified);
71
-	}
72
-
73
-	/**
74
-	 * Returns the timestamp of when this record was created
75
-	 * @return \DateTime
76
-	 */
77
-	public function getCreationDate()
78
-	{
79
-		return new \DateTime($this->created);
80
-	}
81
-
82
-	/**
83
-	 * @return void
84
-	 */
85
-	abstract protected function extendTableDefinition($columnName, $definition);
39
+        $this->registerUpdateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsUpdateHook');
40
+        $this->registerCreateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsCreateHook');
41
+
42
+        $this->created = null;
43
+        $this->lastModified = null;
44
+    }
45
+
46
+    /**
47
+     * The hook that gets called to set the timestamp whenever a new record is created
48
+     */
49
+    protected function DatefieldsCreateHook()
50
+    {
51
+        // @TODO: Should this be split up to seperate hooks for "last_modified" and "created" for consistency?
52
+        $this->created = (new \DateTime('now'))->format('Y-m-d H:i:s');
53
+        $this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
54
+    }
55
+
56
+    /**
57
+     * The hook that gets called to set the timestamp whenever a record gets updated
58
+     */
59
+    protected function DatefieldsUpdateHook()
60
+    {
61
+        $this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
62
+    }
63
+
64
+    /**
65
+     * Returns the timestamp of last update for this record
66
+     * @return \DateTime
67
+     */
68
+    public function getLastModifiedDate()
69
+    {
70
+        return new \DateTime($this->lastModified);
71
+    }
72
+
73
+    /**
74
+     * Returns the timestamp of when this record was created
75
+     * @return \DateTime
76
+     */
77
+    public function getCreationDate()
78
+    {
79
+        return new \DateTime($this->created);
80
+    }
81
+
82
+    /**
83
+     * @return void
84
+     */
85
+    abstract protected function extendTableDefinition($columnName, $definition);
86 86
 	
87
-	/**
88
-	 * @return void
89
-	 */
90
-	abstract protected function registerSearchHook($columnName, $fn);
91
-
92
-	/**
93
-	 * @return void
94
-	 */
95
-	abstract protected function registerDeleteHook($columnName, $fn);
96
-
97
-	/**
98
-	 * @return void
99
-	 */
100
-	abstract protected function registerUpdateHook($columnName, $fn);
101
-
102
-	/**
103
-	 * @return void
104
-	 */
105
-	abstract protected function registerReadHook($columnName, $fn);
106
-
107
-	/**
108
-	 * @return void
109
-	 */
110
-	abstract protected function registerCreateHook($columnName, $fn);
87
+    /**
88
+     * @return void
89
+     */
90
+    abstract protected function registerSearchHook($columnName, $fn);
91
+
92
+    /**
93
+     * @return void
94
+     */
95
+    abstract protected function registerDeleteHook($columnName, $fn);
96
+
97
+    /**
98
+     * @return void
99
+     */
100
+    abstract protected function registerUpdateHook($columnName, $fn);
101
+
102
+    /**
103
+     * @return void
104
+     */
105
+    abstract protected function registerReadHook($columnName, $fn);
106
+
107
+    /**
108
+     * @return void
109
+     */
110
+    abstract protected function registerCreateHook($columnName, $fn);
111 111
 }
112 112
 
113
-	
114 113
\ No newline at end of file
114
+    
115 115
\ No newline at end of file
Please login to merge, or discard this patch.
src/AbstractActiveRecord.php 2 patches
Indentation   +612 added lines, -612 removed lines patch added patch discarded remove patch
@@ -18,623 +18,623 @@
 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
-	/** @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
-	public function __construct(\PDO $pdo)
54
-	{
55
-		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56
-		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
57
-
58
-		$this->setPdo($pdo);
59
-
60
-		$this->registeredCreateHooks = [];
61
-		$this->registeredReadHooks = [];
62
-		$this->registeredUpdateHooks = [];
63
-		$this->registeredDeleteHooks = [];
64
-		$this->registeredSearchHooks = [];
65
-		$this->tableDefinition = $this->getTableDefinition();
66
-
67
-		// Extend table definition with default ID field, throw exception if field already exists
68
-		if (array_key_exists('id', $this->tableDefinition)) {
69
-			$message = "Table definition in record contains a field with name \"id\"";
70
-			$message .= ", which is a reserved name by ActiveRecord";
71
-			throw new ActiveRecordException($message, 0);
72
-		}
73
-
74
-		$this->tableDefinition[self::COLUMN_NAME_ID] =
75
-		[
76
-			'value' => &$this->id,
77
-			'validate' => null,
78
-			'type' => self::COLUMN_TYPE_ID,
79
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY
80
-		];
81
-	}
82
-
83
-	private function checkHookConstraints($columnName, $hookMap)
84
-	{
85
-		// Check whether column exists
86
-		if (!array_key_exists($columnName, $this->tableDefinition)) 
87
-		{
88
-			throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
89
-		}
90
-
91
-		// Enforcing 1 hook per table column
92
-		if (array_key_exists($columnName, $hookMap)) {
93
-			$message = "Hook is trying to register on an already registered column \"$columnName\", ";
94
-			$message .= "do you have conflicting traits?";
95
-			throw new ActiveRecordException($message, 0);
96
-		}
97
-	}
98
-
99
-	/**
100
-	 * Register a new hook for a specific column that gets called before execution of the create() method
101
-	 * Only one hook per column can be registered at a time
102
-	 * @param string $columnName The name of the column that is registered.
103
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
104
-	 */
105
-	public function registerCreateHook($columnName, $fn)
106
-	{
107
-		$this->checkHookConstraints($columnName, $this->registeredCreateHooks);
108
-
109
-		if (is_string($fn) && is_callable([$this, $fn])) {
110
-			$this->registeredCreateHooks[$columnName] = [$this, $fn];
111
-		} else if (is_callable($fn)) {
112
-			$this->registeredCreateHooks[$columnName] = $fn;
113
-		} else {
114
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
115
-		}
116
-	}
117
-
118
-	/**
119
-	 * Register a new hook for a specific column that gets called before execution of the read() method
120
-	 * Only one hook per column can be registered at a time
121
-	 * @param string $columnName The name of the column that is registered.
122
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
123
-	 */
124
-	public function registerReadHook($columnName, $fn)
125
-	{
126
-		$this->checkHookConstraints($columnName, $this->registeredReadHooks);
127
-
128
-		if (is_string($fn) && is_callable([$this, $fn])) {
129
-			$this->registeredReadHooks[$columnName] = [$this, $fn];
130
-		} else if (is_callable($fn)) {
131
-			$this->registeredReadHooks[$columnName] = $fn;
132
-		} else {
133
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
134
-		}
135
-	}
136
-
137
-	/**
138
-	 * Register a new hook for a specific column that gets called before execution of the update() method
139
-	 * Only one hook per column can be registered at a time
140
-	 * @param string $columnName The name of the column that is registered.
141
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
142
-	 */
143
-	public function registerUpdateHook($columnName, $fn)
144
-	{
145
-		$this->checkHookConstraints($columnName, $this->registeredUpdateHooks);
146
-
147
-		if (is_string($fn) && is_callable([$this, $fn])) {
148
-			$this->registeredUpdateHooks[$columnName] = [$this, $fn];
149
-		} else if (is_callable($fn)) {
150
-			$this->registeredUpdateHooks[$columnName] = $fn;
151
-		} else {
152
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * Register a new hook for a specific column that gets called before execution of the delete() 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 registerDeleteHook($columnName, $fn)
163
-	{
164
-		$this->checkHookConstraints($columnName, $this->registeredDeleteHooks);
165
-
166
-		if (is_string($fn) && is_callable([$this, $fn])) {
167
-			$this->registeredDeleteHooks[$columnName] = [$this, $fn];
168
-		} else if (is_callable($fn)) {
169
-			$this->registeredDeleteHooks[$columnName] = $fn;
170
-		} else {
171
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
172
-		}
173
-	}
174
-
175
-	/**
176
-	 * Register a new hook for a specific column that gets called before execution of the search() method
177
-	 * Only one hook per column can be registered at a time
178
-	 * @param string $columnName The name of the column that is registered.
179
-	 * @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; 
180
-	 */
181
-	public function registerSearchHook($columnName, $fn)
182
-	{
183
-		$this->checkHookConstraints($columnName, $this->registeredSearchHooks);
184
-
185
-		if (is_string($fn) && is_callable([$this, $fn])) {
186
-			$this->registeredSearchHooks[$columnName] = [$this, $fn];
187
-		} else if (is_callable($fn)) {
188
-			$this->registeredSearchHooks[$columnName] = $fn;
189
-		} else {
190
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
191
-		}
192
-	}
193
-
194
-	/**
195
-	 * Adds a new column definition to the table.
196
-	 * @param string $columnName The name of the column that is registered.
197
-	 * @param Array $definition The definition of that column.
198
-	 */
199
-	public function extendTableDefinition($columnName, $definition)
200
-	{
201
-		if ($this->tableDefinition === null) {
202
-			throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor");
203
-		}
204
-
205
-		// Enforcing table can only be extended with new columns
206
-		if (array_key_exists($columnName, $this->tableDefinition)) {
207
-			$message = "Table is being extended with a column that already exists, ";
208
-			$message .= "\"$columnName\" conflicts with your table definition";
209
-			throw new ActiveRecordException($message, 0);
210
-		}
211
-
212
-		$this->tableDefinition[$columnName] = $definition;
213
-	}
214
-
215
-	/**
216
-	 * Returns the type string as it should appear in the mysql create table statement for the given column
217
-	 * @return string The type string
218
-	 */
219
-	private function getDatabaseTypeString($colName, $type, $length)
220
-	{
221
-		switch (strtoupper($type)) {
222
-			case '':
223
-				throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
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
+    public function __construct(\PDO $pdo)
54
+    {
55
+        $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56
+        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
57
+
58
+        $this->setPdo($pdo);
59
+
60
+        $this->registeredCreateHooks = [];
61
+        $this->registeredReadHooks = [];
62
+        $this->registeredUpdateHooks = [];
63
+        $this->registeredDeleteHooks = [];
64
+        $this->registeredSearchHooks = [];
65
+        $this->tableDefinition = $this->getTableDefinition();
66
+
67
+        // Extend table definition with default ID field, throw exception if field already exists
68
+        if (array_key_exists('id', $this->tableDefinition)) {
69
+            $message = "Table definition in record contains a field with name \"id\"";
70
+            $message .= ", which is a reserved name by ActiveRecord";
71
+            throw new ActiveRecordException($message, 0);
72
+        }
73
+
74
+        $this->tableDefinition[self::COLUMN_NAME_ID] =
75
+        [
76
+            'value' => &$this->id,
77
+            'validate' => null,
78
+            'type' => self::COLUMN_TYPE_ID,
79
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY
80
+        ];
81
+    }
82
+
83
+    private function checkHookConstraints($columnName, $hookMap)
84
+    {
85
+        // Check whether column exists
86
+        if (!array_key_exists($columnName, $this->tableDefinition)) 
87
+        {
88
+            throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
89
+        }
90
+
91
+        // Enforcing 1 hook per table column
92
+        if (array_key_exists($columnName, $hookMap)) {
93
+            $message = "Hook is trying to register on an already registered column \"$columnName\", ";
94
+            $message .= "do you have conflicting traits?";
95
+            throw new ActiveRecordException($message, 0);
96
+        }
97
+    }
98
+
99
+    /**
100
+     * Register a new hook for a specific column that gets called before execution of the create() method
101
+     * Only one hook per column can be registered at a time
102
+     * @param string $columnName The name of the column that is registered.
103
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
104
+     */
105
+    public function registerCreateHook($columnName, $fn)
106
+    {
107
+        $this->checkHookConstraints($columnName, $this->registeredCreateHooks);
108
+
109
+        if (is_string($fn) && is_callable([$this, $fn])) {
110
+            $this->registeredCreateHooks[$columnName] = [$this, $fn];
111
+        } else if (is_callable($fn)) {
112
+            $this->registeredCreateHooks[$columnName] = $fn;
113
+        } else {
114
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
115
+        }
116
+    }
117
+
118
+    /**
119
+     * Register a new hook for a specific column that gets called before execution of the read() method
120
+     * Only one hook per column can be registered at a time
121
+     * @param string $columnName The name of the column that is registered.
122
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
123
+     */
124
+    public function registerReadHook($columnName, $fn)
125
+    {
126
+        $this->checkHookConstraints($columnName, $this->registeredReadHooks);
127
+
128
+        if (is_string($fn) && is_callable([$this, $fn])) {
129
+            $this->registeredReadHooks[$columnName] = [$this, $fn];
130
+        } else if (is_callable($fn)) {
131
+            $this->registeredReadHooks[$columnName] = $fn;
132
+        } else {
133
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
134
+        }
135
+    }
136
+
137
+    /**
138
+     * Register a new hook for a specific column that gets called before execution of the update() method
139
+     * Only one hook per column can be registered at a time
140
+     * @param string $columnName The name of the column that is registered.
141
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
142
+     */
143
+    public function registerUpdateHook($columnName, $fn)
144
+    {
145
+        $this->checkHookConstraints($columnName, $this->registeredUpdateHooks);
146
+
147
+        if (is_string($fn) && is_callable([$this, $fn])) {
148
+            $this->registeredUpdateHooks[$columnName] = [$this, $fn];
149
+        } else if (is_callable($fn)) {
150
+            $this->registeredUpdateHooks[$columnName] = $fn;
151
+        } else {
152
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
153
+        }
154
+    }
155
+
156
+    /**
157
+     * Register a new hook for a specific column that gets called before execution of the delete() 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 registerDeleteHook($columnName, $fn)
163
+    {
164
+        $this->checkHookConstraints($columnName, $this->registeredDeleteHooks);
165
+
166
+        if (is_string($fn) && is_callable([$this, $fn])) {
167
+            $this->registeredDeleteHooks[$columnName] = [$this, $fn];
168
+        } else if (is_callable($fn)) {
169
+            $this->registeredDeleteHooks[$columnName] = $fn;
170
+        } else {
171
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
172
+        }
173
+    }
174
+
175
+    /**
176
+     * Register a new hook for a specific column that gets called before execution of the search() method
177
+     * Only one hook per column can be registered at a time
178
+     * @param string $columnName The name of the column that is registered.
179
+     * @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; 
180
+     */
181
+    public function registerSearchHook($columnName, $fn)
182
+    {
183
+        $this->checkHookConstraints($columnName, $this->registeredSearchHooks);
184
+
185
+        if (is_string($fn) && is_callable([$this, $fn])) {
186
+            $this->registeredSearchHooks[$columnName] = [$this, $fn];
187
+        } else if (is_callable($fn)) {
188
+            $this->registeredSearchHooks[$columnName] = $fn;
189
+        } else {
190
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
191
+        }
192
+    }
193
+
194
+    /**
195
+     * Adds a new column definition to the table.
196
+     * @param string $columnName The name of the column that is registered.
197
+     * @param Array $definition The definition of that column.
198
+     */
199
+    public function extendTableDefinition($columnName, $definition)
200
+    {
201
+        if ($this->tableDefinition === null) {
202
+            throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor");
203
+        }
204
+
205
+        // Enforcing table can only be extended with new columns
206
+        if (array_key_exists($columnName, $this->tableDefinition)) {
207
+            $message = "Table is being extended with a column that already exists, ";
208
+            $message .= "\"$columnName\" conflicts with your table definition";
209
+            throw new ActiveRecordException($message, 0);
210
+        }
211
+
212
+        $this->tableDefinition[$columnName] = $definition;
213
+    }
214
+
215
+    /**
216
+     * Returns the type string as it should appear in the mysql create table statement for the given column
217
+     * @return string The type string
218
+     */
219
+    private function getDatabaseTypeString($colName, $type, $length)
220
+    {
221
+        switch (strtoupper($type)) {
222
+            case '':
223
+                throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
224 224
 			
225
-			case 'BOOL';
226
-			case 'BOOLEAN':
227
-			case 'DATETIME':
228
-			case 'DATE':
229
-			case 'TIME':
230
-			case 'TEXT':
231
-			case 'INT UNSIGNED':
232
-				return $type;
233
-
234
-			case 'VARCHAR':
235
-				if ($length === null) {
236
-					throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
237
-				} else {
238
-					return sprintf('%s(%d)', $type, $length);	
239
-				}
240
-
241
-			case 'INT':
242
-			case 'TINYINT':
243
-			case 'BIGINT':
244
-			default: 	
245
-				// Implicitly assuming that non-specified cases are correct without a length parameter
246
-				if ($length === null) {
247
-					return $type;
248
-				} else {
249
-					return sprintf('%s(%d)', $type, $length);	
250
-				}
251
-		}
252
-	}
253
-
254
-	/**
255
-	 * Builds the part of a MySQL create table statement that corresponds to the supplied column
256
-	 * @param string $colName 	Name of the database column
257
-	 * @param string $type 		The type of the string
258
-	 * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
259
-	 * @return string
260
-	 */
261
-	private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
262
-	{
263
-		$stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length));
264
-		if ($properties & ColumnProperty::NOT_NULL) {
265
-			$stmnt .= 'NOT NULL ';
266
-		} else {
267
-			$stmnt .= 'NULL ';
268
-		}
269
-
270
-		if ($default !== NULL) {
271
-			$stmnt .= 'DEFAULT ' . var_export($default, true) . ' ';
272
-		}
273
-
274
-		if ($properties & ColumnProperty::AUTO_INCREMENT) {
275
-			$stmnt .= 'AUTO_INCREMENT ';
276
-		}
277
-
278
-		if ($properties & ColumnProperty::UNIQUE) {
279
-			$stmnt .= 'UNIQUE ';
280
-		}
281
-
282
-		if ($properties & ColumnProperty::PRIMARY_KEY) {
283
-			$stmnt .= 'PRIMARY KEY ';
284
-		}
285
-
286
-		return $stmnt;
287
-	}
288
-
289
-	/**
290
-	 * Sorts the column statement components in the order such that the id appears first, 
291
-	 * 		followed by all other columns in alphabetical ascending order
292
-	 * @param   Array $colStatements Array of column statements
293
-	 * @return  Array
294
-	 */
295
-	private function sortColumnStatements($colStatements)
296
-	{
297
-		// Find ID statement and put it first
298
-		$sortedStatements = [];
299
-
300
-		$sortedStatements[] = $colStatements[self::COLUMN_NAME_ID];
301
-		unset($colStatements[self::COLUMN_NAME_ID]);
302
-
303
-		// Sort remaining columns in alphabetical order
304
-		$columns = array_keys($colStatements);
305
-		sort($columns);
306
-		foreach ($columns as $colName) {
307
-			$sortedStatements[] = $colStatements[$colName];
308
-		}
309
-
310
-		return $sortedStatements;
311
-	}
312
-
313
-	/**
314
-	 * Builds the MySQL Create Table statement for the internal table definition
315
-	 * @return string
316
-	 */
317
-	public function buildCreateTableSQL()
318
-	{
319
-		$columnStatements = [];
320
-		foreach ($this->tableDefinition as $colName => $definition) {
321
-			// Destructure column definition
322
-			$type    = $definition['type'] ?? null;
323
-			$default = $definition['default'] ?? null;
324
-			$length  = $definition['length'] ?? null;
325
-			$properties = $definition['properties'] ?? null;
326
-
327
-			if (isset($definition['relation']) && $type !== null) {
328
-				$tableName = $this->getTableName();
329
-				$msg = "Column \"$colName\" on table \"$tableName\": ";
330
-				$msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
331
-				throw new ActiveRecordException($msg);
332
-			} else if (isset($definition['relation'])) {
333
-				$type = self::COLUMN_TYPE_ID;
334
-			}
335
-
336
-			$columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
337
-		}
338
-
339
-		// Sort table (first column is id, the remaining are alphabetically sorted)
340
-		$columnStatements = $this->sortColumnStatements($columnStatements);
341
-
342
-		$sql = sprintf("CREATE TABLE %s (\n%s\n);", 
343
-			$this->getTableName(), 
344
-			implode(",\n", $columnStatements));
345
-
346
-		return $sql;
347
-	}
348
-
349
-	/**
350
-	 * Creates the entity as a table in the database
351
-	 */
352
-	public function createTable()
353
-	{
354
-		$this->pdo->query($this->buildCreateTableSQL());
355
-	}
356
-
357
-	/**
358
-	 * builds a MySQL constraint statement for the given parameters
359
-	 * @param string $parentTable
360
-	 * @param string $parentColumn
361
-	 * @param string $childTable
362
-	 * @param string $childColumn
363
-	 * @return string The MySQL table constraint string
364
-	 */
365
-	protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
366
-	{
367
-		$template = <<<SQL
225
+            case 'BOOL';
226
+            case 'BOOLEAN':
227
+            case 'DATETIME':
228
+            case 'DATE':
229
+            case 'TIME':
230
+            case 'TEXT':
231
+            case 'INT UNSIGNED':
232
+                return $type;
233
+
234
+            case 'VARCHAR':
235
+                if ($length === null) {
236
+                    throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
237
+                } else {
238
+                    return sprintf('%s(%d)', $type, $length);	
239
+                }
240
+
241
+            case 'INT':
242
+            case 'TINYINT':
243
+            case 'BIGINT':
244
+            default: 	
245
+                // Implicitly assuming that non-specified cases are correct without a length parameter
246
+                if ($length === null) {
247
+                    return $type;
248
+                } else {
249
+                    return sprintf('%s(%d)', $type, $length);	
250
+                }
251
+        }
252
+    }
253
+
254
+    /**
255
+     * Builds the part of a MySQL create table statement that corresponds to the supplied column
256
+     * @param string $colName 	Name of the database column
257
+     * @param string $type 		The type of the string
258
+     * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
259
+     * @return string
260
+     */
261
+    private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
262
+    {
263
+        $stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length));
264
+        if ($properties & ColumnProperty::NOT_NULL) {
265
+            $stmnt .= 'NOT NULL ';
266
+        } else {
267
+            $stmnt .= 'NULL ';
268
+        }
269
+
270
+        if ($default !== NULL) {
271
+            $stmnt .= 'DEFAULT ' . var_export($default, true) . ' ';
272
+        }
273
+
274
+        if ($properties & ColumnProperty::AUTO_INCREMENT) {
275
+            $stmnt .= 'AUTO_INCREMENT ';
276
+        }
277
+
278
+        if ($properties & ColumnProperty::UNIQUE) {
279
+            $stmnt .= 'UNIQUE ';
280
+        }
281
+
282
+        if ($properties & ColumnProperty::PRIMARY_KEY) {
283
+            $stmnt .= 'PRIMARY KEY ';
284
+        }
285
+
286
+        return $stmnt;
287
+    }
288
+
289
+    /**
290
+     * Sorts the column statement components in the order such that the id appears first, 
291
+     * 		followed by all other columns in alphabetical ascending order
292
+     * @param   Array $colStatements Array of column statements
293
+     * @return  Array
294
+     */
295
+    private function sortColumnStatements($colStatements)
296
+    {
297
+        // Find ID statement and put it first
298
+        $sortedStatements = [];
299
+
300
+        $sortedStatements[] = $colStatements[self::COLUMN_NAME_ID];
301
+        unset($colStatements[self::COLUMN_NAME_ID]);
302
+
303
+        // Sort remaining columns in alphabetical order
304
+        $columns = array_keys($colStatements);
305
+        sort($columns);
306
+        foreach ($columns as $colName) {
307
+            $sortedStatements[] = $colStatements[$colName];
308
+        }
309
+
310
+        return $sortedStatements;
311
+    }
312
+
313
+    /**
314
+     * Builds the MySQL Create Table statement for the internal table definition
315
+     * @return string
316
+     */
317
+    public function buildCreateTableSQL()
318
+    {
319
+        $columnStatements = [];
320
+        foreach ($this->tableDefinition as $colName => $definition) {
321
+            // Destructure column definition
322
+            $type    = $definition['type'] ?? null;
323
+            $default = $definition['default'] ?? null;
324
+            $length  = $definition['length'] ?? null;
325
+            $properties = $definition['properties'] ?? null;
326
+
327
+            if (isset($definition['relation']) && $type !== null) {
328
+                $tableName = $this->getTableName();
329
+                $msg = "Column \"$colName\" on table \"$tableName\": ";
330
+                $msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
331
+                throw new ActiveRecordException($msg);
332
+            } else if (isset($definition['relation'])) {
333
+                $type = self::COLUMN_TYPE_ID;
334
+            }
335
+
336
+            $columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
337
+        }
338
+
339
+        // Sort table (first column is id, the remaining are alphabetically sorted)
340
+        $columnStatements = $this->sortColumnStatements($columnStatements);
341
+
342
+        $sql = sprintf("CREATE TABLE %s (\n%s\n);", 
343
+            $this->getTableName(), 
344
+            implode(",\n", $columnStatements));
345
+
346
+        return $sql;
347
+    }
348
+
349
+    /**
350
+     * Creates the entity as a table in the database
351
+     */
352
+    public function createTable()
353
+    {
354
+        $this->pdo->query($this->buildCreateTableSQL());
355
+    }
356
+
357
+    /**
358
+     * builds a MySQL constraint statement for the given parameters
359
+     * @param string $parentTable
360
+     * @param string $parentColumn
361
+     * @param string $childTable
362
+     * @param string $childColumn
363
+     * @return string The MySQL table constraint string
364
+     */
365
+    protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
366
+    {
367
+        $template = <<<SQL
368 368
 ALTER TABLE `%s`
369 369
 ADD CONSTRAINT
370 370
 FOREIGN KEY (`%s`)
371 371
 REFERENCES `%s`(`%s`)
372 372
 ON DELETE CASCADE;
373 373
 SQL;
374
-		return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
375
-	}
376
-
377
-	/**
378
-	 * Iterates over the specified constraints in the table definition, 
379
-	 * 		and applies these to the database.
380
-	 */
381
-	public function createTableConstraints()
382
-	{
383
-		// Iterate over columns, check whether "relation" field exists, if so create constraint
384
-		foreach ($this->tableDefinition as $colName => $definition) {
385
-			if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
386
-				// Forge new relation
387
-				$target = $definition['relation'];
388
-				$constraintSql = $this->buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
389
-
390
-				$this->pdo->query($constraintSql);
391
-			} else if (isset($definition['relation'])) {
392
-				$msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
393
-					$colName,
394
-					$this->getTableName());
395
-				throw new ActiveRecordException($msg);
396
-			}
397
-		}
398
-	}
399
-
400
-	/**
401
-	 * Returns the name -> variable mapping for the table definition.
402
-	 * @return Array The mapping
403
-	 */
404
-	protected function getActiveRecordColumns()
405
-	{
406
-		$bindings = [];
407
-		foreach ($this->tableDefinition as $colName => $definition) {
408
-
409
-			// Ignore the id column (key) when inserting or updating
410
-			if ($colName == self::COLUMN_NAME_ID) {
411
-				continue;
412
-			}
413
-
414
-			$bindings[$colName] = &$definition['value'];
415
-		}
416
-		return $bindings;
417
-	}
418
-
419
-	/**
420
-	 * {@inheritdoc}
421
-	 */
422
-	public function create()
423
-	{
424
-		foreach ($this->registeredCreateHooks as $colName => $fn) {
425
-			$fn();
426
-		}
427
-
428
-		// Insert default values for not-null fields
429
-		foreach ($this->tableDefinition as $colName => $colDef) {
430
-			if ($this->tableDefinition[$colName]['value'] === null
431
-				&& isset($this->tableDefinition[$colName]['properties'])
432
-				&& $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
433
-				&& isset($this->tableDefinition[$colName]['default'])) {
434
-				$this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
435
-			}
436
-		}
437
-
438
-		try {
439
-			(new Query($this->getPdo(), $this->getTableName()))
440
-				->insert($this->getActiveRecordColumns())
441
-				->execute();
442
-
443
-			$this->setId(intval($this->getPdo()->lastInsertId()));
444
-		} catch (\PDOException $e) {
445
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
446
-		}
447
-
448
-		return $this;
449
-	}
450
-
451
-	/**
452
-	 * {@inheritdoc}
453
-	 */
454
-	public function read($id)
455
-	{
456
-		foreach ($this->registeredReadHooks as $colName => $fn) {
457
-			$fn();
458
-		}
459
-
460
-		try {
461
-			$row = (new Query($this->getPdo(), $this->getTableName()))
462
-				->select()
463
-				->where(Query::Equal('id', $id))
464
-				->execute()
465
-				->fetch();
466
-
467
-			if ($row === false) {
468
-				throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));
469
-			}
470
-
471
-			$this->fill($row)->setId($id);
472
-		} catch (\PDOException $e) {
473
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
474
-		}
475
-
476
-		return $this;
477
-	}
478
-
479
-	/**
480
-	 * {@inheritdoc}
481
-	 */
482
-	public function update()
483
-	{
484
-		foreach ($this->registeredUpdateHooks as $colName => $fn) {
485
-			$fn();
486
-		}
487
-
488
-		try {
489
-			(new Query($this->getPdo(), $this->getTableName()))
490
-				->update($this->getActiveRecordColumns())
491
-				->where(Query::Equal('id', $this->getId()))
492
-				->execute();
493
-		} catch (\PDOException $e) {
494
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
495
-		}
496
-
497
-		return $this;
498
-	}
499
-
500
-	/**
501
-	 * {@inheritdoc}
502
-	 */
503
-	public function delete()
504
-	{
505
-		foreach ($this->registeredDeleteHooks as $colName => $fn) {
506
-			$fn();
507
-		}
508
-
509
-		try {
510
-			(new Query($this->getPdo(), $this->getTableName()))
511
-				->delete()
512
-				->where(Query::Equal('id', $this->getId()))
513
-				->execute();
514
-
515
-			$this->setId(null);
516
-		} catch (\PDOException $e) {
517
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
518
-		}
519
-
520
-		return $this;
521
-	}
522
-
523
-	/**
524
-	 * {@inheritdoc}
525
-	 */
526
-	public function sync()
527
-	{
528
-		if (!$this->exists()) {
529
-			return $this->create();
530
-		}
531
-
532
-		return $this->update();
533
-	}
534
-
535
-	/**
536
-	 * {@inheritdoc}
537
-	 */
538
-	public function exists()
539
-	{
540
-		return $this->getId() !== null;
541
-	}
542
-
543
-	/**
544
-	 * {@inheritdoc}
545
-	 */
546
-	public function fill(array $attributes)
547
-	{
548
-		$columns = $this->getActiveRecordColumns();
549
-		$columns['id'] = &$this->id;
550
-
551
-		foreach ($attributes as $key => $value) {
552
-			if (array_key_exists($key, $columns)) {
553
-				$columns[$key] = $value;
554
-			}
555
-		}
556
-
557
-		return $this;
558
-	}
559
-
560
-	/**
561
-	 * {@inheritdoc}
562
-	 */
563
-	public function search(array $ignoredTraits = [])
564
-	{
565
-		$clauses = [];
566
-		foreach ($this->registeredSearchHooks as $column => $fn) {
567
-			if (!in_array($column, $ignoredTraits)) {
568
-				$clauses[] = $fn();
569
-			}
570
-		}
571
-
572
-		return new ActiveRecordQuery($this, $this->getTableName(), $clauses);
573
-	}
574
-
575
-	/**
576
-	 * Returns the PDO.
577
-	 *
578
-	 * @return \PDO the PDO.
579
-	 */
580
-	public function getPdo()
581
-	{
582
-		return $this->pdo;
583
-	}
584
-
585
-	/**
586
-	 * Set the PDO.
587
-	 *
588
-	 * @param \PDO $pdo
589
-	 * @return $this
590
-	 */
591
-	protected function setPdo($pdo)
592
-	{
593
-		$this->pdo = $pdo;
594
-
595
-		return $this;
596
-	}
597
-
598
-	/**
599
-	 * Returns the ID.
600
-	 *
601
-	 * @return null|int The ID.
602
-	 */
603
-	public function getId()
604
-	{
605
-		return $this->id;
606
-	}
607
-
608
-	/**
609
-	 * Set the ID.
610
-	 *
611
-	 * @param int $id
612
-	 * @return $this
613
-	 */
614
-	protected function setId($id)
615
-	{
616
-		$this->id = $id;
617
-
618
-		return $this;
619
-	}
620
-
621
-
622
-	public function newInstance()
623
-	{
624
-		return new static($this->pdo);
625
-	}
626
-
627
-	/**
628
-	 * Returns the active record table.
629
-	 *
630
-	 * @return string the active record table name.
631
-	 */
632
-	abstract protected function getTableName();
633
-
634
-	/**
635
-	 * Returns the active record columns.
636
-	 *
637
-	 * @return array the active record columns.
638
-	 */
639
-	abstract protected function getTableDefinition();
374
+        return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
375
+    }
376
+
377
+    /**
378
+     * Iterates over the specified constraints in the table definition, 
379
+     * 		and applies these to the database.
380
+     */
381
+    public function createTableConstraints()
382
+    {
383
+        // Iterate over columns, check whether "relation" field exists, if so create constraint
384
+        foreach ($this->tableDefinition as $colName => $definition) {
385
+            if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
386
+                // Forge new relation
387
+                $target = $definition['relation'];
388
+                $constraintSql = $this->buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
389
+
390
+                $this->pdo->query($constraintSql);
391
+            } else if (isset($definition['relation'])) {
392
+                $msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
393
+                    $colName,
394
+                    $this->getTableName());
395
+                throw new ActiveRecordException($msg);
396
+            }
397
+        }
398
+    }
399
+
400
+    /**
401
+     * Returns the name -> variable mapping for the table definition.
402
+     * @return Array The mapping
403
+     */
404
+    protected function getActiveRecordColumns()
405
+    {
406
+        $bindings = [];
407
+        foreach ($this->tableDefinition as $colName => $definition) {
408
+
409
+            // Ignore the id column (key) when inserting or updating
410
+            if ($colName == self::COLUMN_NAME_ID) {
411
+                continue;
412
+            }
413
+
414
+            $bindings[$colName] = &$definition['value'];
415
+        }
416
+        return $bindings;
417
+    }
418
+
419
+    /**
420
+     * {@inheritdoc}
421
+     */
422
+    public function create()
423
+    {
424
+        foreach ($this->registeredCreateHooks as $colName => $fn) {
425
+            $fn();
426
+        }
427
+
428
+        // Insert default values for not-null fields
429
+        foreach ($this->tableDefinition as $colName => $colDef) {
430
+            if ($this->tableDefinition[$colName]['value'] === null
431
+                && isset($this->tableDefinition[$colName]['properties'])
432
+                && $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
433
+                && isset($this->tableDefinition[$colName]['default'])) {
434
+                $this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
435
+            }
436
+        }
437
+
438
+        try {
439
+            (new Query($this->getPdo(), $this->getTableName()))
440
+                ->insert($this->getActiveRecordColumns())
441
+                ->execute();
442
+
443
+            $this->setId(intval($this->getPdo()->lastInsertId()));
444
+        } catch (\PDOException $e) {
445
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
446
+        }
447
+
448
+        return $this;
449
+    }
450
+
451
+    /**
452
+     * {@inheritdoc}
453
+     */
454
+    public function read($id)
455
+    {
456
+        foreach ($this->registeredReadHooks as $colName => $fn) {
457
+            $fn();
458
+        }
459
+
460
+        try {
461
+            $row = (new Query($this->getPdo(), $this->getTableName()))
462
+                ->select()
463
+                ->where(Query::Equal('id', $id))
464
+                ->execute()
465
+                ->fetch();
466
+
467
+            if ($row === false) {
468
+                throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));
469
+            }
470
+
471
+            $this->fill($row)->setId($id);
472
+        } catch (\PDOException $e) {
473
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
474
+        }
475
+
476
+        return $this;
477
+    }
478
+
479
+    /**
480
+     * {@inheritdoc}
481
+     */
482
+    public function update()
483
+    {
484
+        foreach ($this->registeredUpdateHooks as $colName => $fn) {
485
+            $fn();
486
+        }
487
+
488
+        try {
489
+            (new Query($this->getPdo(), $this->getTableName()))
490
+                ->update($this->getActiveRecordColumns())
491
+                ->where(Query::Equal('id', $this->getId()))
492
+                ->execute();
493
+        } catch (\PDOException $e) {
494
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
495
+        }
496
+
497
+        return $this;
498
+    }
499
+
500
+    /**
501
+     * {@inheritdoc}
502
+     */
503
+    public function delete()
504
+    {
505
+        foreach ($this->registeredDeleteHooks as $colName => $fn) {
506
+            $fn();
507
+        }
508
+
509
+        try {
510
+            (new Query($this->getPdo(), $this->getTableName()))
511
+                ->delete()
512
+                ->where(Query::Equal('id', $this->getId()))
513
+                ->execute();
514
+
515
+            $this->setId(null);
516
+        } catch (\PDOException $e) {
517
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
518
+        }
519
+
520
+        return $this;
521
+    }
522
+
523
+    /**
524
+     * {@inheritdoc}
525
+     */
526
+    public function sync()
527
+    {
528
+        if (!$this->exists()) {
529
+            return $this->create();
530
+        }
531
+
532
+        return $this->update();
533
+    }
534
+
535
+    /**
536
+     * {@inheritdoc}
537
+     */
538
+    public function exists()
539
+    {
540
+        return $this->getId() !== null;
541
+    }
542
+
543
+    /**
544
+     * {@inheritdoc}
545
+     */
546
+    public function fill(array $attributes)
547
+    {
548
+        $columns = $this->getActiveRecordColumns();
549
+        $columns['id'] = &$this->id;
550
+
551
+        foreach ($attributes as $key => $value) {
552
+            if (array_key_exists($key, $columns)) {
553
+                $columns[$key] = $value;
554
+            }
555
+        }
556
+
557
+        return $this;
558
+    }
559
+
560
+    /**
561
+     * {@inheritdoc}
562
+     */
563
+    public function search(array $ignoredTraits = [])
564
+    {
565
+        $clauses = [];
566
+        foreach ($this->registeredSearchHooks as $column => $fn) {
567
+            if (!in_array($column, $ignoredTraits)) {
568
+                $clauses[] = $fn();
569
+            }
570
+        }
571
+
572
+        return new ActiveRecordQuery($this, $this->getTableName(), $clauses);
573
+    }
574
+
575
+    /**
576
+     * Returns the PDO.
577
+     *
578
+     * @return \PDO the PDO.
579
+     */
580
+    public function getPdo()
581
+    {
582
+        return $this->pdo;
583
+    }
584
+
585
+    /**
586
+     * Set the PDO.
587
+     *
588
+     * @param \PDO $pdo
589
+     * @return $this
590
+     */
591
+    protected function setPdo($pdo)
592
+    {
593
+        $this->pdo = $pdo;
594
+
595
+        return $this;
596
+    }
597
+
598
+    /**
599
+     * Returns the ID.
600
+     *
601
+     * @return null|int The ID.
602
+     */
603
+    public function getId()
604
+    {
605
+        return $this->id;
606
+    }
607
+
608
+    /**
609
+     * Set the ID.
610
+     *
611
+     * @param int $id
612
+     * @return $this
613
+     */
614
+    protected function setId($id)
615
+    {
616
+        $this->id = $id;
617
+
618
+        return $this;
619
+    }
620
+
621
+
622
+    public function newInstance()
623
+    {
624
+        return new static($this->pdo);
625
+    }
626
+
627
+    /**
628
+     * Returns the active record table.
629
+     *
630
+     * @return string the active record table name.
631
+     */
632
+    abstract protected function getTableName();
633
+
634
+    /**
635
+     * Returns the active record columns.
636
+     *
637
+     * @return array the active record columns.
638
+     */
639
+    abstract protected function getTableDefinition();
640 640
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -268,7 +268,7 @@
 block discarded – undo
268 268
 		}
269 269
 
270 270
 		if ($default !== NULL) {
271
-			$stmnt .= 'DEFAULT ' . var_export($default, true) . ' ';
271
+			$stmnt .= 'DEFAULT '.var_export($default, true).' ';
272 272
 		}
273 273
 
274 274
 		if ($properties & ColumnProperty::AUTO_INCREMENT) {
Please login to merge, or discard this patch.