Completed
Push — v2 ( e36f84...6821a7 )
by Berend
02:44
created

AutoApi::toArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
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 $createHooks;
18
19
	/** @var array A map of column name to functions that hook the read function */
20
	protected $readHooks;
21
22
	/** @var array A map of column name to functions that hook the update function */
23
	protected $updateHooks;
24
25
	/** @var array A map of column name to functions that hook the update function */
26
	protected $deleteHooks;	
27
28
	/** @var array A map of column name to functions that hook the search function */
29
	protected $searchHooks;
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
	 * @return Array an associative array containing the query parameters, and a data field containing an array of search results (associative arrays indexed by the keys in $fieldWhitelist)
42
	 */
43 1
	public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null, int $maxResultLimit = 100): Array
44
	{
45 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

45
		/** @scrutinizer ignore-call */ 
46
  $query = $this->search();
Loading history...
46
47
		// Build query
48 1
		$orderColumn = $queryParams['search_order_by'] ?? null;
49 1
		if (!in_array($orderColumn, $fieldWhitelist)) {
50
			$orderColumn = null;
51
		}
52
53 1
		$orderDirection = $queryParams['search_order_direction'] ?? null;
54 1
		if ($orderColumn !== null) {
55 1
			$query->orderBy($orderColumn, $orderDirection);
56
		}
57
		
58 1
		if ($whereClause !== null) {
59 1
			$query->where($whereClause);
60
		}
61
62 1
		$limit = min((int) ($queryParams['search_limit'] ?? $maxResultLimit), $maxResultLimit);
63 1
		$query->limit($limit);
64
65 1
		$offset = $queryParams['search_offset'] ?? 0;
66 1
		$query->offset($offset);
67
68 1
		$numPages = $query->getNumberOfPages();
69 1
		$currentPage = $query->getCurrentPage();
70
71
		// Fetch results
72 1
		$results = $query->fetchAll();
73 1
		$resultsArray = [];
74 1
		foreach ($results as $result) {
75 1
			$resultsArray[] = $result->toArray($fieldWhitelist);
76
		}
77
78
		return [
79 1
			'search_offset' => $offset,
80 1
			'search_limit' => $limit,
81 1
			'search_order_by' => $orderColumn,
82 1
			'search_order_direction' => $orderDirection,
83 1
			'search_pages' => $numPages,
84 1
			'search_current' => $currentPage,
85 1
			'data' => $resultsArray
86
		];
87
	}
88
89
	/**
90
	 * @param string|int $id the id of the current entity
91
	 * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
92
	 * 
93
	 * @param Array An associative array containing the data for this record, 
0 ignored issues
show
Bug introduced by
The type miBadger\ActiveRecord\Traits\An was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
94
	 * 				where the keys are entries in $fieldWhitelist
95
	 */
96 1
	public function apiRead($id, Array $fieldWhitelist): Array
97
	{
98
		// @TODO: Should apiRead throw exception or return null on fail?
99 1
		$this->read($id);
100 1
		return $this->toArray($fieldWhitelist);
101
	}
102
103
	/* =============================================================
104
	 * ===================== Constraint validation =================
105
	 * ============================================================= */
106
107
	/**
108
	 * Copy all table variables between two instances
109
	 */
110 7
	public function syncInstanceFrom($from)
111
	{
112 7
		foreach ($this->tableDefinition as $colName => $definition) {
113 7
			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
114
		}
115 7
	}
116
117 11
	private function filterInputColumns($input, $whitelist)
118
	{
119 11
		$filteredInput = $input;
120 11
		foreach ($input as $colName => $value) {
121 11
			if (!in_array($colName, $whitelist)) {
122 11
				unset($filteredInput[$colName]);
123
			}
124
		}
125 11
		return $filteredInput;
126
	}
127
128 11
	private function validateExcessKeys($input)
129
	{
130 11
		$errors = [];
131 11
		foreach ($input as $colName => $value) {
132 11
			if (!array_key_exists($colName, $this->tableDefinition)) {
133 1
				$errors[$colName] = "Unknown input field";
134 11
				continue;
135
			}
136
		}
137 11
		return $errors;
138
	}
139
140 5
	private function validateImmutableColumns($input)
