Test Failed
Push — v2 ( e257c3...a1ad0c )
by Berend
03:35
created

AutoApi::syncInstances()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
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
	// @TODO: Or can it perhaps even be a wrapper object that exposes this functionality?
15
	//			Usage Code something like (new AutoApi($object))->apiRead($_POST);
16
	//		Advantage: AbstractActiveRecord stays smaller and is less godlike
17
	// 		Question: How to update values (Is only the (complete) TableDefinition enough?)
18
19
	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...
20
	{
21
		// @TODO: How to handle this case?
22
		// => Default parameter names for searching? (limit, pagination, sort order etc)
23
		//		Find default names for this and store in class
24
		// => Limited search parameters? (We don't want to be able to search on a password field for example)
25
	}
26
27
	public function toArray($fieldWhitelist)
28
	{
29
		$output = [];
30
		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...
31
			if (in_array($colName, $fieldWhitelist)) {
32
				$output[$colName] = $definition['value'];
33
			}
34
		}
35
36
		return $output;
37
	}
38
39
	public function apiRead($id, $fieldWhitelist)
40
	{
41
		$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...
42
		return $this->toArray($fieldWhitelist);
43
	}
44
45
	/* =============================================================
46
	 * ===================== Constraint validation =================
47
	 * ============================================================= */
48
	// @TODO: Check whether length is ok
49
50
	/**
51
	 * Copy all table variables between two instances
52
	 */
53
	private function syncInstances($to, $from)
54
	{
55
		foreach ($to->tableDefinition as $colName => $definition) {
56
			$definition['value'] = $from->tableDefinition[$colName]['value'];
57
		}
58
	}
59
60
	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...
61
	{
62
		$filteredInput = $input;
63
		foreach ($input as $colName => $value) {
64
			if (!in_array($colName, $whitelist)) {
65
				unset($filteredInput[$colName]);
66
			}
67
		}
68
		return $filteredInput;
69
	}
70
71
	private function validateExcessKeys($input)
72
	{
73
		$errors = [];
74
		foreach ($input as $colName => $value) {
75
			if (!array_key_exists($colName, $this->tableDefinition)) {
76
				$errors[$colName] = "Unknown input field";
77
				continue;
78
			}
79
		}
80
		return $errors;
81
	}
82
83
	private function validateImmutableColumns($input)
84
	{
85
		$errors = [];
86
		foreach ($this->tableDefinition as $colName => $definition) {
87
			$property = $definition['properties'] ?? null;
88
			if (array_key_exists($colName, $input)
89
				&& $property & ColumnProperty::IMMUTABLE) {
90
				$errors[$colName] = "Field cannot be changed";
91
			}
92
		}
93
		return $errors;
94
	}
95
96
	private function validateInputValues($input)
97
	{
98
		$errors = [];
99
		foreach ($this->tableDefinition as $colName => $definition) {
100
			// Validation check 1: If validate function is present
101
			if (array_key_exists($colName, $input) 
102
				&& is_callable($definition['validate'] ?? null)) {
103
				$inputValue = $input[$colName];
104
105
				// If validation function fails
106
				[$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...
107
				if (!$status) {
108
					$errors[$colName] = $message;
109
				}	
110
			}
111
112
			// Validation check 2: If relation column, check whether entity exists
113
			$properties = $definition['properties'] ?? null;
114
			if (isset($definition['relation'])
115
				&& ($properties & ColumnProperty::NOT_NULL)) {
116
				$instance = clone $definition['relation'];
117
				try {
118
					$instance->read($input[$colName] ?? null);
119
				} catch (ActiveRecordException $e) {
120
					$errors[$colName] = "Entity for this value doesn't exist";
121
				}
122
			}
123
		}
124
		return $errors;
125
	}
126
127
	/**
128
	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
129
	 */
130
	private function validateMissingKeys()
131
	{
132
		$errors = [];
133
134
		foreach ($this->tableDefinition as $colName => $colDefinition) {
135
			$default = $colDefinition['default'] ?? null;
136
			$properties = $colDefinition['properties'] ?? null;
137
			$value = $colDefinition['value'];
138
139
			// If nullable and default not set => null
140
			// If nullable and default null => default (null)
141
			// If nullable and default set => default (value)
142
143
			// if not nullable and default not set => error
144
			// if not nullable and default null => error
145
			// if not nullable and default st => default (value)
146
			// => if not nullable and default null and value not set => error message in this method
147
			if ($properties & ColumnProperty::NOT_NULL
148
				&& $default === null
149
				&& !($properties & ColumnProperty::AUTO_INCREMENT)
150
				// && !array_key_exists($colName, $input)
151
				&& $value === null) {
152
				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
153
			}
154
		}
155
156
		return $errors;
157
	}
158
159
	/**
160
	 * Copies the values for entries in the input with matching variable names in the record definition
161
	 */
162
	private function loadData($input)
163
	{
164
		foreach ($this->tableDefinition as $colName => $definition) {
165
			if (array_key_exists($colName, $input)) {
166
				$definition['value'] = $input[$colName];
167
			}
168
		}
169
	}
170
171
	public function apiCreate($input, $fieldWhitelist)
172
	{
173
		// Clone $this to new instance (for restoring if validation goes wrong)
174
		$transaction = clone $this;
175
		$errors = [];
176
177
		// Filter out all non-whitelisted input values
178
		$input = $this->filterInputColumns($input, $fieldWhitelist);
179
180
		// Validate excess keys
181
		$errors += $transaction->validateExcessKeys($input);
182
183
		// Validate input values (using validation function)
184
		$errors += $transaction->validateInputValues($input);
185
186
		// "Copy" data into transaction
187
		$transaction->loadData($input);
188
189
		// Run create hooks
190
		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...
191
			$fn();
192
		}
193
194
		// Validate missing keys
195
		$errors += $transaction->validateMissingKeys();
196
197
		// If no errors, commit the pending data
198
		if (empty($errors)) {
199
			$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...
200
201
			try {
202
				$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...
203
					->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...
204
					->execute();
205
206
				$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...
207
			} catch (\PDOException $e) {
208
				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
209
				throw new ActiveRecordException($e->getMessage(), 0, $e);
210
			}
211
212
			return [null, $this->toArray($fieldWhitelist)];
213
		} else {
214
			return [$errors, null];
215
		}
216
	}
217
218
	public function apiUpdate($input, $fieldWhitelist)
219
	{
220
		$transaction = clone $this;
221
		$errors = [];
222
223
		// Filter out all non-whitelisted input values
224
		$input = $this->filterInputColumns($input, $fieldWhitelist);
225
226
		// Check for excess keys
227
		$errors += $transaction->validateExcessKeys($input);
228
229
		// Check for immutable keys
230
		$errors += $transaction->validateImmutableColumns($input);
231
232
		// Validate input values (using validation function)
233
		$errors += $transaction->validateInputValues($input);
234
235
		// "Copy" data into transaction
236
		$transaction->loadData($input);
237
238
		// Run create hooks
239
		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...
240
			$fn();
241
		}
242
243
		// Validate missing keys
244
		$errors += $transaction->validateMissingKeys();
245
246
		// Update database
247
		if (empty($errors)) {
248
			$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...
249
250
			try {
251
				(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...
252
					->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...
253
					->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...
254
					->execute();
255
			} catch (\PDOException $e) {
256
				throw new ActiveRecordException($e->getMessage(), 0, $e);
257
			}
258
259
			return [null, $this->toArray($fieldWhitelist)];
260
		} else {
261
			return [$errors, null];
262
		}
263
	}
264
}
265