Passed
Push — v2 ( e57a7e...6c89a0 )
by Berend
04:02
created

AutoApi::apiCreate()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 54
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 10.1536

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 54
ccs 23
cts 26
cp 0.8846
rs 7.6666
cc 10
nc 20
nop 3
crap 10.1536

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use miBadger\Query\QueryExpression;
9
10
trait AutoApi
11
{
12
	/* =======================================================================
13
	 * ===================== Automatic API Support ===========================
14
	 * ======================================================================= */
15
16
	/** @var array A map of column name to functions that hook the insert function */
17
	protected $registeredCreateHooks;
18
19
	/** @var array A map of column name to functions that hook the read function */
20
	protected $registeredReadHooks;
21
22
	/** @var array A map of column name to functions that hook the update function */
23
	protected $registeredUpdateHooks;
24
25
	/** @var array A map of column name to functions that hook the update function */
26
	protected $registeredDeleteHooks;	
27
28
	/** @var array A map of column name to functions that hook the search function */
29
	protected $registeredSearchHooks;
30
31
	/** @var array A list of table column definitions */
32
	protected $tableDefinition;
33
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 1
	public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null)
42
	{
43 1
		$query = $this->search();
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

43
		/** @scrutinizer ignore-call */ 
44
  $query = $this->search();
Loading history...
44
45
		// Build query
46 1
		$orderColumn = $queryParams['search_order_by'] ?? null;
47 1
		$orderDirection = $queryParams['search_order_direction'] ?? null;
48 1
		if ($orderColumn !== null) {
49 1
			$query->orderBy($orderColumn, $orderDirection);
50
		}
51
		
52 1
		$limit = $queryParams['search_limit'] ?? null;
53 1
		if ($limit !== null) {
54
			$query->limit($limit);
55
		}
56
57 1
		$offset = $queryParams['search_offset'] ?? null;
58 1
		if ($offset !== null) {
59
			$query->offset($offset);
60
		}
61
62 1
		if ($whereClause !== null) {
63 1
			$query->where($whereClause);
64
		}
65
66
		// Fetch results
67 1
		$results = $query->fetchAll();
68
69 1
		$resultsArray = [];
70 1
		foreach ($results as $result) {
71 1
			$resultsArray[] = $result->toArray($fieldWhitelist);
72
		}
73
74 1
		return $resultsArray;
75
	}
76
77 5
	public function toArray($fieldWhitelist)
78
	{
79 5
		$output = [];
80 5
		foreach ($this->tableDefinition as $colName => $definition) {
81 5
			if (in_array($colName, $fieldWhitelist)) {
82 5
				$output[$colName] = $definition['value'];
83
			}
84
		}
85
86 5
		return $output;
87
	}
88
89 1
	public function apiRead($id, Array $fieldWhitelist)
90
	{
91 1
		$this->read($id);
92 1
		return $this->toArray($fieldWhitelist);
93
	}
94
95
	/* =============================================================
96
	 * ===================== Constraint validation =================
97
	 * ============================================================= */
98
99
	/**
100
	 * Copy all table variables between two instances
101
	 */
102 6
	public function syncInstanceFrom($from)
103
	{
104 6
		foreach ($this->tableDefinition as $colName => $definition) {
105 6
			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
106
		}
107 6
	}
108
109 9
	private function filterInputColumns($input, $whitelist)
110
	{
111 9
		$filteredInput = $input;
112 9
		foreach ($input as $colName => $value) {
113 9
			if (!in_array($colName, $whitelist)) {
114 9
				unset($filteredInput[$colName]);
115
			}
116
		}
117 9
		return $filteredInput;
118
	}
119
120 9
	private function validateExcessKeys($input)
121
	{
122 9
		$errors = [];
123 9
		foreach ($input as $colName => $value) {
124 9
			if (!array_key_exists($colName, $this->tableDefinition)) {
125 1
				$errors[$colName] = "Unknown input field";
126 9
				continue;
127
			}
128
		}
129 9
		return $errors;
130
	}
131
132 4
	private function validateImmutableColumns($input)