141
	{
142 5
		$errors = [];
143 5
		foreach ($this->tableDefinition as $colName => $definition) {
144 5
			$property = $definition['properties'] ?? null;
145 5
			if (array_key_exists($colName, $input)
146 5
				&& $property & ColumnProperty::IMMUTABLE) {
147 5
				$errors[$colName] = "Field cannot be changed";
148
			}
149
		}
150 5
		return $errors;
151
	}
152
153
	/**
154
	 * Checks whether input values are correct:
155
	 * 1. Checks whether a value passes the validation function for that column
156
	 * 2. Checks whether a value supplied to a relationship column is a valid value
157
	 */
158 11
	private function validateInputValues($input)
159
	{
160 11
		$errors = [];
161 11
		foreach ($this->tableDefinition as $colName => $definition) {
162
			// Validation check 1: If validate function is present
163 11
			if (array_key_exists($colName, $input) 
164 11
				&& is_callable($definition['validate'] ?? null)) {
165 7
				$inputValue = $input[$colName];
166
167
				// If validation function fails
168 7
				[$status, $message] = $definition['validate']($inputValue);
169 7
				if (!$status) {
170 2
					$errors[$colName] = $message;
171
				}	
172
			}
173
174
			// Validation check 2: If relation column, check whether entity exists
175 11
			$properties = $definition['properties'] ?? null;
176 11
			if (isset($definition['relation'])
177 11
				&& ($properties & ColumnProperty::NOT_NULL)) {
178 1
				$instance = clone $definition['relation'];
179
				try {
180 1
					$instance->read($input[$colName] ?? $definition['value'] ?? null);
181 1
				} catch (ActiveRecordException $e) {
182 11
					$errors[$colName] = "Entity for this value doesn't exist";
183
				}
184
			}
185
		}
186 11
		return $errors;
187
	}
188
189
	/**
190
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
191
	 * Determines whether there are required columns for which no data is provided
192
	 */
193 11
	private function validateMissingKeys($input)
194
	{
195 11
		$errors = [];
196
197 11
		foreach ($this->tableDefinition as $colName => $colDefinition) {
198 11
			$default = $colDefinition['default'] ?? null;
199 11
			$properties = $colDefinition['properties'] ?? null;
200 11
			$value = $colDefinition['value'];
201
202
			// If nullable and default not set => null
203
			// If nullable and default null => default (null)
204
			// If nullable and default set => default (value)
205
206
			// if not nullable and default not set => error
207
			// if not nullable and default null => error
208
			// if not nullable and default st => default (value)
209
			// => if not nullable and default null and value not set (or null) => error message in this method
210 11
			if ($properties & ColumnProperty::NOT_NULL
211 11
				&& $default === null
212 11
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
213 11
				&& (!array_key_exists($colName, $input) 
214 8
					|| $input[$colName] === null 
215 11
					|| (is_string($input[$colName]) && $input[$colName] === '') )
216 6
				&& ($value === null
217 11
					|| (is_string($value) && $value === ''))) {
218 11
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
219
			} 
220
		}
221
222 11
		return $errors;
223
	}
224
225
	/**
226
	 * Copies the values for entries in the input with matching variable names in the record definition
227
	 * @param Array $input The input data to be loaded into $this record
228
	 */
229 11
	private function loadData($input)
230
	{
231 11
		foreach ($this->tableDefinition as $colName => $definition) {
232 11
			if (array_key_exists($colName, $input)) {
233 11
				$definition['value'] = $input[$colName];
234
			}
235
		}
236 11
	}
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 7
	public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
