Test Failed
Push — v2 ( a0d121...604284 )
by Berend
02:50
created

AutoApi   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 349
Duplicated Lines 0 %

Test Coverage

Coverage 90.09%

Importance

Changes 0
Metric Value
wmc 46
eloc 125
dl 0
loc 349
ccs 100
cts 111
cp 0.9009
rs 8.72
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A apiRead() 0 4 1
A validateExcessKeys() 0 10 3
A filterInputColumns() 0 9 3
A validateImmutableColumns() 0 11 4
A toArray() 0 10 3
A apiSearch() 0 30 5
A validateMissingKeys() 0 27 6
A syncInstances() 0 4 2
A apiCreate() 0 44 4
A loadData() 0 5 3
A apiUpdate() 0 44 4
B validateInputValues() 0 29 8

How to fix   Complexity   

Complex Class

Complex classes like AutoApi often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AutoApi, and based on these observations, apply Extract Interface, too.

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(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);
0 ignored issues
show
Bug introduced by
It seems like search() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

37
		/** @scrutinizer ignore-call */ 
38
  $query = $this->search($ignoredTraits);
Loading history...
38
39
		$orderColumn = $queryparams['order_by'] ?? null;
40
		$orderDirection = $queryparams['order_direction'] ?? null;
41 4
		if ($orderColumn !== null) {
42
			$query->orderBy($orderColumn, $orderDirection);
43 4
		}
44 4
		
45 4
		$limit = $queryparams['limit'] ?? null;
46 4
		if ($limit !== null) {
47
			$query->limit($limit);
48
		}
49
50 4
		$offset = $queryparams['offset'] ?? null;
51
		if ($offset !== null) {
52
			$query->offset($offset);
53 1
		}
54
55 1
		$results = $query->fetchAll();
56 1
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 3
	{
67
		$output = [];
68 3
		foreach ($this->tableDefinition as $colName => $definition) {
69 3
			if (in_array($colName, $fieldWhitelist)) {
70
				$output[$colName] = $definition['value'];
71 3
			}
72
		}
73 7
74
		return $output;
75 7
	}
76 7
77 7
	public function apiRead($id, Array $fieldWhitelist)
78 7
	{
79
		$this->read($id);
80
		return $this->toArray($fieldWhitelist);
81 7
	}
82
83
	/* =============================================================
84 7
	 * ===================== Constraint validation =================
85
	 * ============================================================= */
86 7
87 7
	/**
88 7
	 * Copy all table variables between two instances
89 1
	 */
90 7
	private function syncInstances($to, $from)
91
	{
92
		foreach ($to->tableDefinition as $colName => $definition) {
93 7
			$definition['value'] = $from->tableDefinition[$colName]['value'];
94
		}
95
	}
96 2
97
	private function filterInputColumns($input, $whitelist)
98 2
	{
99 2
		$filteredInput = $input;
100 2
		foreach ($input as $colName => $value) {
101 2
			if (!in_array($colName, $whitelist)) {
102 2
				unset($filteredInput[$colName]);
103 2
			}
104
		}
105
		return $filteredInput;
106 2
	}
107
108
	private function validateExcessKeys($input)
109 7
	{
110
		$errors = [];
111 7
		foreach ($input as $colName => $value) {
112 7
			if (!array_key_exists($colName, $this->tableDefinition)) {
113
				$errors[$colName] = "Unknown input field";
114 7
				continue;
115 7
			}
116 5
		}
117
		return $errors;
118
	}
119 5
120 5
	private function validateImmutableColumns($input)
121 1
	{
122
		$errors = [];
123
		foreach ($this->tableDefinition as $colName => $definition) {
124
			$property = $definition['properties'] ?? null;
125
			if (array_key_exists($colName, $input)
126 7
				&& $property & ColumnProperty::IMMUTABLE) {
127 7
				$errors[$colName] = "Field cannot be changed";
128 7
			}
129
		}
130
		return $errors;
131
	}
132
133 7
	private function validateInputValues($input)
134
	{
135
		$errors = [];
136
		foreach ($this->tableDefinition as $colName => $definition) {
137 7
			// Validation check 1: If validate function is present
138
			if (array_key_exists($colName, $input) 
139
				&& is_callable($definition['validate'] ?? null)) {
140
				$inputValue = $input[$colName];
141
142
				// If validation function fails
143 7
				[$status, $message] = $definition['validate']($inputValue);
144
				if (!$status) {
145 7
					$errors[$colName] = $message;
146
				}	
147 7
			}
148 7
149 7
			// Validation check 2: If relation column, check whether entity exists
150 7
			$properties = $definition['properties'] ?? null;
151
			if (isset($definition['relation'])
152
				&& ($properties & ColumnProperty::NOT_NULL)) {
153
				$instance = clone $definition['relation'];
154
				try {
155
					$instance->read($input[$colName] ?? null);
156
				} catch (ActiveRecordException $e) {
157
					$errors[$colName] = "Entity for this value doesn't exist";
158
				}
159
			}
160 7
		}
161 7
		return $errors;
162 7
	}
163
164 7
	/**
165 7
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
166
	 */
167
	private function validateMissingKeys()
168
	{
169 7
		$errors = [];
170
171
		foreach ($this->tableDefinition as $colName => $colDefinition) {
172
			$default = $colDefinition['default'] ?? null;
173
			$properties = $colDefinition['properties'] ?? null;
174
			$value = $colDefinition['value'];
175
176 7
			// If nullable and default not set => null
177
			// If nullable and default null => default (null)
178 7
			// If nullable and default set => default (value)
179 7
180 7
			// if not nullable and default not set => error
181
			// if not nullable and default null => error
182
			// if not nullable and default st => default (value)
183 7
			// => if not nullable and default null and value not set => error message in this method
184
			if ($properties & ColumnProperty::NOT_NULL
185
				&& $default === null
186
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
187
				// && !array_key_exists($colName, $input)
188
				&& $value === null) {
189
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
190
			}
191 5
		}
192
193
		return $errors;
194 5
	}
