Test Failed
Push — v2 ( c354b7...e39573 )
by Berend
03:33
created
src/Traits/ManyToManyRelation.php 1 patch
Indentation   +109 added lines, -109 removed lines patch added patch discarded remove patch
@@ -8,115 +8,115 @@
 block discarded – undo
8 8
 
9 9
 Trait ManyToManyRelation
10 10
 {
11
-	// These variables are relevant for internal bookkeeping (constraint generation etc)
12
-
13
-	/** @var string The name of the left column of the relation. */
14
-	private $_leftColumnName;
15
-
16
-	/** @var string The name of the right column of the relation. */
17
-	private $_rightColumnName;
18
-
19
-	/** @var string The name of the left table of the relation. */
20
-	private $_leftEntityTable;
21
-
22
-	/** @var string The name of the right table of the relation. */
23
-	private $_rightEntityTable;
24
-
25
-	/** @var \PDO The PDO object. */
26
-	protected $pdo;
27
-	/**
28
-	 * Initializes the the ManyToManyRelation trait on the included object
29
-	 * 
30
-	 * @param AbstractActiveRecord $leftEntity The left entity of the relation
31
-	 * @param int $leftVariable The reference to the variable where the id for the left entity will be stored
32
-	 * @param AbstractActiveRecord $rightEntity The left entity of the relation
33
-	 * @param int $leftVariable The reference to the variable where the id for the right entity will be stored
34
-	 * @return void
35
-	 */
36
-	protected function initManyToManyRelation(AbstractActiveRecord $leftEntity, &$leftVariable, AbstractActiveRecord $rightEntity, &$rightVariable)
37
-	{
38
-		$this->_leftEntityTable = $leftEntity->getTableName();
39
-		$this->_rightEntityTable = $rightEntity->getTableName();
40
-
41
-		if (get_class($leftEntity) === get_class($rightEntity)) {
42
-			$this->_leftColumnName = sprintf("id_%s_left", $leftEntity->getTableName());
43
-			$this->_rightColumnName = sprintf("id_%s_right", $rightEntity->getTableName());
44
-		} else {
45
-			$this->_leftColumnName = sprintf("id_%s", $leftEntity->getTableName());
46
-			$this->_rightColumnName = sprintf("id_%s", $rightEntity->getTableName());
47
-		}
48
-
49
-		$this->extendTableDefinition($this->_leftColumnName, [
50
-			'value' => &$leftVariable,
51
-			'validate' => null,
52
-			'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
53
-			'properties' => ColumnProperty::NOT_NULL
54
-		]);
55
-
56
-		$this->extendTableDefinition($this->_rightColumnName, [
57
-			'value' => &$rightVariable,
58
-			'validate' => null,
59
-			'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
60
-			'properties' => ColumnProperty::NOT_NULL
61
-		]);
62
-	}
63
-
64
-	/**
65
-	 * Build the constraints for the many-to-many relation table
66
-	 * @return void
67
-	 */
68
-	public function createTableConstraints()
69
-	{
70
-		$childTable = $this->getTableName();
71
-
72
-		$leftParentTable = $this->_leftEntityTable;
73
-		$rightParentTable = $this->_rightEntityTable;
74
-
75
-		$leftConstraint = $this->buildConstraint($leftParentTable, 'id', $childTable, $this->_leftColumnName);
76
-		$rightConstraint = $this->buildConstraint($rightParentTable, 'id', $childTable, $this->_rightColumnName);
77
-
78
-		$this->pdo->query($leftConstraint);
79
-		$this->pdo->query($rightConstraint);
80
-	}
81
-
82
-	/**
83
-	 * @return void
84
-	 */	
85
-	abstract public function getTableName();
86
-
87
-	/**
88
-	 * @return void
89
-	 */
90
-	abstract protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn);
91
-
92
-	/**
93
-	 * @return void
94
-	 */
95
-	abstract protected function extendTableDefinition($columnName, $definition);
11
+    // These variables are relevant for internal bookkeeping (constraint generation etc)
12
+
13
+    /** @var string The name of the left column of the relation. */
14
+    private $_leftColumnName;
15
+
16
+    /** @var string The name of the right column of the relation. */
17
+    private $_rightColumnName;
18
+
19
+    /** @var string The name of the left table of the relation. */
20
+    private $_leftEntityTable;
21
+
22
+    /** @var string The name of the right table of the relation. */
23
+    private $_rightEntityTable;
24
+
25
+    /** @var \PDO The PDO object. */
26
+    protected $pdo;
27
+    /**
28
+     * Initializes the the ManyToManyRelation trait on the included object
29
+     * 
30
+     * @param AbstractActiveRecord $leftEntity The left entity of the relation
31
+     * @param int $leftVariable The reference to the variable where the id for the left entity will be stored
32
+     * @param AbstractActiveRecord $rightEntity The left entity of the relation
33
+     * @param int $leftVariable The reference to the variable where the id for the right entity will be stored
34
+     * @return void
35
+     */
36
+    protected function initManyToManyRelation(AbstractActiveRecord $leftEntity, &$leftVariable, AbstractActiveRecord $rightEntity, &$rightVariable)
37
+    {
38
+        $this->_leftEntityTable = $leftEntity->getTableName();
39
+        $this->_rightEntityTable = $rightEntity->getTableName();
40
+
41
+        if (get_class($leftEntity) === get_class($rightEntity)) {
42
+            $this->_leftColumnName = sprintf("id_%s_left", $leftEntity->getTableName());
43
+            $this->_rightColumnName = sprintf("id_%s_right", $rightEntity->getTableName());
44
+        } else {
45
+            $this->_leftColumnName = sprintf("id_%s", $leftEntity->getTableName());
46
+            $this->_rightColumnName = sprintf("id_%s", $rightEntity->getTableName());
47
+        }
48
+
49
+        $this->extendTableDefinition($this->_leftColumnName, [
50
+            'value' => &$leftVariable,
51
+            'validate' => null,
52
+            'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
53
+            'properties' => ColumnProperty::NOT_NULL
54
+        ]);
55
+
56
+        $this->extendTableDefinition($this->_rightColumnName, [
57
+            'value' => &$rightVariable,
58
+            'validate' => null,
59
+            'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
60
+            'properties' => ColumnProperty::NOT_NULL
61
+        ]);
62
+    }
63
+
64
+    /**
65
+     * Build the constraints for the many-to-many relation table
66
+     * @return void
67
+     */
68
+    public function createTableConstraints()
69
+    {
70
+        $childTable = $this->getTableName();
71
+
72
+        $leftParentTable = $this->_leftEntityTable;
73
+        $rightParentTable = $this->_rightEntityTable;
74
+
75
+        $leftConstraint = $this->buildConstraint($leftParentTable, 'id', $childTable, $this->_leftColumnName);
76
+        $rightConstraint = $this->buildConstraint($rightParentTable, 'id', $childTable, $this->_rightColumnName);
77
+
78
+        $this->pdo->query($leftConstraint);
79
+        $this->pdo->query($rightConstraint);
80
+    }
81
+
82
+    /**
83
+     * @return void
84
+     */	
85
+    abstract public function getTableName();
86
+
87
+    /**
88
+     * @return void
89
+     */
90
+    abstract protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn);
91
+
92
+    /**
93
+     * @return void
94
+     */
95
+    abstract protected function extendTableDefinition($columnName, $definition);
96 96
 	