245
	{
246
		// Clone $this to new instance (for restoring if validation goes wrong)
247 7
		$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

247
		/** @scrutinizer ignore-call */ 
248
  $transaction = $this->newInstance();
Loading history...
248 7
		$errors = [];
249
250
		// Filter out all non-whitelisted input values
251 7
		$input = $this->filterInputColumns($input, $createWhitelist);
252
253
		// Validate excess keys
254 7
		$errors += $transaction->validateExcessKeys($input);
255
256
		// Validate input values (using validation function)
257 7
		$errors += $transaction->validateInputValues($input);
258
259
		// "Copy" data into transaction
260 7
		$transaction->loadData($input);
261
262
		// Run create hooks
263 7
		foreach ($transaction->createHooks as $colName => $fn) {
264
			$fn();
265
		}
266
267
		// Validate missing keys
268 7
		$errors += $transaction->validateMissingKeys($input);
269
270
		// If no errors, commit the pending data
271 7
		if (empty($errors)) {
272 3
			$this->syncInstanceFrom($transaction);
273
274
			// Insert default values for not-null fields
275 3
			$this->insertDefaults();
0 ignored issues
show
Bug introduced by
It seems like insertDefaults() 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

275
			$this->/** @scrutinizer ignore-call */ 
276
          insertDefaults();
Loading history...
276
277
			try {
278 3
				(new Query($this->getPdo(), $this->getTableName()))
279 3
					->insert($this->getActiveRecordColumns())
280 3
					->execute();
281
282 3
				$this->setId(intval($this->getPdo()->lastInsertId()));
283
			} catch (\PDOException $e) {
284
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
285
				throw new ActiveRecordException($e->getMessage(), 0, $e);
286
			}
287
288 3
			return [null, $this->toArray($readWhitelist)];
289
		} else {
290 5
			return [$errors, null];
291
		}
292
	}
293
294
	/**
295
	 * @param Array $input Associative array of input values
296
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
297
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
298
	 * 					of the modified data.
299
	 */
300 5
	public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
301
	{
302 5
		$transaction = $this->newInstance();
303 5
		$transaction->syncInstanceFrom($this);
304 5
		$errors = [];
305
306
		// Filter out all non-whitelisted input values
307 5
		$input = $this->filterInputColumns($input, $updateWhitelist);
308
309
		// Check for excess keys
310 5
		$errors += $transaction->validateExcessKeys($input);
311
312
		// Check for immutable keys
313 5
		$errors += $transaction->validateImmutableColumns($input);
314
315
		// Validate input values (using validation function)
316 5
		$errors += $transaction->validateInputValues($input);
317
318
		// "Copy" data into transaction
319 5
		$transaction->loadData($input);
320
321
		// Run create hooks
322 5
		foreach ($transaction->updateHooks as $colName => $fn) {
323
			$fn();
324
		}
325
326
		// Validate missing keys
327 5
		$errors += $transaction->validateMissingKeys($input);
328
329
		// Update database
330 5
		if (empty($errors)) {
331 2
			$this->syncInstanceFrom($transaction);
332
333
			try {
334 2
				(new Query($this->getPdo(), $this->getTableName()))
335 2
					->update($this->getActiveRecordColumns())
336 2
					->where(Query::Equal('id', $this->getId()))
337 2
					->execute();
338
			} catch (\PDOException $e) {
339
				throw new ActiveRecordException($e->getMessage(), 0, $e);
340
			}
341
342 2
			return [null, $this->toArray($readWhitelist)];
343
		} else {
344 4
			return [$errors, null];
345
		}
346
	}
347
348
	/**
349
	 * Returns this active record after reading the attributes from the entry with the given identifier.
350
	 *
351
	 * @param mixed $id
352
	 * @return $this
353
	 * @throws ActiveRecordException on failure.
354
	 */
355
	abstract public function read($id);
356
357
	/**
358
	 * Returns the PDO.
359
	 *
360
	 * @return \PDO the PDO.
361
	 */
362
	abstract public function getPdo();
363
364
	/**
365
	 * Set the ID.
366
	 *
367
	 * @param int $id
368
	 * @return $this
369
	 */
370
	abstract protected function setId($id);
371
372
	/**
373
	 * Returns the ID.
374
	 *
375
	 * @return null|int The ID.
376
	 */
377
	abstract protected function getId();
378
379
380
	/**
381
	 * Returns the serialized form of the specified columns
382
	 * 
383
	 * @return Array
384
	 */
385
	abstract public function toArray(Array $fieldWhitelist);
386
387
	/**
388
	 * Returns the active record table.
389
	 *
390
	 * @return string the active record table name.
391
	 */
392
	abstract public function getTableName();
393
394
	/**
395
	 * Returns the name -> variable mapping for the table definition.
396
	 * @return Array The mapping
397
	 */
398
	abstract protected function getActiveRecordColumns();
399
}
400