Passed
Push — v2 ( ee04d9...67b65f )
by Berend
02:48
created

AutoApi::apiRead()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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
	public function apiSearch($inputs, $fieldWhitelist)
0 ignored issues
show
Unused Code introduced by
The parameter $inputs is not used and could be removed.

This check looks from 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 $fieldWhitelist is not used and could be removed.

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

Loading history...
16
	{
17
		// @TODO: How to handle this case?
18
		// => Default parameter names for searching? (limit, pagination, sort order etc)
19
		//		Find default names for this and store in class
20
		// => Limited search parameters? (We don't want to be able to search on a password field for example)
21
	}
22
23 4
	public function toArray($fieldWhitelist)
24
	{
25 4
		$output = [];
26 4
		foreach ($this->tableDefinition as $colName => $definition) {
0 ignored issues
show
Bug introduced by
The property tableDefinition does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
27 4
			if (in_array($colName, $fieldWhitelist)) {
28 4
				$output[$colName] = $definition['value'];
29
			}
30
		}
31
32 4
		return $output;
33
	}
34
35 1
	public function apiRead($id, $fieldWhitelist)
36
	{
37 1
		$this->read($id);
0 ignored issues
show
Bug introduced by
It seems like read() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
38 1
		return $this->toArray($fieldWhitelist);
39
	}
40
41
	/* =============================================================
42
	 * ===================== Constraint validation =================
43
	 * ============================================================= */
44
45
	/**
46
	 * Copy all table variables between two instances
47
	 */
48 3
	private function syncInstances($to, $from)
49
	{
50 3
		foreach ($to->tableDefinition as $colName => $definition) {
51 3
			$definition['value'] = $from->tableDefinition[$colName]['value'];
52
		}
53 3
	}
54
55 7
	private function filterInputColumns($input, $whitelist)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
56
	{
57 7
		$filteredInput = $input;
58 7
		foreach ($input as $colName => $value) {
59 7
			if (!in_array($colName, $whitelist)) {
60 7
				unset($filteredInput[$colName]);
61
			}
62
		}
63 7
		return $filteredInput;
64
	}
65
66 7
	private function validateExcessKeys($input)
67
	{
68 7
		$errors = [];
69 7
		foreach ($input as $colName => $value) {
70 7
			if (!array_key_exists($colName, $this->tableDefinition)) {
71 1
				$errors[$colName] = "Unknown input field";
72 7
				continue;
73
			}
74
		}
75 7
		return $errors;
76
	}
77
78 2
	private function validateImmutableColumns($input)
79
	{
80 2
		$errors = [];
81 2
		foreach ($this->tableDefinition as $colName => $definition) {
82 2
			$property = $definition['properties'] ?? null;
83 2
			if (array_key_exists($colName, $input)
84 2
				&& $property & ColumnProperty::IMMUTABLE) {
85 2
				$errors[$colName] = "Field cannot be changed";
86
			}
87
		}
88 2
		return $errors;
89
	}
90
91 7
	private function validateInputValues($input)
92
	{
93 7
		$errors = [];
94 7
		foreach ($this->tableDefinition as $colName => $definition) {
95
			// Validation check 1: If validate function is present
96 7
			if (array_key_exists($colName, $input) 
97 7
				&& is_callable($definition['validate'] ?? null)) {
98 5
				$inputValue = $input[$colName];
99
100
				// If validation function fails
101 5
				[$status, $message] = $definition['validate']($inputValue);
0 ignored issues
show
Bug introduced by
The variable $status does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $message does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
102 5
				if (!$status) {
103 1
					$errors[$colName] = $message;
104
				}	
105
			}
106
107
			// Validation check 2: If relation column, check whether entity exists
108 7
			$properties = $definition['properties'] ?? null;
109 7
			if (isset($definition['relation'])
110 7
				&& ($properties & ColumnProperty::NOT_NULL)) {
111
				$instance = clone $definition['relation'];
112
				try {
113
					$instance->read($input[$colName] ?? null);
114
				} catch (ActiveRecordException $e) {
115 7
					$errors[$colName] = "Entity for this value doesn't exist";
116
				}
117
			}
118
		}
119 7
		return $errors;
120
	}
121
122
	/**
123
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
124
	 */
125 7
	private function validateMissingKeys()
126
	{
127 7
		$errors = [];
128
129 7
		foreach ($this->tableDefinition as $colName => $colDefinition) {
130 7
			$default = $colDefinition['default'] ?? null;
131 7
			$properties = $colDefinition['properties'] ?? null;
132 7
			$value = $colDefinition['value'];
133
134
			// If nullable and default not set => null
135
			// If nullable and default null => default (null)
136
			// If nullable and default set => default (value)
137
138
			// if not nullable and default not set => error
139
			// if not nullable and default null => error
140
			// if not nullable and default st => default (value)
141
			// => if not nullable and default null and value not set => error message in this method
142 7
			if ($properties & ColumnProperty::NOT_NULL
143 7
				&& $default === null
144 7
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
145
				// && !array_key_exists($colName, $input)
146 7
				&& $value === null) {
147 7
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
148
			}
149
		}
150
151 7
		return $errors;
152
	}
