Test Failed
Push — v2 ( c354b7...e39573 )
by Berend
03:33
created
src/Traits/AutoApi.php 1 patch
Indentation   +367 added lines, -367 removed lines patch added patch discarded remove patch
@@ -9,386 +9,386 @@
 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, int $maxResultLimit = 100)
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, int $maxResultLimit = 100)
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'] ?? $maxResultLimit;
53
-		$query->limit($limit);
54
-
55
-		$offset = $queryParams['search_offset'] ?? 0;
56
-		$query->offset($offset);
57
-
58
-		if ($whereClause !== null) {
59
-			$query->where($whereClause);
60
-		}
61
-
62
-		$numPages = $query->getNumberOfPages();
63
-		$currentPage = $query->getCurrentPage();
64
-
65
-		// Fetch results
66
-		$results = $query->fetchAll();
67
-		$resultsArray = [];
68
-		foreach ($results as $result) {
69
-			$resultsArray[] = $result->toArray($fieldWhitelist);
70
-		}
71
-
72
-		return [
73
-			'search_offset' => $offset,
74
-			'search_limit' => $limit,
75
-			'search_order_by' => $orderColumn,
76
-			'search_order_direction' => $orderDirection,
77
-			'search_pages' => $numPages,
78
-			'search_current' => $currentPage,
79
-			'data' => $resultsArray
80
-		];
81
-	}
82
-
83
-	public function toArray($fieldWhitelist)
84
-	{
85
-		$output = [];
86
-		foreach ($this->tableDefinition as $colName => $definition) {
87
-			if (in_array($colName, $fieldWhitelist)) {
88
-				$output[$colName] = $definition['value'];
89
-			}
90
-		}
91
-
92
-		return $output;
93
-	}
94
-
95
-	public function apiRead($id, Array $fieldWhitelist)
96
-	{
97
-		// @TODO: Should apiRead throw exception or return null on fail?
98
-		$this->read($id);
99
-		return $this->toArray($fieldWhitelist);
100
-	}
101
-
102
-	/* =============================================================
52
+        $limit = $queryParams['search_limit'] ?? $maxResultLimit;
53
+        $query->limit($limit);
54
+
55
+        $offset = $queryParams['search_offset'] ?? 0;
56
+        $query->offset($offset);
57
+
58
+        if ($whereClause !== null) {
59
+            $query->where($whereClause);
60
+        }
61
+
62
+        $numPages = $query->getNumberOfPages();
63
+        $currentPage = $query->getCurrentPage();
64
+
65
+        // Fetch results
66
+        $results = $query->fetchAll();
67
+        $resultsArray = [];
68
+        foreach ($results as $result) {
69
+            $resultsArray[] = $result->toArray($fieldWhitelist);
70
+        }
71
+
72
+        return [
73
+            'search_offset' => $offset,
74
+            'search_limit' => $limit,
75
+            'search_order_by' => $orderColumn,
76
+            'search_order_direction' => $orderDirection,
77
+            'search_pages' => $numPages,
78
+            'search_current' => $currentPage,
79
+            'data' => $resultsArray
80
+        ];
81
+    }
82
+
83
+    public function toArray($fieldWhitelist)
84
+    {
85
+        $output = [];
86
+        foreach ($this->tableDefinition as $colName => $definition) {
87
+            if (in_array($colName, $fieldWhitelist)) {
88
+                $output[$colName] = $definition['value'];
89
+            }
90
+        }
91
+
92
+        return $output;
93
+    }
94
+
95
+    public function apiRead($id, Array $fieldWhitelist)
96
+    {
97
+        // @TODO: Should apiRead throw exception or return null on fail?
98
+        $this->read($id);
99
+        return $this->toArray($fieldWhitelist);
100
+    }
101
+
102
+    /* =============================================================
103 103
 	 * ===================== Constraint validation =================
104 104
 	 * ============================================================= */
105 105
 