133
	{
134 4
		$errors = [];
135 4
		foreach ($this->tableDefinition as $colName => $definition) {
136 4
			$property = $definition['properties'] ?? null;
137 4
			if (array_key_exists($colName, $input)
138 4
				&& $property & ColumnProperty::IMMUTABLE) {
139 4
				$errors[$colName] = "Field cannot be changed";
140
			}
141
		}
142 4
		return $errors;
143
	}
144
145
	/**
146
	 * Checks whether input values are correct:
147
	 * 1. Checks whether a value passes the validation function for that column
148
	 * 2. Checks whether a value supplied to a relationship column is a valid value
149
	 */
150 9
	private function validateInputValues($input)
151
	{
152 9
		$errors = [];
153 9
		foreach ($this->tableDefinition as $colName => $definition) {
154
			// Validation check 1: If validate function is present
155 9
			if (array_key_exists($colName, $input) 
156 9
				&& is_callable($definition['validate'] ?? null)) {
157 6
				$inputValue = $input[$colName];
158
159
				// If validation function fails
160 6
				[$status, $message] = $definition['validate']($inputValue);
161 6
				if (!$status) {
162 2
					$errors[$colName] = $message;
163
				}	
164
			}
165
166
			// Validation check 2: If relation column, check whether entity exists
167 9
			$properties = $definition['properties'] ?? null;
168 9
			if (isset($definition['relation'])
169 9
				&& ($properties & ColumnProperty::NOT_NULL)) {
170
				$instance = clone $definition['relation'];
171
				try {
172
					$instance->read($input[$colName] ?? null);
173
				} catch (ActiveRecordException $e) {
174 9
					$errors[$colName] = "Entity for this value doesn't exist";
175
				}
176
			}
177
		}
178 9
		return $errors;
179
	}
180
181
	/**
182
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
183
	 * Determines whether there are required columns for which no data is provided
184
	 */
185 9
	private function validateMissingKeys($input)
186
	{
187 9
		$errors = [];
188
189 9
		foreach ($this->tableDefinition as $colName => $colDefinition) {
190 9
			$default = $colDefinition['default'] ?? null;
191 9
			$properties = $colDefinition['properties'] ?? null;
192 9
			$value = $colDefinition['value'];
193
194
			// If nullable and default not set => null
195
			// If nullable and default null => default (null)
196
			// If nullable and default set => default (value)
197
198
			// if not nullable and default not set => error
199
			// if not nullable and default null => error
200
			// if not nullable and default st => default (value)
201
			// => if not nullable and default null and value not set (or null) => error message in this method
202 9
			if ($properties & ColumnProperty::NOT_NULL
203 9
				&& $default === null
204 9
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
205 9
				&& (!array_key_exists($colName, $input) || $input[$colName] === null)
206 9
				&& $value === null) {
207 9
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
208
			}
209
		}
210
211 9
		return $errors;
212
	}
213
214
	/**
215
	 * Copies the values for entries in the input with matching variable names in the record definition
216
	 * @param Array $input The input data to be loaded into $this record
217
	 */
218 9
	private function loadData($input)
219
	{
220 9
		foreach ($this->tableDefinition as $colName => $definition) {
221 9
			if (array_key_exists($colName, $input)) {
222 9
				$definition['value'] = $input[$colName];
223
			}
224
		}
225 9
	}
226
227
	/**
228
	 * @param Array $input Associative array of input values
229
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
230
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
231
	 * 					of the modified data.
232
	 */
233 5
	public function apiCreate($input, Array $createWhitelist, Array $readWhitelist)