153
154
	/**
155
	 * Copies the values for entries in the input with matching variable names in the record definition
156
	 * @param Array $input The input data to be loaded into $this record
157
	 */
158 7
	private function loadData($input)
159
	{
160 7
		foreach ($this->tableDefinition as $colName => $definition) {
161 7
			if (array_key_exists($colName, $input)) {
162 7
				$definition['value'] = $input[$colName];
163
			}
164
		}
165 7
	}
166
167
	/**
168
	 * @param Array $input Associative array of input values
169
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
170
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
171
	 * 					of the modified data.
172
	 */
173 5
	public function apiCreate($input, $fieldWhitelist)
174
	{
175
		// Clone $this to new instance (for restoring if validation goes wrong)
176 5
		$transaction = clone $this;
177 5
		$errors = [];
178
179
		// Filter out all non-whitelisted input values
180 5
		$input = $this->filterInputColumns($input, $fieldWhitelist);
181
182
		// Validate excess keys
183 5
		$errors += $transaction->validateExcessKeys($input);
184
185
		// Validate input values (using validation function)
186 5
		$errors += $transaction->validateInputValues($input);
187
188
		// "Copy" data into transaction
189 5
		$transaction->loadData($input);
190
191
		// Run create hooks
192 5
		foreach ($this->registeredCreateHooks as $colName => $fn) {
0 ignored issues
show
Bug introduced by
The property registeredCreateHooks does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
193
			$fn();
194
		}
195
196
		// Validate missing keys
197 5
		$errors += $transaction->validateMissingKeys();
198
199
		// If no errors, commit the pending data
200 5
		if (empty($errors)) {
201 2
			$this->syncInstances($this, $transaction);
0 ignored issues
show
Unused Code introduced by
The call to the method miBadger\ActiveRecord\Tr...utoApi::syncInstances() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
202
203
			try {
204 2
				$q = (new Query($this->getPdo(), $this->getActiveRecordTable()))
0 ignored issues
show
Bug introduced by
It seems like getPdo() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like getActiveRecordTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Unused Code introduced by
$q is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
205 2
					->insert($this->getActiveRecordColumns())
0 ignored issues
show
Bug introduced by
It seems like getActiveRecordColumns() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
206 2
					->execute();
207
208 2
				$this->setId(intval($this->getPdo()->lastInsertId()));
0 ignored issues
show
Bug introduced by
It seems like getPdo() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like setId() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
209
			} catch (\PDOException $e) {
210
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
211
				throw new ActiveRecordException($e->getMessage(), 0, $e);
212
			}
213
214 2
			return [null, $this->toArray($fieldWhitelist)];
215
		} else {
216 3
			return [$errors, null];
217
		}
218
	}
219
220
	/**
221
	 * @param Array $input Associative array of input values
222
	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
223
	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
224
	 * 					of the modified data.
225
	 */
226 2
	public function apiUpdate($input, $fieldWhitelist)
227
	{
228 2
		$transaction = clone $this;
229 2
		$errors = [];
230
231
		// Filter out all non-whitelisted input values
232 2
		$input = $this->filterInputColumns($input, $fieldWhitelist);
233
234
		// Check for excess keys
235 2
		$errors += $transaction->validateExcessKeys($input);
236
237
		// Check for immutable keys
238 2
		$errors += $transaction->validateImmutableColumns($input);
239
240
		// Validate input values (using validation function)
241 2
		$errors += $transaction->validateInputValues($input);
242
243
		// "Copy" data into transaction
244 2
		$transaction->loadData($input);
245
246
		// Run create hooks
247 2
		foreach ($this->registeredUpdateHooks as $colName => $fn) {
0 ignored issues
show
Bug introduced by
The property registeredUpdateHooks does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
248
			$fn();
249
		}
250
251
		// Validate missing keys
252 2
		$errors += $transaction->validateMissingKeys();
253
254
		// Update database
255 2
		if (empty($errors)) {
256 1
			$this->syncInstances($this, $transaction);
0 ignored issues
show
Unused Code introduced by
The call to the method miBadger\ActiveRecord\Tr...utoApi::syncInstances() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
257
258
			try {
259 1
				(new Query($this->getPdo(), $this->getActiveRecordTable()))
0 ignored issues
show
Bug introduced by
It seems like getPdo() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like getActiveRecordTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
260 1
					->update($this->getActiveRecordColumns())
0 ignored issues
show
Bug introduced by
It seems like getActiveRecordColumns() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
261 1
					->where('id', '=', $this->getId())
0 ignored issues
show
Bug introduced by
It seems like getId() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
262 1
					->execute();
263
			} catch (\PDOException $e) {
264
				throw new ActiveRecordException($e->getMessage(), 0, $e);
265
			}
266
267 1
			return [null, $this->toArray($fieldWhitelist)];
268
		} else {
269 1
			return [$errors, null];
270
		}
271
	}
272
}
273