106
-	/**
107
-	 * Copy all table variables between two instances
108
-	 */
109
-	public function syncInstanceFrom($from)
110
-	{
111
-		foreach ($this->tableDefinition as $colName => $definition) {
112
-			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
113
-		}
114
-	}
115
-
116
-	private function filterInputColumns($input, $whitelist)
117
-	{
118
-		$filteredInput = $input;
119
-		foreach ($input as $colName => $value) {
120
-			if (!in_array($colName, $whitelist)) {
121
-				unset($filteredInput[$colName]);
122
-			}
123
-		}
124
-		return $filteredInput;
125
-	}
126
-
127
-	private function validateExcessKeys($input)
128
-	{
129
-		$errors = [];
130
-		foreach ($input as $colName => $value) {
131
-			if (!array_key_exists($colName, $this->tableDefinition)) {
132
-				$errors[$colName] = "Unknown input field";
133
-				continue;
134
-			}
135
-		}
136
-		return $errors;
137
-	}
138
-
139
-	private function validateImmutableColumns($input)
140
-	{
141
-		$errors = [];
142
-		foreach ($this->tableDefinition as $colName => $definition) {
143
-			$property = $definition['properties'] ?? null;
144
-			if (array_key_exists($colName, $input)
145
-				&& $property & ColumnProperty::IMMUTABLE) {
146
-				$errors[$colName] = "Field cannot be changed";
147
-			}
148
-		}
149
-		return $errors;
150
-	}
151
-
152
-	/**
153
-	 * Checks whether input values are correct:
154
-	 * 1. Checks whether a value passes the validation function for that column
155
-	 * 2. Checks whether a value supplied to a relationship column is a valid value
156
-	 */
157
-	private function validateInputValues($input)
158
-	{
159
-		$errors = [];
160
-		foreach ($this->tableDefinition as $colName => $definition) {
161
-			// Validation check 1: If validate function is present
162
-			if (array_key_exists($colName, $input) 
163
-				&& is_callable($definition['validate'] ?? null)) {
164
-				$inputValue = $input[$colName];
165
-
166
-				// If validation function fails
167
-				[$status, $message] = $definition['validate']($inputValue);
168
-				if (!$status) {
169
-					$errors[$colName] = $message;
170
-				}	
171
-			}
172
-
173
-			// Validation check 2: If relation column, check whether entity exists
174
-			$properties = $definition['properties'] ?? null;
175
-			if (isset($definition['relation'])
176
-				&& ($properties & ColumnProperty::NOT_NULL)) {
177
-				$instance = clone $definition['relation'];
178
-				try {
179
-					$instance->read($input[$colName] ?? $definition['value'] ?? null);
180
-				} catch (ActiveRecordException $e) {
181
-					$errors[$colName] = "Entity for this value doesn't exist";
182
-				}
183
-			}
184
-		}
185
-		return $errors;
186
-	}
187
-
188
-	/**
189
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
190
-	 * Determines whether there are required columns for which no data is provided
191
-	 */
192
-	private function validateMissingKeys($input)
193
-	{
194
-		$errors = [];
195
-
196
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
197
-			$default = $colDefinition['default'] ?? null;
198
-			$properties = $colDefinition['properties'] ?? null;
199
-			$value = $colDefinition['value'];
200
-
201
-			// If nullable and default not set => null
202
-			// If nullable and default null => default (null)
203
-			// If nullable and default set => default (value)
204
-
205
-			// if not nullable and default not set => error
206
-			// if not nullable and default null => error
207
-			// if not nullable and default st => default (value)
208
-			// => if not nullable and default null and value not set (or null) => error message in this method
209
-			if ($properties & ColumnProperty::NOT_NULL
210
-				&& $default === null
211
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
212
-				&& (!array_key_exists($colName, $input) || $input[$colName] === null)
213
-				&& $value === null) {
214
-				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
215
-			} 
216
-		}
217
-
218
-		return $errors;
219
-	}
220
-
221
-	/**
222
-	 * Copies the values for entries in the input with matching variable names in the record definition
223
-	 * @param Array $input The input data to be loaded into $this record
224
-	 */
225
-	private function loadData($input)
226
-	{
227
-		foreach ($this->tableDefinition as $colName => $definition) {
228
-			if (array_key_exists($colName, $input)) {
229
-				$definition['value'] = $input[$colName];
230
-			}
231
-		}
232
-	}
233
-
234
-	/**
235
-	 * @param Array $input Associative array of input values
236
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
237
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
238
-	 * 					of the modified data.
239
-	 */
240
-	public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
241
-	{
242
-		// Clone $this to new instance (for restoring if validation goes wrong)
243
-		$transaction = $this->newInstance();
244
-		$errors = [];
245
-
246
-		// Filter out all non-whitelisted input values
247
-		$input = $this->filterInputColumns($input, $createWhitelist);
248
-
249
-		// Validate excess keys
250
-		$errors += $transaction->validateExcessKeys($input);
251
-
252
-		// Validate input values (using validation function)
253
-		$errors += $transaction->validateInputValues($input);
254
-
255
-		// "Copy" data into transaction
256
-		$transaction->loadData($input);
257
-
258
-		// Run create hooks
259
-		foreach ($transaction->registeredCreateHooks as $colName => $fn) {
260
-			$fn();
261
-		}
262
-
263
-		// Validate missing keys
264
-		$errors += $transaction->validateMissingKeys($input);
265
-
266
-		// If no errors, commit the pending data
267
-		if (empty($errors)) {
268
-			$this->syncInstanceFrom($transaction);
269
-
270
-			// Insert default values for not-null fields
271
-			foreach ($this->tableDefinition as $colName => $colDef) {
272
-				if ($this->tableDefinition[$colName]['value'] === null
273
-					&& isset($this->tableDefinition[$colName]['properties'])
274
-					&& $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
275
-					&& isset($this->tableDefinition[$colName]['default'])) {
276
-					$this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
277
-				}
278
-			}
279
-
280
-			try {
281
-				(new Query($this->getPdo(), $this->getTableName()))
282
-					->insert($this->getActiveRecordColumns())
283
-					->execute();
284
-
285
-				$this->setId(intval($this->getPdo()->lastInsertId()));
286
-			} catch (\PDOException $e) {
287
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
288
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
289
-			}
290
-
291
-			return [null, $this->toArray($readWhitelist)];
292
-		} else {
293
-			return [$errors, null];
294
-		}
295
-	}
296
-
297
-	/**
298
-	 * @param Array $input Associative array of input values
299
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
300
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
301
-	 * 					of the modified data.
302
-	 */
303
-	public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
304
-	{
305
-		$transaction = $this->newInstance();
306
-		$transaction->syncInstanceFrom($this);
307
-		$errors = [];
308
-
309
-		// Filter out all non-whitelisted input values
310
-		$input = $this->filterInputColumns($input, $updateWhitelist);
311
-
312
-		// Check for excess keys
313
-		$errors += $transaction->validateExcessKeys($input);
314
-
315
-		// Check for immutable keys
316
-		$errors += $transaction->validateImmutableColumns($input);
317
-
318
-		// Validate input values (using validation function)
319
-		$errors += $transaction->validateInputValues($input);
320
-
321
-		// "Copy" data into transaction
322
-		$transaction->loadData($input);
323
-
324
-		// Run create hooks
325
-		foreach ($transaction->registeredUpdateHooks as $colName => $fn) {
326
-			$fn();
327
-		}
328
-
329
-		// Validate missing keys
330
-		$errors += $transaction->validateMissingKeys($input);
331
-
332
-		// Update database
333
-		if (empty($errors)) {
334
-			$this->syncInstanceFrom($transaction);
335
-
336
-			try {
337
-				(new Query($this->getPdo(), $this->getTableName()))
338
-					->update($this->getActiveRecordColumns())
339
-					->where(Query::Equal('id', $this->getId()))
340
-					->execute();
341
-			} catch (\PDOException $e) {
342
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
343
-			}
344
-
345
-			return [null, $this->toArray($readWhitelist)];
346
-		} else {
347
-			return [$errors, null];
348
-		}
349
-	}
350
-
351
-	/**
352
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
353
-	 *
354
-	 * @param mixed $id
355
-	 * @return $this
356
-	 * @throws ActiveRecordException on failure.
357
-	 */
358
-	abstract public function read($id);
359
-
360
-	/**
361
-	 * Returns the PDO.
362
-	 *
363
-	 * @return \PDO the PDO.
364
-	 */
365
-	abstract public function getPdo();
366
-
367
-	/**
368
-	 * Set the ID.
369
-	 *
370
-	 * @param int $id
371
-	 * @return $this
372
-	 */
373
-	abstract protected function setId($id);
374
-
375
-	/**
376
-	 * Returns the ID.
377
-	 *
378
-	 * @return null|int The ID.
379
-	 */
380
-	abstract protected function getId();
381
-
382
-	/**
383
-	 * Returns the active record table.
384
-	 *
385
-	 * @return string the active record table name.
386
-	 */
387
-	abstract public function getTableName();
388
-
389
-	/**
390
-	 * Returns the name -> variable mapping for the table definition.
391
-	 * @return Array The mapping
392
-	 */
393
-	abstract protected function getActiveRecordColumns();
106
+    /**
107
+     * Copy all table variables between two instances
108
+     */
109
+    public function syncInstanceFrom($from)
110
+    {
111
+        foreach ($this->tableDefinition as $colName => $definition) {
112
+            $this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
113
+        }
114
+    }
115
+
116
+    private function filterInputColumns($input, $whitelist)
117
+    {
118
+        $filteredInput = $input;
119
+        foreach ($input as $colName => $value) {
120
+            if (!in_array($colName, $whitelist)) {
121
+                unset($filteredInput[$colName]);
122
+            }
123
+        }
124
+        return $filteredInput;
125
+    }
126
+
127
+    private function validateExcessKeys($input)
128
+    {
129
+        $errors = [];
130
+        foreach ($input as $colName => $value) {
131
+            if (!array_key_exists($colName, $this->tableDefinition)) {
132
+                $errors[$colName] = "Unknown input field";
133
+                continue;
134
+            }
135
+        }
136
+        return $errors;
137
+    }
138
+
139
+    private function validateImmutableColumns($input)
140
+    {
141
+        $errors = [];
142
+        foreach ($this->tableDefinition as $colName => $definition) {
143
+            $property = $definition['properties'] ?? null;
144
+            if (array_key_exists($colName, $input)
145
+                && $property & ColumnProperty::IMMUTABLE) {
146
+                $errors[$colName] = "Field cannot be changed";
147
+            }
148
+        }
149
+        return $errors;
150
+    }
151
+
152
+    /**
153
+     * Checks whether input values are correct:
154
+     * 1. Checks whether a value passes the validation function for that column
155
+     * 2. Checks whether a value supplied to a relationship column is a valid value
156
+     */
157
+    private function validateInputValues($input)
158
+    {
159
+        $errors = [];
160
+        foreach ($this->tableDefinition as $colName => $definition) {
161
+            // Validation check 1: If validate function is present
162
+            if (array_key_exists($colName, $input) 
163
+                && is_callable($definition['validate'] ?? null)) {
164
+                $inputValue = $input[$colName];
165
+
166
+                // If validation function fails
167
+                [$status, $message] = $definition['validate']($inputValue);
168
+                if (!$status) {
169
+                    $errors[$colName] = $message;
170
+                }	
171
+            }
172
+
173
+            // Validation check 2: If relation column, check whether entity exists
174
+            $properties = $definition['properties'] ?? null;
175
+            if (isset($definition['relation'])
176
+                && ($properties & ColumnProperty::NOT_NULL)) {
177
+                $instance = clone $definition['relation'];
178
+                try {
179
+                    $instance->read($input[$colName] ?? $definition['value'] ?? null);
180
+                } catch (ActiveRecordException $e) {
181
+                    $errors[$colName] = "Entity for this value doesn't exist";
182
+                }
183
+            }
184
+        }
185
+        return $errors;
186
+    }
187
+
188
+    /**
189
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
190
+     * Determines whether there are required columns for which no data is provided
191
+     */
192
+    private function validateMissingKeys($input)
193
+    {
194
+        $errors = [];
195
+
196
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
197
+            $default = $colDefinition['default'] ?? null;
198
+            $properties = $colDefinition['properties'] ?? null;
199
+            $value = $colDefinition['value'];
200
+
201
+            // If nullable and default not set => null
202
+            // If nullable and default null => default (null)
203
+            // If nullable and default set => default (value)
204
+
205
+            // if not nullable and default not set => error
206
+            // if not nullable and default null => error
207
+            // if not nullable and default st => default (value)
208
+            // => if not nullable and default null and value not set (or null) => error message in this method
209
+            if ($properties & ColumnProperty::NOT_NULL
210
+                && $default === null
211
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
212
+                && (!array_key_exists($colName, $input) || $input[$colName] === null)
213
+                && $value === null) {
214
+                $errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
215
+            } 
216
+        }
217
+
218
+        return $errors;
219
+    }
220
+
221
+    /**
222
+     * Copies the values for entries in the input with matching variable names in the record definition
223
+     * @param Array $input The input data to be loaded into $this record
224
+     */
225
+    private function loadData($input)
226
+    {
227
+        foreach ($this->tableDefinition as $colName => $definition) {
228
+            if (array_key_exists($colName, $input)) {
229
+                $definition['value'] = $input[$colName];
230
+            }
231
+        }
232
+    }
233
+
234
+    /**
235
+     * @param Array $input Associative array of input values
236
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
237
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
238
+     * 					of the modified data.
239
+     */
240
+    public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
241
+    {
242
+        // Clone $this to new instance (for restoring if validation goes wrong)
243
+        $transaction = $this->newInstance();
244
+        $errors = [];
245
+
246
+        // Filter out all non-whitelisted input values
247
+        $input = $this->filterInputColumns($input, $createWhitelist);
248
+
249
+        // Validate excess keys
250
+        $errors += $transaction->validateExcessKeys($input);
251
+
252
+        // Validate input values (using validation function)
253
+        $errors += $transaction->validateInputValues($input);
254
+
255
+        // "Copy" data into transaction
256
+        $transaction->loadData($input);
257
+
258
+        // Run create hooks
259
+        foreach ($transaction->registeredCreateHooks as $colName => $fn) {
260
+            $fn();
261
+        }
262
+
263
+        // Validate missing keys
264
+        $errors += $transaction->validateMissingKeys($input);
265
+
266
+        // If no errors, commit the pending data
267
+        if (empty($errors)) {
268
+            $this->syncInstanceFrom($transaction);
269
+
270
+            // Insert default values for not-null fields
271
+            foreach ($this->tableDefinition as $colName => $colDef) {
272
+                if ($this->tableDefinition[$colName]['value'] === null
273
+                    && isset($this->tableDefinition[$colName]['properties'])
274
+                    && $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
275
+                    && isset($this->tableDefinition[$colName]['default'])) {
276
+                    $this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
277
+                }
278
+            }
279
+
280
+            try {
281
+                (new Query($this->getPdo(), $this->getTableName()))
282
+                    ->insert($this->getActiveRecordColumns())
283
+                    ->execute();
284
+
285
+                $this->setId(intval($this->getPdo()->lastInsertId()));
286
+            } catch (\PDOException $e) {
287
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
288
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
289
+            }
290
+
291
+            return [null, $this->toArray($readWhitelist)];
292
+        } else {
293
+            return [$errors, null];
294
+        }
295
+    }
296
+
297
+    /**
298
+     * @param Array $input Associative array of input values
299
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
300
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
301
+     * 					of the modified data.
302
+     */
303
+    public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
304
+    {
305
+        $transaction = $this->newInstance();
306
+        $transaction->syncInstanceFrom($this);
307
+        $errors = [];
308
+
309
+        // Filter out all non-whitelisted input values
310
+        $input = $this->filterInputColumns($input, $updateWhitelist);
311
+
312
+        // Check for excess keys
313
+        $errors += $transaction->validateExcessKeys($input);
314
+
315
+        // Check for immutable keys
316
+        $errors += $transaction->validateImmutableColumns($input);
317
+
318
+        // Validate input values (using validation function)
319
+        $errors += $transaction->validateInputValues($input);
320
+
321
+        // "Copy" data into transaction
322
+        $transaction->loadData($input);
323
+
324
+        // Run create hooks
325
+        foreach ($transaction->registeredUpdateHooks as $colName => $fn) {
326
+            $fn();
327
+        }
328
+
329
+        // Validate missing keys
330
+        $errors += $transaction->validateMissingKeys($input);
331
+
332
+        // Update database
333
+        if (empty($errors)) {
334
+            $this->syncInstanceFrom($transaction);
335
+
336
+            try {
337
+                (new Query($this->getPdo(), $this->getTableName()))
338
+                    ->update($this->getActiveRecordColumns())
339
+                    ->where(Query::Equal('id', $this->getId()))
340
+                    ->execute();
341
+            } catch (\PDOException $e) {
342
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
343
+            }
344
+
345
+            return [null, $this->toArray($readWhitelist)];
346
+        } else {
347
+            return [$errors, null];
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Returns this active record after reading the attributes from the entry with the given identifier.
353
+     *
354
+     * @param mixed $id
355
+     * @return $this
356
+     * @throws ActiveRecordException on failure.
357
+     */
358
+    abstract public function read($id);
359
+
360
+    /**
361
+     * Returns the PDO.
362
+     *
363
+     * @return \PDO the PDO.
364
+     */
365
+    abstract public function getPdo();
366
+
367
+    /**
368
+     * Set the ID.
369
+     *
370
+     * @param int $id
371
+     * @return $this
372
+     */
373
+    abstract protected function setId($id);
374
+
375
+    /**
376
+     * Returns the ID.
377
+     *
378
+     * @return null|int The ID.
379
+     */
380
+    abstract protected function getId();
381
+
382
+    /**
383
+     * Returns the active record table.
384
+     *
385
+     * @return string the active record table name.
386
+     */
387
+    abstract public function getTableName();
388
+
389
+    /**
390
+     * Returns the name -> variable mapping for the table definition.
391
+     * @return Array The mapping
392
+     */
393
+    abstract protected function getActiveRecordColumns();
394 394
 }
Please login to merge, or discard this patch.