234
	{
235
		// Clone $this to new instance (for restoring if validation goes wrong)
236 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

236
		/** @scrutinizer ignore-call */ 
237
  $transaction = $this->newInstance();
Loading history...
237 5
		$errors = [];
238
239
		// Filter out all non-whitelisted input values
240 5
		$input = $this->filterInputColumns($input, $createWhitelist);
241
242
		// Validate excess keys
243 5
		$errors += $transaction->validateExcessKeys($input);
244
245
		// Validate input values (using validation function)
246 5
		$errors += $transaction->validateInputValues($input);
247
248
		// "Copy" data into transaction
249 5
		$transaction->loadData($input);
250
251
		// Run create hooks
252 5
		foreach ($transaction->registeredCreateHooks as $colName => $fn) {
253
			$fn();
254
		}
255
256
		// Validate missing keys
257 5
		$errors += $transaction->validateMissingKeys($input);
258
259
		// If no errors, commit the pending data
260 5
		if (empty($errors)) {
261 2
			$this->syncInstanceFrom($transaction);
262
263
			// Insert default values for not-null fields
264 2
			foreach ($this->tableDefinition as $colName => $colDef) {
265 2
				if ($this->tableDefinition[$colName]['value'] === null
266 2
					&& isset($this->tableDefinition[$colName]['properties'])
267 2
					&& $this->tableDefinition[$colName]['properties'] && ColumnProperty::NOT_NULL > 0
268 2
					&& isset($this->tableDefinition[$colName]['default'])) {
269 2
					$this->tableDefinition[$colName]['value'] = $this->tableDefinition[$colName]['default'];
270
				}
271
			}
272
273
			try {
274 2
				(new Query($this->getPdo(), $this->getTableName()))
275 2
					->insert($this->getActiveRecordColumns())
276 2
					->execute();
277
278 2
				$this->setId(intval($this->getPdo()->lastInsertId()));
279
			} catch (\PDOException $e) {
280
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
281
				throw new ActiveRecordException($e->getMessage(), 0, $e);
282
			}
283
284 2
			return [null, $this->toArray($readWhitelist)];
285
		} else {
286 3
			return [$errors, null];
287
		}
288
	}
289
290
	/**
291
	 * @param Array $input Associative array of input values
292
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
293
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
294
	 * 					of the modified data.
295
	 */
296 4
	public function apiUpdate($input, Array $updateWhitelist, Array $readWhitelist)
297
	{
298 4
		$transaction = $this->newInstance();
299 4
		$transaction->syncInstanceFrom($this);
300 4
		$errors = [];
301
302
		// Filter out all non-whitelisted input values
303 4
		$input = $this->filterInputColumns($input, $updateWhitelist);
304
305
		// Check for excess keys
306 4
		$errors += $transaction->validateExcessKeys($input);
307
308
		// Check for immutable keys
309 4
		$errors += $transaction->validateImmutableColumns($input);
310
311
		// Validate input values (using validation function)
312 4
		$errors += $transaction->validateInputValues($input);
313
314
		// "Copy" data into transaction
315 4
		$transaction->loadData($input);
316
317
		// Run create hooks
318 4
		foreach ($transaction->registeredUpdateHooks as $colName => $fn) {
319
			$fn();
320
		}
321
322
		// Validate missing keys
323 4
		$errors += $transaction->validateMissingKeys($input);
324
325
		// Update database
326 4
		if (empty($errors)) {
327 1
			$this->syncInstanceFrom($transaction);
328
329
			try {
330 1
				(new Query($this->getPdo(), $this->getTableName()))
331 1
					->update($this->getActiveRecordColumns())
332 1
					->where(Query::Equal('id', $this->getId()))
333 1
					->execute();
334
			} catch (\PDOException $e) {
335
				throw new ActiveRecordException($e->getMessage(), 0, $e);
336
			}
337
338 1
			return [null, $this->toArray($readWhitelist)];
339
		} else {
340 3
			return [$errors, null];
341
		}
342
	}
343
344
	/**
345
	 * Returns this active record after reading the attributes from the entry with the given identifier.
346
	 *
347
	 * @param mixed $id
348
	 * @return $this
349
	 * @throws ActiveRecordException on failure.
350
	 */
351
	abstract public function read($id);
352
353
	/**
354
	 * Returns the PDO.
355
	 *
356
	 * @return \PDO the PDO.
357
	 */
358
	abstract public function getPdo();
359
360
	/**
361
	 * Set the ID.
362
	 *
363
	 * @param int $id
364
	 * @return $this
365
	 */
366
	abstract protected function setId($id);
367
368
	/**
369
	 * Returns the ID.
370
	 *
371
	 * @return null|int The ID.
372
	 */
373
	abstract protected function getId();
374
375
	/**
376
	 * Returns the active record table.
377
	 *
378
	 * @return string the active record table name.
379
	 */
380
	abstract protected function getTableName();
381
382
	/**
383
	 * Returns the name -> variable mapping for the table definition.
384
	 * @return Array The mapping
385
	 */
386
	abstract protected function getActiveRecordColumns();
387
}
388