97
-	/**
98
-	 * @return void
99
-	 */
100
-	abstract protected function registerSearchHook($columnName, $fn);
101
-
102
-	/**
103
-	 * @return void
104
-	 */
105
-	abstract protected function registerDeleteHook($columnName, $fn);
106
-
107
-	/**
108
-	 * @return void
109
-	 */
110
-	abstract protected function registerUpdateHook($columnName, $fn);
111
-
112
-	/**
113
-	 * @return void
114
-	 */
115
-	abstract protected function registerReadHook($columnName, $fn);
116
-
117
-	/**
118
-	 * @return void
119
-	 */
120
-	abstract protected function registerCreateHook($columnName, $fn);
97
+    /**
98
+     * @return void
99
+     */
100
+    abstract protected function registerSearchHook($columnName, $fn);
101
+
102
+    /**
103
+     * @return void
104
+     */
105
+    abstract protected function registerDeleteHook($columnName, $fn);
106
+
107
+    /**
108
+     * @return void
109
+     */
110
+    abstract protected function registerUpdateHook($columnName, $fn);
111
+
112
+    /**
113
+     * @return void
114
+     */
115
+    abstract protected function registerReadHook($columnName, $fn);
116
+
117
+    /**
118
+     * @return void
119
+     */
120
+    abstract protected function registerCreateHook($columnName, $fn);
121 121
 
122 122
 }
Please login to merge, or discard this patch.
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.