195 5
196
	/**
197
	 * Copies the values for entries in the input with matching variable names in the record definition
198 5
	 * @param Array $input The input data to be loaded into $this record
199
	 */
200
	private function loadData($input)
201 5
	{
202
		foreach ($this->tableDefinition as $colName => $definition) {
203
			if (array_key_exists($colName, $input)) {
204 5
				$definition['value'] = $input[$colName];
205
			}
206
		}
207 5
	}
208
209
	/**
210 5
	 * @param Array $input Associative array of input values
211
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
212
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
213
	 * 					of the modified data.
214
	 */
215 5
	public function apiCreate($input, Array $fieldWhitelist)
216
	{
217
		// Clone $this to new instance (for restoring if validation goes wrong)
218 5
		$transaction = $this->newInstance();
0 ignored issues
show
Bug introduced by
It seems like newInstance() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

218
		/** @scrutinizer ignore-call */ 
219
  $transaction = $this->newInstance();
Loading history...
219 2
		$errors = [];
220
221
		// Filter out all non-whitelisted input values
222 2
		$input = $this->filterInputColumns($input, $fieldWhitelist);
223 2
224 2
		// Validate excess keys
225
		$errors += $transaction->validateExcessKeys($input);
226 2
227
		// Validate input values (using validation function)
228
		$errors += $transaction->validateInputValues($input);
229
230
		// "Copy" data into transaction
231
		$transaction->loadData($input);
232 2
233
		// Run create hooks
234 3
		foreach ($this->registeredCreateHooks as $colName => $fn) {
235
			$fn();
236
		}
237
238
		// Validate missing keys
239
		$errors += $transaction->validateMissingKeys();
240
241
		// If no errors, commit the pending data
242
		if (empty($errors)) {
243
			$this->syncInstances($this, $transaction);
244 2
245
			try {
246 2
				(new Query($this->getPdo(), $this->getTableName()))
247 2
					->insert($this->getActiveRecordColumns())
248
					->execute();
249
250 2
				$this->setId(intval($this->getPdo()->lastInsertId()));
251
			} catch (\PDOException $e) {
252
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
253 2
				throw new ActiveRecordException($e->getMessage(), 0, $e);
254
			}
255
256 2
			return [null, $this->toArray($fieldWhitelist)];
257
		} else {
258
			return [$errors, null];
259 2
		}
260
	}
261
262 2
	/**
263
	 * @param Array $input Associative array of input values
264
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
265 2
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
266
	 * 					of the modified data.
267
	 */
268
	public function apiUpdate($input, Array $fieldWhitelist)
269
	{
270 2
		$transaction = $this->newInstance();
271
		$errors = [];
272
273 2
		// Filter out all non-whitelisted input values
274 1
		$input = $this->filterInputColumns($input, $fieldWhitelist);
275
276
		// Check for excess keys
277 1
		$errors += $transaction->validateExcessKeys($input);
278 1
279 1
		// Check for immutable keys
280 1
		$errors += $transaction->validateImmutableColumns($input);
281
282
		// Validate input values (using validation function)
283
		$errors += $transaction->validateInputValues($input);
284
285 1
		// "Copy" data into transaction
286
		$transaction->loadData($input);
287 1
288
		// Run create hooks
289
		foreach ($this->registeredUpdateHooks as $colName => $fn) {
290
			$fn();
291
		}
292
293
		// Validate missing keys
294
		$errors += $transaction->validateMissingKeys();
295
296
		// Update database
297
		if (empty($errors)) {
298
			$this->syncInstances($this, $transaction);
299
300
			try {
301
				(new Query($this->getPdo(), $this->getTableName()))
302
					->update($this->getActiveRecordColumns())
303
					->where(Query::Equal('id', $this->getId()))
0 ignored issues
show
Bug introduced by
It seems like $this->getId() can also be of type integer; however, parameter $right of miBadger\Query\Query::Equal() does only seem to accept miBadger\Query\any, maybe add an additional type check? ( Ignorable by Annotation )

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

303
					->where(Query::Equal('id', /** @scrutinizer ignore-type */ $this->getId()))
Loading history...
304
					->execute();
305
			} catch (\PDOException $e) {
306
				throw new ActiveRecordException($e->getMessage(), 0, $e);
307
			}
308
309
			return [null, $this->toArray($fieldWhitelist)];
310
		} else {
311
			return [$errors, null];
312
		}
313
	}
314
315
	/**
316
	 * Returns this active record after reading the attributes from the entry with the given identifier.
317
	 *
318
	 * @param mixed $id
319
	 * @return $this
320
	 * @throws ActiveRecordException on failure.
321
	 */
322
	abstract public function read($id);
323
324
	/**
325
	 * Returns the PDO.
326
	 *
327
	 * @return \PDO the PDO.
328
	 */
329
	abstract public function getPdo();
330
331
	/**
332
	 * Set the ID.
333
	 *
334
	 * @param int $id
335
	 * @return $this
336
	 */
337
	abstract protected function setId($id);
338
339
	/**
340
	 * Returns the ID.
341
	 *
342
	 * @return null|int The ID.
343
	 */
344
	abstract protected function getId();
345
346
	/**
347
	 * Returns the active record table.
348
	 *
349
	 * @return string the active record table name.
350
	 */
351
	abstract protected function getTableName();
352
353
	/**
354
	 * Returns the name -> variable mapping for the table definition.
355
	 * @return Array The mapping
356
	 */
357
	abstract protected function getActiveRecordColumns();
358
}
359