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