Completed
Push — v2 ( 7e0bb1...9c04df )
by Berend
09:41
created

AutoApi::validateMissingKeys()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 27
c 0
b 0
f 0
ccs 12
cts 12
cp 1
rs 9.2222
cc 6
nc 3
nop 0
crap 6
1
<?php
2
3
namespace miBadger\ActiveRecord\Traits;
4
5
use miBadger\ActiveRecord\ColumnProperty;
6
use miBadger\ActiveRecord\ActiveRecordException;
7
use miBadger\Query\Query;
8
9
trait AutoApi
10
{
11
	/* =======================================================================
12
	 * ===================== Automatic API Support ===========================
13
	 * ======================================================================= */
14
15
	/** @var array A map of column name to functions that hook the insert function */
16
	protected $registeredCreateHooks;
17
18
	/** @var array A map of column name to functions that hook the read function */
19
	protected $registeredReadHooks;
20
21
	/** @var array A map of column name to functions that hook the update function */
22
	protected $registeredUpdateHooks;
23
24
	/** @var array A map of column name to functions that hook the update function */
25
	protected $registeredDeleteHooks;	
26
27
	/** @var array A map of column name to functions that hook the search function */
28
	protected $registeredSearchHooks;
29
30
	/** @var array A list of table column definitions */
31
	protected $tableDefinition;
32
33
	public function apiSearch($inputs, $fieldWhitelist)
0 ignored issues
show
Unused Code introduced by
The parameter $fieldWhitelist is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

33
	public function apiSearch($inputs, /** @scrutinizer ignore-unused */ $fieldWhitelist)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $inputs is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

33
	public function apiSearch(/** @scrutinizer ignore-unused */ $inputs, $fieldWhitelist)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
34
	{
35
		// @TODO: How to handle this case?
36
		// => Default parameter names for searching? (limit, pagination, sort order etc)
37
		//		Find default names for this and store in class
38
		// => Limited search parameters? (We don't want to be able to search on a password field for example)
39
	}
40
41 4
	public function toArray($fieldWhitelist)
42
	{
43 4
		$output = [];
44 4
		foreach ($this->tableDefinition as $colName => $definition) {
45 4
			if (in_array($colName, $fieldWhitelist)) {
46 4
				$output[$colName] = $definition['value'];
47
			}
48
		}
49
50 4
		return $output;
51
	}
52
53 1
	public function apiRead($id, $fieldWhitelist)
54
	{
55 1
		$this->read($id);
56 1
		return $this->toArray($fieldWhitelist);
57
	}
58
59
	/* =============================================================
60
	 * ===================== Constraint validation =================
61
	 * ============================================================= */
62
63
	/**
64
	 * Copy all table variables between two instances
65
	 */
66 3
	private function syncInstances($to, $from)
67
	{
68 3
		foreach ($to->tableDefinition as $colName => $definition) {
69 3
			$definition['value'] = $from->tableDefinition[$colName]['value'];
70
		}
71 3
	}
72
73 7
	private function filterInputColumns($input, $whitelist)
74
	{
75 7
		$filteredInput = $input;
76 7
		foreach ($input as $colName => $value) {
77 7
			if (!in_array($colName, $whitelist)) {
78 7
				unset($filteredInput[$colName]);
79
			}
80
		}
81 7
		return $filteredInput;
82
	}
83
84 7
	private function validateExcessKeys($input)
85
	{
86 7
		$errors = [];
87 7
		foreach ($input as $colName => $value) {
88 7
			if (!array_key_exists($colName, $this->tableDefinition)) {
89 1
				$errors[$colName] = "Unknown input field";
90 7
				continue;
91
			}
92
		}
93 7
		return $errors;
94
	}
95
96 2
	private function validateImmutableColumns($input)
97
	{
98 2
		$errors = [];
99 2
		foreach ($this->tableDefinition as $colName => $definition) {
100 2
			$property = $definition['properties'] ?? null;
101 2
			if (array_key_exists($colName, $input)
102 2
				&& $property & ColumnProperty::IMMUTABLE) {
103 2
				$errors[$colName] = "Field cannot be changed";
104
			}
105
		}
106 2
		return $errors;
107
	}
108
109 7
	private function validateInputValues($input)
110
	{
111 7
		$errors = [];
112 7
		foreach ($this->tableDefinition as $colName => $definition) {
113
			// Validation check 1: If validate function is present
114 7
			if (array_key_exists($colName, $input) 
115 7
				&& is_callable($definition['validate'] ?? null)) {
116 5
				$inputValue = $input[$colName];
117
118
				// If validation function fails
119 5
				[$status, $message] = $definition['validate']($inputValue);
120 5
				if (!$status) {
121 1
					$errors[$colName] = $message;
122
				}	
123
			}
124
125
			// Validation check 2: If relation column, check whether entity exists
126 7
			$properties = $definition['properties'] ?? null;
127 7
			if (isset($definition['relation'])
128 7
				&& ($properties & ColumnProperty::NOT_NULL)) {
129
				$instance = clone $definition['relation'];
130
				try {
131
					$instance->read($input[$colName] ?? null);
132
				} catch (ActiveRecordException $e) {
133 7
					$errors[$colName] = "Entity for this value doesn't exist";
134
				}
135
			}
136
		}
137 7
		return $errors;
138
	}
139
140
	/**
141
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
142
	 */
143 7
	private function validateMissingKeys()
144
	{
145 7
		$errors = [];
146
147 7
		foreach ($this->tableDefinition as $colName => $colDefinition) {
148 7
			$default = $colDefinition['default'] ?? null;
149 7
			$properties = $colDefinition['properties'] ?? null;
150 7
			$value = $colDefinition['value'];
151
152
			// If nullable and default not set => null
153
			// If nullable and default null => default (null)
154
			// If nullable and default set => default (value)
155
156
			// if not nullable and default not set => error
157
			// if not nullable and default null => error
158
			// if not nullable and default st => default (value)
159
			// => if not nullable and default null and value not set => error message in this method
160 7
			if ($properties & ColumnProperty::NOT_NULL
161 7
				&& $default === null
162 7
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
163
				// && !array_key_exists($colName, $input)
164 7
				&& $value === null) {
165 7
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
166
			}
167
		}
168
169 7
		return $errors;
170
	}
171
172
	/**
173
	 * Copies the values for entries in the input with matching variable names in the record definition
174
	 * @param Array $input The input data to be loaded into $this record
175
	 */
176 7
	private function loadData($input)
177
	{
178 7
		foreach ($this->tableDefinition as $colName => $definition) {
179 7
			if (array_key_exists($colName, $input)) {
180 7
				$definition['value'] = $input[$colName];
181
			}
182
		}
183 7
	}
184
185
	/**
186
	 * @param Array $input Associative array of input values
187
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
188
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
189
	 * 					of the modified data.
190
	 */
191 5
	public function apiCreate($input, $fieldWhitelist)
192
	{
193
		// Clone $this to new instance (for restoring if validation goes wrong)
194 5
		$transaction = clone $this;
195 5
		$errors = [];
196
197
		// Filter out all non-whitelisted input values
198 5
		$input = $this->filterInputColumns($input, $fieldWhitelist);
199
200
		// Validate excess keys
201 5
		$errors += $transaction->validateExcessKeys($input);
202
203
		// Validate input values (using validation function)
204 5
		$errors += $transaction->validateInputValues($input);
205
206
		// "Copy" data into transaction
207 5
		$transaction->loadData($input);
208
209
		// Run create hooks
210 5
		foreach ($this->registeredCreateHooks as $colName => $fn) {
211
			$fn();
212
		}
213
214
		// Validate missing keys
215 5
		$errors += $transaction->validateMissingKeys();
216
217
		// If no errors, commit the pending data
218 5
		if (empty($errors)) {
219 2
			$this->syncInstances($this, $transaction);
220
221
			try {
222 2
				$q = (new Query($this->getPdo(), $this->getActiveRecordTable()))
0 ignored issues
show
Unused Code introduced by
The assignment to $q is dead and can be removed.
Loading history...
223 2
					->insert($this->getActiveRecordColumns())
224 2
					->execute();
225
226 2
				$this->setId(intval($this->getPdo()->lastInsertId()));
227
			} catch (\PDOException $e) {
228
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
229
				throw new ActiveRecordException($e->getMessage(), 0, $e);
230
			}
231
232 2
			return [null, $this->toArray($fieldWhitelist)];
233
		} else {
234 3
			return [$errors, null];
235
		}
236
	}
237
238
	/**
239
	 * @param Array $input Associative array of input values
240
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
241
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
242
	 * 					of the modified data.
243
	 */
244 2
	public function apiUpdate($input, $fieldWhitelist)
245
	{
246 2
		$transaction = clone $this;
247 2
		$errors = [];
248
249
		// Filter out all non-whitelisted input values
250 2
		$input = $this->filterInputColumns($input, $fieldWhitelist);
251
252
		// Check for excess keys
253 2
		$errors += $transaction->validateExcessKeys($input);
254
255
		// Check for immutable keys
256 2
		$errors += $transaction->validateImmutableColumns($input);
257
258
		// Validate input values (using validation function)
259 2
		$errors += $transaction->validateInputValues($input);
260
261
		// "Copy" data into transaction
262 2
		$transaction->loadData($input);
263
264
		// Run create hooks
265 2
		foreach ($this->registeredUpdateHooks as $colName => $fn) {
266
			$fn();
267
		}
268
269
		// Validate missing keys
270 2
		$errors += $transaction->validateMissingKeys();
271
272
		// Update database
273 2
		if (empty($errors)) {
274 1
			$this->syncInstances($this, $transaction);
275
276
			try {
277 1
				(new Query($this->getPdo(), $this->getActiveRecordTable()))
278 1
					->update($this->getActiveRecordColumns())
279 1
					->where('id', '=', $this->getId())
280 1
					->execute();
281
			} catch (\PDOException $e) {
282
				throw new ActiveRecordException($e->getMessage(), 0, $e);
283
			}
284
285 1
			return [null, $this->toArray($fieldWhitelist)];
286
		} else {
287 1
			return [$errors, null];
288
		}
289
	}
290
291
	/**
292
	 * Returns this active record after reading the attributes from the entry with the given identifier.
293
	 *
294
	 * @param mixed $id
295
	 * @return $this
296
	 * @throws ActiveRecordException on failure.
297
	 */
298
	abstract public function read($id);
299
300
	/**
301
	 * Returns the PDO.
302
	 *
303
	 * @return \PDO the PDO.
304
	 */
305
	abstract public function getPdo();
306
307
	/**
308
	 * Set the ID.
309
	 *
310
	 * @param int $id
311
	 * @return $this
312
	 */
313
	abstract protected function setId($id);
314
315
	/**
316
	 * Returns the ID.
317
	 *
318
	 * @return null|int The ID.
319
	 */
320
	abstract protected function getId();
321
322
	/**
323
	 * Returns the active record table.
324
	 *
325
	 * @return string the active record table name.
326
	 */
327
	abstract protected function getActiveRecordTable();
328
329
	/**
330
	 * Returns the name -> variable mapping for the table definition.
331
	 * @return Array The mapping
332
	 */
333
	abstract protected function getActiveRecordColumns();
334
}
335