Completed
Push — v2 ( ff18b4...317d67 )
by Berend
04:34
created
src/ActiveRecordInterface.php 1 patch
Indentation   +66 added lines, -66 removed lines patch added patch discarded remove patch
@@ -19,79 +19,79 @@
 block discarded – undo
19 19
 interface ActiveRecordInterface
20 20
 {
21 21
 
22
-	public function __construct(\PDO $pdo);
22
+    public function __construct(\PDO $pdo);
23 23
 	
24
-	/**
25
-	 * Returns the ID of the record.
26
-	 *
27
-	 * @return null|int The ID.
28
-	 */	
29
-	public function getId();
24
+    /**
25
+     * Returns the ID of the record.
26
+     *
27
+     * @return null|int The ID.
28
+     */	
29
+    public function getId();
30 30
 
31
-	/**
32
-	 * Returns this active record after creating an entry with the records attributes.
33
-	 *
34
-	 * @return $this
35
-	 * @throws ActiveRecordException on failure.
36
-	 */
37
-	public function create();
31
+    /**
32
+     * Returns this active record after creating an entry with the records attributes.
33
+     *
34
+     * @return $this
35
+     * @throws ActiveRecordException on failure.
36
+     */
37
+    public function create();
38 38
 
39
-	/**
40
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
41
-	 *
42
-	 * @param mixed $id
43
-	 * @return $this
44
-	 * @throws ActiveRecordException on failure.
45
-	 */
46
-	public function read($id);
39
+    /**
40
+     * Returns this active record after reading the attributes from the entry with the given identifier.
41
+     *
42
+     * @param mixed $id
43
+     * @return $this
44
+     * @throws ActiveRecordException on failure.
45
+     */
46
+    public function read($id);
47 47
 
48
-	/**
49
-	 * Returns this active record after updating the attributes to the corresponding entry.
50
-	 *
51
-	 * @return $this
52
-	 * @throws ActiveRecordException on failure.
53
-	 */
54
-	public function update();
48
+    /**
49
+     * Returns this active record after updating the attributes to the corresponding entry.
50
+     *
51
+     * @return $this
52
+     * @throws ActiveRecordException on failure.
53
+     */
54
+    public function update();
55 55
 
56
-	/**
57
-	 * Returns this record after deleting the corresponding entry.
58
-	 *
59
-	 * @return $this
60
-	 * @throws ActiveRecordException on failure.
61
-	 */
62
-	public function delete();
56
+    /**
57
+     * Returns this record after deleting the corresponding entry.
58
+     *
59
+     * @return $this
60
+     * @throws ActiveRecordException on failure.
61
+     */
62
+    public function delete();
63 63
 
64
-	/**
65
-	 * Returns this record after synchronizing it with the corresponding entry.
66
-	 * A new entry is created if this active record does not have a corresponding entry.
67
-	 *
68
-	 * @return $this
69
-	 * @throws ActiveRecordException on failure.
70
-	 */
71
-	public function sync();
64
+    /**
65
+     * Returns this record after synchronizing it with the corresponding entry.
66
+     * A new entry is created if this active record does not have a corresponding entry.
67
+     *
68
+     * @return $this
69
+     * @throws ActiveRecordException on failure.
70
+     */
71
+    public function sync();
72 72
 
73
-	/**
74
-	 * Returns true if this active record has a corresponding entry.
75
-	 *
76
-	 * @return bool true if this active record has a corresponding entry.
77
-	 */
78
-	public function exists();
73
+    /**
74
+     * Returns true if this active record has a corresponding entry.
75
+     *
76
+     * @return bool true if this active record has a corresponding entry.
77
+     */
78
+    public function exists();
79 79
 
80
-	/**
81
-	 * Returns this record after filling it with the given attributes.
82
-	 *
83
-	 * @param array $attributes = []
84
-	 * @return $this
85
-	 * @throws ActiveRecordException on failure.
86
-	 */
87
-	public function fill(array $attributes);
80
+    /**
81
+     * Returns this record after filling it with the given attributes.
82
+     *
83
+     * @param array $attributes = []
84
+     * @return $this
85
+     * @throws ActiveRecordException on failure.
86
+     */
87
+    public function fill(array $attributes);
88 88
 
89
-	/**
90
-	 * Returns the records with the given where, order by, limit and offset clauses.
91
-	 *
92
-	 * @param array $excludedTraits
93
-	 * @return ActiveRecordQuery the query representing the current search.
94
-	 * @throws ActiveRecordException on failure.
95
-	 */
96
-	public function search(Array $excludedTraits);
89
+    /**
90
+     * Returns the records with the given where, order by, limit and offset clauses.
91
+     *
92
+     * @param array $excludedTraits
93
+     * @return ActiveRecordQuery the query representing the current search.
94
+     * @throws ActiveRecordException on failure.
95
+     */
96
+    public function search(Array $excludedTraits);
97 97
 }
Please login to merge, or discard this patch.
src/AbstractActiveRecord.php 2 patches
Indentation   +620 added lines, -620 removed lines patch added patch discarded remove patch
@@ -18,626 +18,626 @@
 block discarded – undo
18 18
  */
19 19
 abstract class AbstractActiveRecord implements ActiveRecordInterface
20 20
 {
21
-	const COLUMN_NAME_ID = 'id';
22
-	const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
-
24
-	const CREATE = 'CREATE';
25
-	const READ = 'READ';
26
-	const UPDATE = 'UPDATE';
27
-	const DELETE = 'DELETE';
28
-	const SEARCH = 'SEARCH';
29
-
30
-	/** @var \PDO The PDO object. */
31
-	protected $pdo;
32
-
33
-	/** @var null|int The ID. */
34
-	private $id;
35
-
36
-	/** @var array A map of column name to functions that hook the insert function */
37
-	protected $createHooks;
38
-
39
-	/** @var array A map of column name to functions that hook the read function */
40
-	protected $readHooks;
41
-
42
-	/** @var array A map of column name to functions that hook the update function */
43
-	protected $updateHooks;
44
-
45
-	/** @var array A map of column name to functions that hook the update function */
46
-	protected $deleteHooks;	
47
-
48
-	/** @var array A map of column name to functions that hook the search function */
49
-	protected $searchHooks;
50
-
51
-	/** @var array A list of table column definitions */
52
-	protected $tableDefinition;
53
-
54
-	/**
55
-	 * Construct an abstract active record with the given PDO.
56
-	 *
57
-	 * @param \PDO $pdo
58
-	 */
59
-	public function __construct(\PDO $pdo)
60
-	{
61
-		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
62
-		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
63
-
64
-		$this->setPdo($pdo);
65
-
66
-		$this->createHooks = [];
67
-		$this->readHooks = [];
68
-		$this->updateHooks = [];
69
-		$this->deleteHooks = [];
70
-		$this->searchHooks = [];
71
-		$this->tableDefinition = $this->getTableDefinition();
72
-
73
-		// Extend table definition with default ID field, throw exception if field already exists
74
-		if (array_key_exists('id', $this->tableDefinition)) {
75
-			$message = "Table definition in record contains a field with name \"id\"";
76
-			$message .= ", which is a reserved name by ActiveRecord";
77
-			throw new ActiveRecordException($message, 0);
78
-		}
79
-
80
-		$this->tableDefinition[self::COLUMN_NAME_ID] =
81
-		[
82
-			'value' => &$this->id,
83
-			'validate' => null,
84
-			'type' => self::COLUMN_TYPE_ID,
85
-			'properties' =>
86
-				ColumnProperty::NOT_NULL
87
-				| ColumnProperty::IMMUTABLE
88
-				| ColumnProperty::AUTO_INCREMENT
89
-				| ColumnProperty::PRIMARY_KEY
90
-		];
91
-	}
92
-
93
-	private function checkHookConstraints($columnName, $hookMap)
94
-	{
95
-		// Check whether column exists
96
-		if (!array_key_exists($columnName, $this->tableDefinition)) 
97
-		{
98
-			throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
99
-		}
100
-
101
-		// Enforcing 1 hook per table column
102
-		if (array_key_exists($columnName, $hookMap)) {
103
-			$message = "Hook is trying to register on an already registered column \"$columnName\", ";
104
-			$message .= "do you have conflicting traits?";
105
-			throw new ActiveRecordException($message, 0);
106
-		}
107
-	}
108
-
109
-	public function registerHookOnAction($actionName, $columnName, $fn)
110
-	{
111
-		if (is_string($fn) && is_callable([$this, $fn])) {
112
-			$fn = [$this, $fn];
113
-		}
114
-
115
-		if (!is_callable($fn)) { 
116
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
117
-		}
118
-
119
-		switch ($actionName) {
120
-			case self::CREATE:
121
-				$this->checkHookConstraints($columnName, $this->createHooks);
122
-				$this->createHooks[$columnName] = $fn;
123
-				break;
124
-			case self::READ:
125
-				$this->checkHookConstraints($columnName, $this->readHooks);
126
-				$this->readHooks[$columnName] = $fn;
127
-				break;
128
-			case self::UPDATE:
129
-				$this->checkHookConstraints($columnName, $this->updateHooks);
130
-				$this->updateHooks[$columnName] = $fn;
131
-				break;
132
-			case self::DELETE:
133
-				$this->checkHookConstraints($columnName, $this->deleteHooks);
134
-				$this->deleteHooks[$columnName] = $fn;
135
-				break;
136
-			case self::SEARCH:
137
-				$this->checkHookConstraints($columnName, $this->searchHooks);
138
-				$this->searchHooks[$columnName] = $fn;
139
-				break;
140
-			default:
141
-				throw new ActiveRecordException("Invalid action: Can not register hook on non-existing action");
142
-		}
143
-	}
144
-
145
-	/**
146
-	 * Register a new hook for a specific column that gets called before execution of the create() method
147
-	 * Only one hook per column can be registered at a time
148
-	 * @param string $columnName The name of the column that is registered.
149
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
150
-	 */
151
-	public function registerCreateHook($columnName, $fn)
152
-	{
153
-		$this->registerHookOnAction(self::CREATE, $columnName, $fn);
154
-	}
155
-
156
-	/**
157
-	 * Register a new hook for a specific column that gets called before execution of the read() method
158
-	 * Only one hook per column can be registered at a time
159
-	 * @param string $columnName The name of the column that is registered.
160
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
-	 */
162
-	public function registerReadHook($columnName, $fn)
163
-	{
164
-		$this->registerHookOnAction(self::READ, $columnName, $fn);
165
-	}
166
-
167
-	/**
168
-	 * Register a new hook for a specific column that gets called before execution of the update() method
169
-	 * Only one hook per column can be registered at a time
170
-	 * @param string $columnName The name of the column that is registered.
171
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
172
-	 */
173
-	public function registerUpdateHook($columnName, $fn)
174
-	{
175
-		$this->registerHookOnAction(self::UPDATE, $columnName, $fn);
176
-	}
177
-
178
-	/**
179
-	 * Register a new hook for a specific column that gets called before execution of the delete() method
180
-	 * Only one hook per column can be registered at a time
181
-	 * @param string $columnName The name of the column that is registered.
182
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
183
-	 */
184
-	public function registerDeleteHook($columnName, $fn)
185
-	{
186
-		$this->registerHookOnAction(self::DELETE, $columnName, $fn);
187
-	}
188
-
189
-	/**
190
-	 * Register a new hook for a specific column that gets called before execution of the search() method
191
-	 * Only one hook per column can be registered at a time
192
-	 * @param string $columnName The name of the column that is registered.
193
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object. The callable is required to take one argument: an instance of miBadger\Query\Query; 
194
-	 */
195
-	public function registerSearchHook($columnName, $fn)
196
-	{
197
-		$this->registerHookOnAction(self::SEARCH, $columnName, $fn);
198
-	}
199
-
200
-	/**
201
-	 * Adds a new column definition to the table.
202
-	 * @param string $columnName The name of the column that is registered.
203
-	 * @param Array $definition The definition of that column.
204
-	 */
205
-	protected function extendTableDefinition($columnName, $definition)
206
-	{
207
-		if ($this->tableDefinition === null) {
208
-			throw new ActiveRecordException("tableDefinition is null, has parent been initialized in constructor?");
209
-		}
210
-
211
-		// Enforcing table can only be extended with new columns
212
-		if (array_key_exists($columnName, $this->tableDefinition)) {
213
-			$message = "Table is being extended with a column that already exists, ";
214
-			$message .= "\"$columnName\" conflicts with your table definition";
215
-			throw new ActiveRecordException($message, 0);
216
-		}
217
-
218
-		$this->tableDefinition[$columnName] = $definition;
219
-	}
220
-
221
-	public function hasColumn(string $column) {
222
-		return array_key_exists($column, $this->tableDefinition);
223
-	}
224
-
225
-	public function hasRelation(string $column, ActiveRecordInterface $record) {
226
-		if (!$this->hasColumn($column)) {
227
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
228
-		}
229
-
230
-		if (!isset($this->tableDefinition[$column]['relation'])) {
231
-			return false;
232
-		}
233
-
234
-		$relation = $this->tableDefinition[$column]['relation'];
235
-		if ($relation instanceof AbstractActiveRecord) {
236
-			// Injected object
237
-			return get_class($record) === get_class($relation);
238
-		} else {
239
-			// :: class definition
240
-			return get_class($record) === $relation;
241
-		}
242
-	}
243
-
244
-	public function hasProperty(string $column, $property) {
245
-		if (!$this->hasColumn($column)) {
246
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
247
-		}
248
-
249
-		try {
250
-			$enumValue = ColumnProperty::valueOf($property);
251
-		} catch (\UnexpectedValueException $e) {
252
-			throw new ActiveRecordException("Provided property \"$property\" is not a valid property", 0, $e);
253
-		}
254
-
255
-		$properties = $this->tableDefinition[$column]['properties'] ?? null;
256
-
257
-		return $properties !== null && (($properties & $enumValue->getValue()) > 0);
258
-	}
259
-
260
-	public function getColumnType(string $column) {
261
-		if (!$this->hasColumn($column)) {
262
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
263
-		}
264
-
265
-		return $this->tableDefinition[$column]['type'] ?? null;
266
-	}
267
-
268
-	public function getColumnLength(string $column) {
269
-		if (!$this->hasColumn($column)) {
270
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
271
-		}
272
-
273
-		return $this->tableDefinition[$column]['length'] ?? null;
274
-	}
275
-
276
-	public function getDefault(string $column) {
277
-		if (!$this->hasColumn($column)) {
278
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
279
-		}
280
-
281
-		return $this->tableDefinition[$column]['default'] ?? null;
282
-	}
283
-
284
-	public function validateColumn(string $column, $input) {
285
-		if (!$this->hasColumn($column)) {
286
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
287
-		}
288
-
289
-		$fn = $this->tableDefinition[$column]['validate'] ?? null;
290
-
291
-		if ($fn === null) {
292
-			return [true, ''];
293
-		}
294
-
295
-		if (!is_callable($fn)) {
296
-			throw new ActiveRecordException("Provided validation function is not callable", 0);
297
-		}
298
-
299
-		return $fn($input);
300
-	}
301
-
302
-	/**
303
-	 * Useful for writing unit tests of models against ActiveRecord: 
304
-	 * overrides a relation column with a relation onto a mock object.
305
-	 * @param string $column the name of the column onto which to place the mock relation
306
-	 * @param object $mock the instance of a mock object to palce onto the model.
307
-	 */
308
-	public function injectInstanceOnRelation(string $column, $mock) {
309
-		if (!$this->hasColumn($column)) {
310
-			throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
311
-		}
312
-
313
-		$this->tableDefinition[$column]['relation'] = $mock;
314
-	}
315
-
316
-	/**
317
-	 * Creates the entity as a table in the database
318
-	 */
319
-	public function createTable()
320
-	{
321
-		$this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
322
-	}
323
-
324
-	/**
325
-	 * Iterates over the specified constraints in the table definition, 
326
-	 * 		and applies these to the database.
327
-	 */
328
-	public function createTableConstraints()
329
-	{
330
-		// Iterate over columns, check whether "relation" field exists, if so create constraint
331
-		foreach ($this->tableDefinition as $colName => $definition) {
332
-			if (!isset($definition['relation'])) {
333
-				continue;
334
-			}
335
-
336
-			$relation = $definition['relation'];
337
-			$properties = $definition['properties'] ?? 0;
21
+    const COLUMN_NAME_ID = 'id';
22
+    const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
+
24
+    const CREATE = 'CREATE';
25
+    const READ = 'READ';
26
+    const UPDATE = 'UPDATE';
27
+    const DELETE = 'DELETE';
28
+    const SEARCH = 'SEARCH';
29
+
30
+    /** @var \PDO The PDO object. */
31
+    protected $pdo;
32
+
33
+    /** @var null|int The ID. */
34
+    private $id;
35
+
36
+    /** @var array A map of column name to functions that hook the insert function */
37
+    protected $createHooks;
38
+
39
+    /** @var array A map of column name to functions that hook the read function */
40
+    protected $readHooks;
41
+
42
+    /** @var array A map of column name to functions that hook the update function */
43
+    protected $updateHooks;
44
+
45
+    /** @var array A map of column name to functions that hook the update function */
46
+    protected $deleteHooks;	
47
+
48
+    /** @var array A map of column name to functions that hook the search function */
49
+    protected $searchHooks;
50
+
51
+    /** @var array A list of table column definitions */
52
+    protected $tableDefinition;
53
+
54
+    /**
55
+     * Construct an abstract active record with the given PDO.
56
+     *
57
+     * @param \PDO $pdo
58
+     */
59
+    public function __construct(\PDO $pdo)
60
+    {
61
+        $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
62
+        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
63
+
64
+        $this->setPdo($pdo);
65
+
66
+        $this->createHooks = [];
67
+        $this->readHooks = [];
68
+        $this->updateHooks = [];
69
+        $this->deleteHooks = [];
70
+        $this->searchHooks = [];
71
+        $this->tableDefinition = $this->getTableDefinition();
72
+
73
+        // Extend table definition with default ID field, throw exception if field already exists
74
+        if (array_key_exists('id', $this->tableDefinition)) {
75
+            $message = "Table definition in record contains a field with name \"id\"";
76
+            $message .= ", which is a reserved name by ActiveRecord";
77
+            throw new ActiveRecordException($message, 0);
78
+        }
79
+
80
+        $this->tableDefinition[self::COLUMN_NAME_ID] =
81
+        [
82
+            'value' => &$this->id,
83
+            'validate' => null,
84
+            'type' => self::COLUMN_TYPE_ID,
85
+            'properties' =>
86
+                ColumnProperty::NOT_NULL
87
+                | ColumnProperty::IMMUTABLE
88
+                | ColumnProperty::AUTO_INCREMENT
89
+                | ColumnProperty::PRIMARY_KEY
90
+        ];
91
+    }
92
+
93
+    private function checkHookConstraints($columnName, $hookMap)
94
+    {
95
+        // Check whether column exists
96
+        if (!array_key_exists($columnName, $this->tableDefinition)) 
97
+        {
98
+            throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
99
+        }
100
+
101
+        // Enforcing 1 hook per table column
102
+        if (array_key_exists($columnName, $hookMap)) {
103
+            $message = "Hook is trying to register on an already registered column \"$columnName\", ";
104
+            $message .= "do you have conflicting traits?";
105
+            throw new ActiveRecordException($message, 0);
106
+        }
107
+    }
108
+
109
+    public function registerHookOnAction($actionName, $columnName, $fn)
110
+    {
111
+        if (is_string($fn) && is_callable([$this, $fn])) {
112
+            $fn = [$this, $fn];
113
+        }
114
+
115
+        if (!is_callable($fn)) { 
116
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
117
+        }
118
+
119
+        switch ($actionName) {
120
+            case self::CREATE:
121
+                $this->checkHookConstraints($columnName, $this->createHooks);
122
+                $this->createHooks[$columnName] = $fn;
123
+                break;
124
+            case self::READ:
125
+                $this->checkHookConstraints($columnName, $this->readHooks);
126
+                $this->readHooks[$columnName] = $fn;
127
+                break;
128
+            case self::UPDATE:
129
+                $this->checkHookConstraints($columnName, $this->updateHooks);
130
+                $this->updateHooks[$columnName] = $fn;
131
+                break;
132
+            case self::DELETE:
133
+                $this->checkHookConstraints($columnName, $this->deleteHooks);
134
+                $this->deleteHooks[$columnName] = $fn;
135
+                break;
136
+            case self::SEARCH:
137
+                $this->checkHookConstraints($columnName, $this->searchHooks);
138
+                $this->searchHooks[$columnName] = $fn;
139
+                break;
140
+            default:
141
+                throw new ActiveRecordException("Invalid action: Can not register hook on non-existing action");
142
+        }
143
+    }
144
+
145
+    /**
146
+     * Register a new hook for a specific column that gets called before execution of the create() method
147
+     * Only one hook per column can be registered at a time
148
+     * @param string $columnName The name of the column that is registered.
149
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
150
+     */
151
+    public function registerCreateHook($columnName, $fn)
152
+    {
153
+        $this->registerHookOnAction(self::CREATE, $columnName, $fn);
154
+    }
155
+
156
+    /**
157
+     * Register a new hook for a specific column that gets called before execution of the read() method
158
+     * Only one hook per column can be registered at a time
159
+     * @param string $columnName The name of the column that is registered.
160
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
+     */
162
+    public function registerReadHook($columnName, $fn)
163
+    {
164
+        $this->registerHookOnAction(self::READ, $columnName, $fn);
165
+    }
166
+
167
+    /**
168
+     * Register a new hook for a specific column that gets called before execution of the update() method
169
+     * Only one hook per column can be registered at a time
170
+     * @param string $columnName The name of the column that is registered.
171
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
172
+     */
173
+    public function registerUpdateHook($columnName, $fn)
174
+    {
175
+        $this->registerHookOnAction(self::UPDATE, $columnName, $fn);
176
+    }
177
+
178
+    /**
179
+     * Register a new hook for a specific column that gets called before execution of the delete() method
180
+     * Only one hook per column can be registered at a time
181
+     * @param string $columnName The name of the column that is registered.
182
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
183
+     */
184
+    public function registerDeleteHook($columnName, $fn)
185
+    {
186
+        $this->registerHookOnAction(self::DELETE, $columnName, $fn);
187
+    }
188
+
189
+    /**
190
+     * Register a new hook for a specific column that gets called before execution of the search() method
191
+     * Only one hook per column can be registered at a time
192
+     * @param string $columnName The name of the column that is registered.
193
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object. The callable is required to take one argument: an instance of miBadger\Query\Query; 
194
+     */
195
+    public function registerSearchHook($columnName, $fn)
196
+    {
197
+        $this->registerHookOnAction(self::SEARCH, $columnName, $fn);
198
+    }
199
+
200
+    /**
201
+     * Adds a new column definition to the table.
202
+     * @param string $columnName The name of the column that is registered.
203
+     * @param Array $definition The definition of that column.
204
+     */
205
+    protected function extendTableDefinition($columnName, $definition)
206
+    {
207
+        if ($this->tableDefinition === null) {
208
+            throw new ActiveRecordException("tableDefinition is null, has parent been initialized in constructor?");
209
+        }
210
+
211
+        // Enforcing table can only be extended with new columns
212
+        if (array_key_exists($columnName, $this->tableDefinition)) {
213
+            $message = "Table is being extended with a column that already exists, ";
214
+            $message .= "\"$columnName\" conflicts with your table definition";
215
+            throw new ActiveRecordException($message, 0);
216
+        }
217
+
218
+        $this->tableDefinition[$columnName] = $definition;
219
+    }
220
+
221
+    public function hasColumn(string $column) {
222
+        return array_key_exists($column, $this->tableDefinition);
223
+    }
224
+
225
+    public function hasRelation(string $column, ActiveRecordInterface $record) {
226
+        if (!$this->hasColumn($column)) {
227
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
228
+        }
229
+
230
+        if (!isset($this->tableDefinition[$column]['relation'])) {
231
+            return false;
232
+        }
233
+
234
+        $relation = $this->tableDefinition[$column]['relation'];
235
+        if ($relation instanceof AbstractActiveRecord) {
236
+            // Injected object
237
+            return get_class($record) === get_class($relation);
238
+        } else {
239
+            // :: class definition
240
+            return get_class($record) === $relation;
241
+        }
242
+    }
243
+
244
+    public function hasProperty(string $column, $property) {
245
+        if (!$this->hasColumn($column)) {
246
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
247
+        }
248
+
249
+        try {
250
+            $enumValue = ColumnProperty::valueOf($property);
251
+        } catch (\UnexpectedValueException $e) {
252
+            throw new ActiveRecordException("Provided property \"$property\" is not a valid property", 0, $e);
253
+        }
254
+
255
+        $properties = $this->tableDefinition[$column]['properties'] ?? null;
256
+
257
+        return $properties !== null && (($properties & $enumValue->getValue()) > 0);
258
+    }
259
+
260
+    public function getColumnType(string $column) {
261
+        if (!$this->hasColumn($column)) {
262
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
263
+        }
264
+
265
+        return $this->tableDefinition[$column]['type'] ?? null;
266
+    }
267
+
268
+    public function getColumnLength(string $column) {
269
+        if (!$this->hasColumn($column)) {
270
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
271
+        }
272
+
273
+        return $this->tableDefinition[$column]['length'] ?? null;
274
+    }
275
+
276
+    public function getDefault(string $column) {
277
+        if (!$this->hasColumn($column)) {
278
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
279
+        }
280
+
281
+        return $this->tableDefinition[$column]['default'] ?? null;
282
+    }
283
+
284
+    public function validateColumn(string $column, $input) {
285
+        if (!$this->hasColumn($column)) {
286
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
287
+        }
288
+
289
+        $fn = $this->tableDefinition[$column]['validate'] ?? null;
290
+
291
+        if ($fn === null) {
292
+            return [true, ''];
293
+        }
294
+
295
+        if (!is_callable($fn)) {
296
+            throw new ActiveRecordException("Provided validation function is not callable", 0);
297
+        }
298
+
299
+        return $fn($input);
300
+    }
301
+
302
+    /**
303
+     * Useful for writing unit tests of models against ActiveRecord: 
304
+     * overrides a relation column with a relation onto a mock object.
305
+     * @param string $column the name of the column onto which to place the mock relation
306
+     * @param object $mock the instance of a mock object to palce onto the model.
307
+     */
308
+    public function injectInstanceOnRelation(string $column, $mock) {
309
+        if (!$this->hasColumn($column)) {
310
+            throw new ActiveRecordException("Provided column \"$column\" does not exist in table definition", 0);
311
+        }
312
+
313
+        $this->tableDefinition[$column]['relation'] = $mock;
314
+    }
315
+
316
+    /**
317
+     * Creates the entity as a table in the database
318
+     */
319
+    public function createTable()
320
+    {
321
+        $this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
322
+    }
323
+
324
+    /**
325
+     * Iterates over the specified constraints in the table definition, 
326
+     * 		and applies these to the database.
327
+     */
328
+    public function createTableConstraints()
329
+    {
330
+        // Iterate over columns, check whether "relation" field exists, if so create constraint
331
+        foreach ($this->tableDefinition as $colName => $definition) {
332
+            if (!isset($definition['relation'])) {
333
+                continue;
334
+            }
335
+
336
+            $relation = $definition['relation'];
337
+            $properties = $definition['properties'] ?? 0;
338 338
 			
339
-			if (is_string($relation) 
340
-				&& class_exists($relation) 
341
-				&& new $relation($this->pdo) instanceof AbstractActiveRecord) {
342
-				// ::class relation in tableDefinition
343
-				$target = new $definition['relation']($this->pdo);
344
-			}
345
-			else if ($relation instanceof AbstractActiveRecord) {
346
-				throw new ActiveRecordException(sprintf(
347
-					"Relation constraint on column \"%s\" of table \"%s\" can not be built from relation instance, use %s::class in table definition instead",
348
-					$colName,
349
-					$this->getTableName(),
350
-					get_class($relation)
351
-				));
352
-			}
353
-			else {
354
-				// Invalid class
355
-				throw new ActiveRecordException(sprintf(
356
-					"Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
357
-					$colName,
358
-					$this->getTableName()));
359
-			}
360
-
361
-			// Add new relation constraint on database
362
-			if ($properties & ColumnProperty::NOT_NULL) {
363
-				$constraintSql = SchemaBuilder::buildConstraintOnDeleteCascade($target->getTableName(), 'id', $this->getTableName(), $colName);
364
-			} else {
365
-				$constraintSql = SchemaBuilder::buildConstraintOnDeleteSetNull($target->getTableName(), 'id', $this->getTableName(), $colName);
366
-			}
367
-			$this->pdo->query($constraintSql);
368
-		}
369
-	}
370
-
371
-	/**
372
-	 * Returns the name -> variable mapping for the table definition.
373
-	 * @return Array The mapping
374
-	 */
375
-	protected function getActiveRecordColumns()
376
-	{
377
-		$bindings = [];
378
-		foreach ($this->tableDefinition as $colName => $definition) {
379
-
380
-			// Ignore the id column (key) when inserting or updating
381
-			if ($colName == self::COLUMN_NAME_ID) {
382
-				continue;
383
-			}
384
-
385
-			$bindings[$colName] = &$definition['value'];
386
-		}
387
-		return $bindings;
388
-	}
389
-
390
-	protected function insertDefaults()
391
-	{
392
-		// Insert default values for not-null fields
393
-		foreach ($this->tableDefinition as $colName => $colDef) {
394
-			if ($colDef['value'] === null
395
-				&& ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
396
-				&& isset($colDef['default'])) {
397
-				$this->tableDefinition[$colName]['value'] = $colDef['default'];
398
-			}
399
-		}		
400
-	}
401
-
402
-	/**
403
-	 * {@inheritdoc}
404
-	 */
405
-	public function create()
406
-	{
407
-		foreach ($this->createHooks as $colName => $fn) {
408
-			$fn();
409
-		}
410
-
411
-		$this->insertDefaults();
412
-
413
-		try {
414
-			(new Query($this->getPdo(), $this->getTableName()))
415
-				->insert($this->getActiveRecordColumns())
416
-				->execute();
417
-
418
-			$this->setId(intval($this->getPdo()->lastInsertId()));
419
-		} catch (\PDOException $e) {
420
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
421
-		}
422
-
423
-		return $this;
424
-	}
425
-
426
-	/**
427
-	 * {@inheritdoc}
428
-	 */
429
-	public function read($id)
430
-	{
431
-		$whereConditions = [
432
-			Query::Equal('id', $id)
433
-		];
434
-		foreach ($this->readHooks as $colName => $fn) {
435
-			$cond = $fn();
436
-			if ($cond !== null) {
437
-				$whereConditions[] = $cond;
438
-			}
439
-		}
440
-
441
-		try {
442
-			$row = (new Query($this->getPdo(), $this->getTableName()))
443
-				->select()
444
-				->where(Query::AndArray($whereConditions))
445
-				->execute()
446
-				->fetch();
339
+            if (is_string($relation) 
340
+                && class_exists($relation) 
341
+                && new $relation($this->pdo) instanceof AbstractActiveRecord) {
342
+                // ::class relation in tableDefinition
343
+                $target = new $definition['relation']($this->pdo);
344
+            }
345
+            else if ($relation instanceof AbstractActiveRecord) {
346
+                throw new ActiveRecordException(sprintf(
347
+                    "Relation constraint on column \"%s\" of table \"%s\" can not be built from relation instance, use %s::class in table definition instead",
348
+                    $colName,
349
+                    $this->getTableName(),
350
+                    get_class($relation)
351
+                ));
352
+            }
353
+            else {
354
+                // Invalid class
355
+                throw new ActiveRecordException(sprintf(
356
+                    "Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
357
+                    $colName,
358
+                    $this->getTableName()));
359
+            }
360
+
361
+            // Add new relation constraint on database
362
+            if ($properties & ColumnProperty::NOT_NULL) {
363
+                $constraintSql = SchemaBuilder::buildConstraintOnDeleteCascade($target->getTableName(), 'id', $this->getTableName(), $colName);
364
+            } else {
365
+                $constraintSql = SchemaBuilder::buildConstraintOnDeleteSetNull($target->getTableName(), 'id', $this->getTableName(), $colName);
366
+            }
367
+            $this->pdo->query($constraintSql);
368
+        }
369
+    }
370
+
371
+    /**
372
+     * Returns the name -> variable mapping for the table definition.
373
+     * @return Array The mapping
374
+     */
375
+    protected function getActiveRecordColumns()
376
+    {
377
+        $bindings = [];
378
+        foreach ($this->tableDefinition as $colName => $definition) {
379
+
380
+            // Ignore the id column (key) when inserting or updating
381
+            if ($colName == self::COLUMN_NAME_ID) {
382
+                continue;
383
+            }
384
+
385
+            $bindings[$colName] = &$definition['value'];
386
+        }
387
+        return $bindings;
388
+    }
389
+
390
+    protected function insertDefaults()
391
+    {
392
+        // Insert default values for not-null fields
393
+        foreach ($this->tableDefinition as $colName => $colDef) {
394
+            if ($colDef['value'] === null
395
+                && ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
396
+                && isset($colDef['default'])) {
397
+                $this->tableDefinition[$colName]['value'] = $colDef['default'];
398
+            }
399
+        }		
400
+    }
401
+
402
+    /**
403
+     * {@inheritdoc}
404
+     */
405
+    public function create()
406
+    {
407
+        foreach ($this->createHooks as $colName => $fn) {
408
+            $fn();
409
+        }
410
+
411
+        $this->insertDefaults();
412
+
413
+        try {
414
+            (new Query($this->getPdo(), $this->getTableName()))
415
+                ->insert($this->getActiveRecordColumns())
416
+                ->execute();
417
+
418
+            $this->setId(intval($this->getPdo()->lastInsertId()));
419
+        } catch (\PDOException $e) {
420
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
421
+        }
422
+
423
+        return $this;
424
+    }
425
+
426
+    /**
427
+     * {@inheritdoc}
428
+     */
429
+    public function read($id)
430
+    {
431
+        $whereConditions = [
432
+            Query::Equal('id', $id)
433
+        ];
434
+        foreach ($this->readHooks as $colName => $fn) {
435
+            $cond = $fn();
436
+            if ($cond !== null) {
437
+                $whereConditions[] = $cond;
438
+            }
439
+        }
440
+
441
+        try {
442
+            $row = (new Query($this->getPdo(), $this->getTableName()))
443
+                ->select()
444
+                ->where(Query::AndArray($whereConditions))
445
+                ->execute()
446
+                ->fetch();
447 447
 			
448
-			if ($row === false) {
449
-				$msg = sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName());
450
-				throw new ActiveRecordException($msg, ActiveRecordException::NOT_FOUND);
451
-			}
452
-
453
-			$this->fill($row)->setId($id);
454
-		} catch (\PDOException $e) {
455
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
456
-		}
457
-
458
-		return $this;
459
-	}
460
-
461
-	/**
462
-	 * {@inheritdoc}
463
-	 */
464
-	public function update()
465
-	{
466
-		foreach ($this->updateHooks as $colName => $fn) {
467
-			$fn();
468
-		}
469
-
470
-		try {
471
-			(new Query($this->getPdo(), $this->getTableName()))
472
-				->update($this->getActiveRecordColumns())
473
-				->where(Query::Equal('id', $this->getId()))
474
-				->execute();
475
-		} catch (\PDOException $e) {
476
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
477
-		}
478
-
479
-		return $this;
480
-	}
481
-
482
-	/**
483
-	 * {@inheritdoc}
484
-	 */
485
-	public function delete()
486
-	{
487
-		foreach ($this->deleteHooks as $colName => $fn) {
488
-			$fn();
489
-		}
490
-
491
-		try {
492
-			(new Query($this->getPdo(), $this->getTableName()))
493
-				->delete()
494
-				->where(Query::Equal('id', $this->getId()))
495
-				->execute();
496
-
497
-			$this->setId(null);
498
-		} catch (\PDOException $e) {
499
-			throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
500
-		}
501
-
502
-		return $this;
503
-	}
504
-
505
-	/**
506
-	 * {@inheritdoc}
507
-	 */
508
-	public function sync()
509
-	{
510
-		if (!$this->exists()) {
511
-			return $this->create();
512
-		}
513
-
514
-		return $this->update();
515
-	}
516
-
517
-	/**
518
-	 * {@inheritdoc}
519
-	 */
520
-	public function exists()
521
-	{
522
-		return $this->getId() !== null;
523
-	}
524
-
525
-	/**
526
-	 * {@inheritdoc}
527
-	 */
528
-	public function fill(array $attributes)
529
-	{
530
-		$columns = $this->getActiveRecordColumns();
531
-		$columns['id'] = &$this->id;
532
-
533
-		foreach ($attributes as $key => $value) {
534
-			if (array_key_exists($key, $columns)) {
535
-				$columns[$key] = $value;
536
-			}
537
-		}
538
-
539
-		return $this;
540
-	}
541
-
542
-	/**
543
-	 * Returns the serialized form of the specified columns
544
-	 * 
545
-	 * @return Array
546
-	 */
547
-	public function toArray(Array $fieldWhitelist)
548
-	{
549
-		$output = [];
550
-		foreach ($this->tableDefinition as $colName => $definition) {
551
-			if (in_array($colName, $fieldWhitelist)) {
552
-				$output[$colName] = $definition['value'];
553
-			}
554
-		}
555
-
556
-		return $output;
557
-	}
558
-
559
-	/**
560
-	 * {@inheritdoc}
561
-	 */
562
-	public function search(array $ignoredTraits = [])
563
-	{
564
-		$clauses = [];
565
-		foreach ($this->searchHooks as $column => $fn) {
566
-			if (!in_array($column, $ignoredTraits)) {
567
-				$clauses[] = $fn();
568
-			}
569
-		}
570
-
571
-		return new ActiveRecordQuery($this, $clauses);
572
-	}
573
-
574
-	/**
575
-	 * Returns the PDO.
576
-	 *
577
-	 * @return \PDO the PDO.
578
-	 */
579
-	public function getPdo()
580
-	{
581
-		return $this->pdo;
582
-	}
583
-
584
-	/**
585
-	 * Set the PDO.
586
-	 *
587
-	 * @param \PDO $pdo
588
-	 * @return $this
589
-	 */
590
-	protected function setPdo($pdo)
591
-	{
592
-		$this->pdo = $pdo;
593
-
594
-		return $this;
595
-	}
596
-
597
-	/**
598
-	 * Returns the ID.
599
-	 *
600
-	 * @return null|int The ID.
601
-	 */
602
-	public function getId()
603
-	{
604
-		return $this->id;
605
-	}
606
-
607
-	/**
608
-	 * Set the ID.
609
-	 *
610
-	 * @param int $id
611
-	 * @return $this
612
-	 */
613
-	protected function setId($id)
614
-	{
615
-		$this->id = $id;
616
-
617
-		return $this;
618
-	}
619
-
620
-	public function getFinalTableDefinition()
621
-	{
622
-		return $this->tableDefinition;
623
-	}
624
-
625
-	public function newInstance()
626
-	{
627
-		return new static($this->pdo);
628
-	}
629
-
630
-	/**
631
-	 * Returns the active record table.
632
-	 *
633
-	 * @return string the active record table name.
634
-	 */
635
-	abstract public function getTableName(): string;
636
-
637
-	/**
638
-	 * Returns the active record columns.
639
-	 *
640
-	 * @return array the active record columns.
641
-	 */
642
-	abstract protected function getTableDefinition(): Array;
448
+            if ($row === false) {
449
+                $msg = sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName());
450
+                throw new ActiveRecordException($msg, ActiveRecordException::NOT_FOUND);
451
+            }
452
+
453
+            $this->fill($row)->setId($id);
454
+        } catch (\PDOException $e) {
455
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
456
+        }
457
+
458
+        return $this;
459
+    }
460
+
461
+    /**
462
+     * {@inheritdoc}
463
+     */
464
+    public function update()
465
+    {
466
+        foreach ($this->updateHooks as $colName => $fn) {
467
+            $fn();
468
+        }
469
+
470
+        try {
471
+            (new Query($this->getPdo(), $this->getTableName()))
472
+                ->update($this->getActiveRecordColumns())
473
+                ->where(Query::Equal('id', $this->getId()))
474
+                ->execute();
475
+        } catch (\PDOException $e) {
476
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
477
+        }
478
+
479
+        return $this;
480
+    }
481
+
482
+    /**
483
+     * {@inheritdoc}
484
+     */
485
+    public function delete()
486
+    {
487
+        foreach ($this->deleteHooks as $colName => $fn) {
488
+            $fn();
489
+        }
490
+
491
+        try {
492
+            (new Query($this->getPdo(), $this->getTableName()))
493
+                ->delete()
494
+                ->where(Query::Equal('id', $this->getId()))
495
+                ->execute();
496
+
497
+            $this->setId(null);
498
+        } catch (\PDOException $e) {
499
+            throw new ActiveRecordException($e->getMessage(), ActiveRecordException::DB_ERROR, $e);
500
+        }
501
+
502
+        return $this;
503
+    }
504
+
505
+    /**
506
+     * {@inheritdoc}
507
+     */
508
+    public function sync()
509
+    {
510
+        if (!$this->exists()) {
511
+            return $this->create();
512
+        }
513
+
514
+        return $this->update();
515
+    }
516
+
517
+    /**
518
+     * {@inheritdoc}
519
+     */
520
+    public function exists()
521
+    {
522
+        return $this->getId() !== null;
523
+    }
524
+
525
+    /**
526
+     * {@inheritdoc}
527
+     */
528
+    public function fill(array $attributes)
529
+    {
530
+        $columns = $this->getActiveRecordColumns();
531
+        $columns['id'] = &$this->id;
532
+
533
+        foreach ($attributes as $key => $value) {
534
+            if (array_key_exists($key, $columns)) {
535
+                $columns[$key] = $value;
536
+            }
537
+        }
538
+
539
+        return $this;
540
+    }
541
+
542
+    /**
543
+     * Returns the serialized form of the specified columns
544
+     * 
545
+     * @return Array
546
+     */
547
+    public function toArray(Array $fieldWhitelist)
548
+    {
549
+        $output = [];
550
+        foreach ($this->tableDefinition as $colName => $definition) {
551
+            if (in_array($colName, $fieldWhitelist)) {
552
+                $output[$colName] = $definition['value'];
553
+            }
554
+        }
555
+
556
+        return $output;
557
+    }
558
+
559
+    /**
560
+     * {@inheritdoc}
561
+     */
562
+    public function search(array $ignoredTraits = [])
563
+    {
564
+        $clauses = [];
565
+        foreach ($this->searchHooks as $column => $fn) {
566
+            if (!in_array($column, $ignoredTraits)) {
567
+                $clauses[] = $fn();
568
+            }
569
+        }
570
+
571
+        return new ActiveRecordQuery($this, $clauses);
572
+    }
573
+
574
+    /**
575
+     * Returns the PDO.
576
+     *
577
+     * @return \PDO the PDO.
578
+     */
579
+    public function getPdo()
580
+    {
581
+        return $this->pdo;
582
+    }
583
+
584
+    /**
585
+     * Set the PDO.
586
+     *
587
+     * @param \PDO $pdo
588
+     * @return $this
589
+     */
590
+    protected function setPdo($pdo)
591
+    {
592
+        $this->pdo = $pdo;
593
+
594
+        return $this;
595
+    }
596
+
597
+    /**
598
+     * Returns the ID.
599
+     *
600
+     * @return null|int The ID.
601
+     */
602
+    public function getId()
603
+    {
604
+        return $this->id;
605
+    }
606
+
607
+    /**
608
+     * Set the ID.
609
+     *
610
+     * @param int $id
611
+     * @return $this
612
+     */
613
+    protected function setId($id)
614
+    {
615
+        $this->id = $id;
616
+
617
+        return $this;
618
+    }
619
+
620
+    public function getFinalTableDefinition()
621
+    {
622
+        return $this->tableDefinition;
623
+    }
624
+
625
+    public function newInstance()
626
+    {
627
+        return new static($this->pdo);
628
+    }
629
+
630
+    /**
631
+     * Returns the active record table.
632
+     *
633
+     * @return string the active record table name.
634
+     */
635
+    abstract public function getTableName(): string;
636
+
637
+    /**
638
+     * Returns the active record columns.
639
+     *
640
+     * @return array the active record columns.
641
+     */
642
+    abstract protected function getTableDefinition(): Array;
643 643
 }
Please login to merge, or discard this patch.
Braces   +2 added lines, -4 removed lines patch added patch discarded remove patch
@@ -341,16 +341,14 @@
 block discarded – undo
341 341
 				&& new $relation($this->pdo) instanceof AbstractActiveRecord) {
342 342
 				// ::class relation in tableDefinition
343 343
 				$target = new $definition['relation']($this->pdo);
344
-			}
345
-			else if ($relation instanceof AbstractActiveRecord) {
344
+			} else if ($relation instanceof AbstractActiveRecord) {
346 345
 				throw new ActiveRecordException(sprintf(
347 346
 					"Relation constraint on column \"%s\" of table \"%s\" can not be built from relation instance, use %s::class in table definition instead",
348 347
 					$colName,
349 348
 					$this->getTableName(),
350 349
 					get_class($relation)
351 350
 				));
352
-			}
353
-			else {
351
+			} else {
354 352
 				// Invalid class
355 353
 				throw new ActiveRecordException(sprintf(
356 354
 					"Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
Please login to merge, or discard this patch.
src/Traits/AutoApi.php 1 patch
Indentation   +415 added lines, -415 removed lines patch added patch discarded remove patch
@@ -9,436 +9,436 @@
 block discarded – undo
9 9
 
10 10
 trait AutoApi
11 11
 {
12
-	/* =======================================================================
12
+    /* =======================================================================
13 13
 	 * ===================== Automatic API Support ===========================
14 14
 	 * ======================================================================= */
15 15
 
16
-	/** @var array A map of column name to functions that hook the insert function */
17
-	protected $createHooks;
16
+    /** @var array A map of column name to functions that hook the insert function */
17
+    protected $createHooks;
18 18
 
19
-	/** @var array A map of column name to functions that hook the read function */
20
-	protected $readHooks;
19
+    /** @var array A map of column name to functions that hook the read function */
20
+    protected $readHooks;
21 21
 
22
-	/** @var array A map of column name to functions that hook the update function */
23
-	protected $updateHooks;
22
+    /** @var array A map of column name to functions that hook the update function */
23
+    protected $updateHooks;
24 24
 
25
-	/** @var array A map of column name to functions that hook the update function */
26
-	protected $deleteHooks;	
25
+    /** @var array A map of column name to functions that hook the update function */
26
+    protected $deleteHooks;	
27 27
 
28
-	/** @var array A map of column name to functions that hook the search function */
29
-	protected $searchHooks;
28
+    /** @var array A map of column name to functions that hook the search function */
29
+    protected $searchHooks;
30 30
 
31
-	/** @var array A list of table column definitions */
32
-	protected $tableDefinition;
31
+    /** @var array A list of table column definitions */
32
+    protected $tableDefinition;
33 33
 
34 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
-	public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null, int $maxResultLimit = 100): Array
44
-	{
45
-		$query = $this->search();
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
+    public function apiSearch(Array $queryParams, Array $fieldWhitelist, ?QueryExpression $whereClause = null, int $maxResultLimit = 100): Array
44
+    {
45
+        $query = $this->search();
46 46
 
47
-		// Build query
48
-		$orderColumn = $queryParams['search_order_by'] ?? null;
49
-		if (!in_array($orderColumn, $fieldWhitelist)) {
50
-			$orderColumn = null;
51
-		}
47
+        // Build query
48
+        $orderColumn = $queryParams['search_order_by'] ?? null;
49
+        if (!in_array($orderColumn, $fieldWhitelist)) {
50
+            $orderColumn = null;
51
+        }
52 52
 
53
-		$orderDirection = $queryParams['search_order_direction'] ?? null;
54
-		if ($orderColumn !== null) {
55
-			$query->orderBy($orderColumn, $orderDirection);
56
-		}
53
+        $orderDirection = $queryParams['search_order_direction'] ?? null;
54
+        if ($orderColumn !== null) {
55
+            $query->orderBy($orderColumn, $orderDirection);
56
+        }
57 57
 		
58
-		if ($whereClause !== null) {
59
-			$query->where($whereClause);
60
-		}
61
-
62
-		$limit = min((int) ($queryParams['search_limit'] ?? $maxResultLimit), $maxResultLimit);
63
-		$query->limit($limit);
64
-
65
-		$offset = $queryParams['search_offset'] ?? 0;
66
-		$query->offset($offset);
67
-
68
-		$numPages = $query->getNumberOfPages();
69
-		$currentPage = $query->getCurrentPage();
70
-
71
-		// Fetch results
72
-		$results = $query->fetchAll();
73
-		$resultsArray = [];
74
-		foreach ($results as $result) {
75
-			$resultsArray[] = $result->toArray($fieldWhitelist);
76
-		}
77
-
78
-		return [
79
-			'search_offset' => $offset,
80
-			'search_limit' => $limit,
81
-			'search_order_by' => $orderColumn,
82
-			'search_order_direction' => $orderDirection,
83
-			'search_pages' => $numPages,
84
-			'search_current' => $currentPage,
85
-			'data' => $resultsArray
86
-		];
87
-	}
88
-
89
-	/**
90
-	 * Performs a read call on the entity (modifying the current object) 
91
-	 * 	and returns a result array ($error, $data), with an optional error, and the results array (as filtered by the whitelist)
92
-	 * 	containing the loaded data.
93
-	 * 
94
-	 * @param string|int $id the id of the current entity
95
-	 * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
96
-	 * 
97
-	 * @param Array [$error, $result]
98
-	 * 				Where $error contains the error message (@TODO: & Error type?)
99
-	 * 				Where result is an associative array containing the data for this record, and the keys are a subset of $fieldWhitelist
100
-	 * 				
101
-	 */
102
-	public function apiRead($id, Array $fieldWhitelist = []): Array
103
-	{
104
-		try {
105
-			$this->read($id);	
106
-		} catch (ActiveRecordException $e) {
107
-			if ($e->getCode() === ActiveRecordException::NOT_FOUND) {
108
-				$err = [
109
-					'type' => 'invalid',
110
-					'message' => $e->getMessage()
111
-				];
112
-				return [$err, null];
113
-			}
114
-			throw $e;
115
-		}
116
-		return [null, $this->toArray($fieldWhitelist)];
117
-	}
118
-
119
-	/* =============================================================
58
+        if ($whereClause !== null) {
59
+            $query->where($whereClause);
60
+        }
61
+
62
+        $limit = min((int) ($queryParams['search_limit'] ?? $maxResultLimit), $maxResultLimit);
63
+        $query->limit($limit);
64
+
65
+        $offset = $queryParams['search_offset'] ?? 0;
66
+        $query->offset($offset);
67
+
68
+        $numPages = $query->getNumberOfPages();
69
+        $currentPage = $query->getCurrentPage();
70
+
71
+        // Fetch results
72
+        $results = $query->fetchAll();
73
+        $resultsArray = [];
74
+        foreach ($results as $result) {
75
+            $resultsArray[] = $result->toArray($fieldWhitelist);
76
+        }
77
+
78
+        return [
79
+            'search_offset' => $offset,
80
+            'search_limit' => $limit,
81
+            'search_order_by' => $orderColumn,
82
+            'search_order_direction' => $orderDirection,
83
+            'search_pages' => $numPages,
84
+            'search_current' => $currentPage,
85
+            'data' => $resultsArray
86
+        ];
87
+    }
88
+
89
+    /**
90
+     * Performs a read call on the entity (modifying the current object) 
91
+     * 	and returns a result array ($error, $data), with an optional error, and the results array (as filtered by the whitelist)
92
+     * 	containing the loaded data.
93
+     * 
94
+     * @param string|int $id the id of the current entity
95
+     * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
96
+     * 
97
+     * @param Array [$error, $result]
98
+     * 				Where $error contains the error message (@TODO: & Error type?)
99
+     * 				Where result is an associative array containing the data for this record, and the keys are a subset of $fieldWhitelist
100
+     * 				
101
+     */
102
+    public function apiRead($id, Array $fieldWhitelist = []): Array
103
+    {
104
+        try {
105
+            $this->read($id);	
106
+        } catch (ActiveRecordException $e) {
107
+            if ($e->getCode() === ActiveRecordException::NOT_FOUND) {
108
+                $err = [
109
+                    'type' => 'invalid',
110
+                    'message' => $e->getMessage()
111
+                ];
112
+                return [$err, null];
113
+            }
114
+            throw $e;
115
+        }
116
+        return [null, $this->toArray($fieldWhitelist)];
117
+    }
118
+
119
+    /* =============================================================
120 120
 	 * ===================== Constraint validation =================
121 121
 	 * ============================================================= */
122 122
 
123
-	/**
124
-	 * Copy all table variables between two instances
125
-	 */
126
-	public function syncInstanceFrom($from)
127
-	{
128
-		foreach ($this->tableDefinition as $colName => $definition) {
129
-			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
130
-		}
131
-	}
132
-
133
-	private function filterInputColumns($input, $whitelist)
134
-	{
135
-		$filteredInput = $input;
136
-		foreach ($input as $colName => $value) {
137
-			if (!in_array($colName, $whitelist)) {
138
-				unset($filteredInput[$colName]);
139
-			}
140
-		}
141
-		return $filteredInput;
142
-	}
143
-
144
-	private function validateExcessKeys($input)
145
-	{
146
-		$errors = [];
147
-		foreach ($input as $colName => $value) {
148
-			if (!array_key_exists($colName, $this->tableDefinition)) {
149
-				$errors[$colName] = [
150
-					'type' => 'unknown_field',
151
-					'message' => 'Unknown input field'
152
-				];
153
-				continue;
154
-			}
155
-		}
156
-		return $errors;
157
-	}
158
-
159
-	private function validateImmutableColumns($input)
160
-	{
161
-		$errors = [];
162
-		foreach ($this->tableDefinition as $colName => $definition) {
163
-			$property = $definition['properties'] ?? null;
164
-			if (array_key_exists($colName, $input)
165
-				&& $property & ColumnProperty::IMMUTABLE) {
166
-				$errors[$colName] = [
167
-					'type' => 'immutable',
168
-					'message' => 'Value cannot be changed'
169
-				];
170
-			}
171
-		}
172
-		return $errors;
173
-	}
174
-
175
-	/**
176
-	 * Checks whether input values are correct:
177
-	 * 1. Checks whether a value passes the validation function for that column
178
-	 * 2. Checks whether a value supplied to a relationship column is a valid value
179
-	 */
180
-	private function validateInputValues($input)
181
-	{
182
-		$errors = [];
183
-		foreach ($this->tableDefinition as $colName => $definition) {
184
-			// Validation check 1: If validate function is present
185
-			if (array_key_exists($colName, $input) 
186
-				&& is_callable($definition['validate'] ?? null)) {
187
-				$inputValue = $input[$colName];
188
-
189
-				// If validation function fails
190
-				[$status, $message] = $definition['validate']($inputValue);
191
-				if (!$status) {
192
-					$errors[$colName] = [
193
-						'type' => 'invalid',
194
-						'message' => $message
195
-					];
196
-				}	
197
-			}
198
-
199
-			// Validation check 2: If relation column, check whether entity exists
200
-			$properties = $definition['properties'] ?? null;
201
-			if (isset($definition['relation'])
202
-				&& ($properties & ColumnProperty::NOT_NULL)) {
123
+    /**
124
+     * Copy all table variables between two instances
125
+     */
126
+    public function syncInstanceFrom($from)
127
+    {
128
+        foreach ($this->tableDefinition as $colName => $definition) {
129
+            $this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
130
+        }
131
+    }
132
+
133
+    private function filterInputColumns($input, $whitelist)
134
+    {
135
+        $filteredInput = $input;
136
+        foreach ($input as $colName => $value) {
137
+            if (!in_array($colName, $whitelist)) {
138
+                unset($filteredInput[$colName]);
139
+            }
140
+        }
141
+        return $filteredInput;
142
+    }
143
+
144
+    private function validateExcessKeys($input)
145
+    {
146
+        $errors = [];
147
+        foreach ($input as $colName => $value) {
148
+            if (!array_key_exists($colName, $this->tableDefinition)) {
149
+                $errors[$colName] = [
150
+                    'type' => 'unknown_field',
151
+                    'message' => 'Unknown input field'
152
+                ];
153
+                continue;
154
+            }
155
+        }
156
+        return $errors;
157
+    }
158
+
159
+    private function validateImmutableColumns($input)
160
+    {
161
+        $errors = [];
162
+        foreach ($this->tableDefinition as $colName => $definition) {
163
+            $property = $definition['properties'] ?? null;
164
+            if (array_key_exists($colName, $input)
165
+                && $property & ColumnProperty::IMMUTABLE) {
166
+                $errors[$colName] = [
167
+                    'type' => 'immutable',
168
+                    'message' => 'Value cannot be changed'
169
+                ];
170
+            }
171
+        }
172
+        return $errors;
173
+    }
174
+
175
+    /**
176
+     * Checks whether input values are correct:
177
+     * 1. Checks whether a value passes the validation function for that column
178
+     * 2. Checks whether a value supplied to a relationship column is a valid value
179
+     */
180
+    private function validateInputValues($input)
181
+    {
182
+        $errors = [];
183
+        foreach ($this->tableDefinition as $colName => $definition) {
184
+            // Validation check 1: If validate function is present
185
+            if (array_key_exists($colName, $input) 
186
+                && is_callable($definition['validate'] ?? null)) {
187
+                $inputValue = $input[$colName];
188
+
189
+                // If validation function fails
190
+                [$status, $message] = $definition['validate']($inputValue);
191
+                if (!$status) {
192
+                    $errors[$colName] = [
193
+                        'type' => 'invalid',
194
+                        'message' => $message
195
+                    ];
196
+                }	
197
+            }
198
+
199
+            // Validation check 2: If relation column, check whether entity exists
200
+            $properties = $definition['properties'] ?? null;
201
+            if (isset($definition['relation'])
202
+                && ($properties & ColumnProperty::NOT_NULL)) {
203 203
 				
204
-				try {
205
-					if ($definition['relation'] instanceof AbstractActiveRecord) {
206
-						$instance = $definition['relation'];
207
-					} else {
208
-						$instance = new $definition['relation']($this->pdo);	
209
-					}
210
-					$instance->read($input[$colName] ?? $definition['value'] ?? null);
211
-				} catch (ActiveRecordException $e) {
212
-					$errors[$colName] = [
213
-						'type' => 'invalid',
214
-						'message' => 'Entry for this value does not exist'
215
-					];
216
-				}
217
-			}
218
-		}
219
-		return $errors;
220
-	}
221
-
222
-	/**
223
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
224
-	 * Determines whether there are required columns for which no data is provided
225
-	 */
226
-	private function validateMissingKeys($input)
227
-	{
228
-		$errors = [];
229
-
230
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
231
-			$default = $colDefinition['default'] ?? null;
232
-			$properties = $colDefinition['properties'] ?? null;
233
-			$value = $colDefinition['value'];
234
-
235
-			// If nullable and default not set => null
236
-			// If nullable and default null => default (null)
237
-			// If nullable and default set => default (value)
238
-
239
-			// if not nullable and default not set => error
240
-			// if not nullable and default null => error
241
-			// if not nullable and default st => default (value)
242
-			// => if not nullable and default null and value not set (or null) => error message in this method
243
-			if ($properties & ColumnProperty::NOT_NULL
244
-				&& $default === null
245
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
246
-				&& (!array_key_exists($colName, $input) 
247
-					|| $input[$colName] === null 
248
-					|| (is_string($input[$colName]) && $input[$colName] === '') )
249
-				&& ($value === null
250
-					|| (is_string($value) && $value === ''))) {
251
-				$errors[$colName] = [
252
-					'type' => 'missing',
253
-					'message' => sprintf("The required field \"%s\" is missing", $colName)
254
-				];
255
-			} 
256
-		}
257
-
258
-		return $errors;
259
-	}
260
-
261
-	/**
262
-	 * Copies the values for entries in the input with matching variable names in the record definition
263
-	 * @param Array $input The input data to be loaded into $this record
264
-	 */
265
-	private function loadData($input)
266
-	{
267
-		foreach ($this->tableDefinition as $colName => $definition) {
268
-			// Skip if this table column does not appear in the input
269
-			if (!array_key_exists($colName, $input)) {
270
-				continue;
271
-			}
272
-
273
-			// Use setter if known, otherwise set value directly
274
-			$fn = $definition['setter'] ?? null;
275
-			if (is_callable($fn)) {
276
-				$fn($input[$colName]);
277
-			} else {
278
-				$definition['value'] = $input[$colName];
279
-			}
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * @param Array $input Associative array of input values
285
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
286
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
287
-	 * 					of the modified data.
288
-	 */
289
-	public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
290
-	{
291
-		// Clone $this to new instance (for restoring if validation goes wrong)
292
-		$transaction = $this->newInstance();
293
-		$errors = [];
294
-
295
-		// Filter out all non-whitelisted input values
296
-		$input = $this->filterInputColumns($input, $createWhitelist);
297
-
298
-		// Validate excess keys
299
-		$errors += $transaction->validateExcessKeys($input);
300
-
301
-		// Validate input values (using validation function)
302
-		$errors += $transaction->validateInputValues($input);
303
-
304
-		// "Copy" data into transaction
305
-		$transaction->loadData($input);
306
-
307
-		// Run create hooks
308
-		foreach ($transaction->createHooks as $colName => $fn) {
309
-			$fn();
310
-		}
311
-
312
-		// Validate missing keys
313
-		$errors += $transaction->validateMissingKeys($input);
314
-
315
-		// If no errors, commit the pending data
316
-		if (empty($errors)) {
317
-			$this->syncInstanceFrom($transaction);
318
-
319
-			// Insert default values for not-null fields
320
-			$this->insertDefaults();
321
-
322
-			try {
323
-				(new Query($this->getPdo(), $this->getTableName()))
324
-					->insert($this->getActiveRecordColumns())
325
-					->execute();
326
-
327
-				$this->setId(intval($this->getPdo()->lastInsertId()));
328
-			} catch (\PDOException $e) {
329
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
330
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
331
-			}
332
-
333
-			return [null, $this->toArray($readWhitelist)];
334
-		} else {
335
-			return [$errors, null];
336
-		}
337
-	}
338
-
339
-	/**
340
-	 * @param Array $input Associative array of input values
341
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
342
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
343
-	 * 					of the modified data.
344
-	 */
345
-	public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
346
-	{
347
-		$transaction = $this->newInstance();
348
-		$transaction->syncInstanceFrom($this);
349
-		$errors = [];
350
-
351
-		// Filter out all non-whitelisted input values
352
-		$input = $this->filterInputColumns($input, $updateWhitelist);
353
-
354
-		// Check for excess keys
355
-		$errors += $transaction->validateExcessKeys($input);
356
-
357
-		// Check for immutable keys
358
-		$errors += $transaction->validateImmutableColumns($input);
359
-
360
-		// Validate input values (using validation function)
361
-		$errors += $transaction->validateInputValues($input);
362
-
363
-		// "Copy" data into transaction
364
-		$transaction->loadData($input);
365
-
366
-		// Run create hooks
367
-		foreach ($transaction->updateHooks as $colName => $fn) {
368
-			$fn();
369
-		}
370
-
371
-		// Validate missing keys
372
-		$errors += $transaction->validateMissingKeys($input);
373
-
374
-		// Update database
375
-		if (empty($errors)) {
376
-			$this->syncInstanceFrom($transaction);
377
-
378
-			try {
379
-				(new Query($this->getPdo(), $this->getTableName()))
380
-					->update($this->getActiveRecordColumns())
381
-					->where(Query::Equal('id', $this->getId()))
382
-					->execute();
383
-			} catch (\PDOException $e) {
384
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
385
-			}
386
-
387
-			return [null, $this->toArray($readWhitelist)];
388
-		} else {
389
-			return [$errors, null];
390
-		}
391
-	}
392
-
393
-	/**
394
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
395
-	 *
396
-	 * @param mixed $id
397
-	 * @return $this
398
-	 * @throws ActiveRecordException on failure.
399
-	 */
400
-	abstract public function read($id);
401
-
402
-	/**
403
-	 * Returns the PDO.
404
-	 *
405
-	 * @return \PDO the PDO.
406
-	 */
407
-	abstract public function getPdo();
408
-
409
-	/**
410
-	 * Set the ID.
411
-	 *
412
-	 * @param int $id
413
-	 * @return $this
414
-	 */
415
-	abstract protected function setId($id);
416
-
417
-	/**
418
-	 * Returns the ID.
419
-	 *
420
-	 * @return null|int The ID.
421
-	 */
422
-	abstract protected function getId();
423
-
424
-
425
-	/**
426
-	 * Returns the serialized form of the specified columns
427
-	 * 
428
-	 * @return Array
429
-	 */
430
-	abstract public function toArray(Array $fieldWhitelist);
431
-
432
-	/**
433
-	 * Returns the active record table.
434
-	 *
435
-	 * @return string the active record table name.
436
-	 */
437
-	abstract public function getTableName();
438
-
439
-	/**
440
-	 * Returns the name -> variable mapping for the table definition.
441
-	 * @return Array The mapping
442
-	 */
443
-	abstract protected function getActiveRecordColumns();
204
+                try {
205
+                    if ($definition['relation'] instanceof AbstractActiveRecord) {
206
+                        $instance = $definition['relation'];
207
+                    } else {
208
+                        $instance = new $definition['relation']($this->pdo);	
209
+                    }
210
+                    $instance->read($input[$colName] ?? $definition['value'] ?? null);
211
+                } catch (ActiveRecordException $e) {
212
+                    $errors[$colName] = [
213
+                        'type' => 'invalid',
214
+                        'message' => 'Entry for this value does not exist'
215
+                    ];
216
+                }
217
+            }
218
+        }
219
+        return $errors;
220
+    }
221
+
222
+    /**
223
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
224
+     * Determines whether there are required columns for which no data is provided
225
+     */
226
+    private function validateMissingKeys($input)
227
+    {
228
+        $errors = [];
229
+
230
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
231
+            $default = $colDefinition['default'] ?? null;
232
+            $properties = $colDefinition['properties'] ?? null;
233
+            $value = $colDefinition['value'];
234
+
235
+            // If nullable and default not set => null
236
+            // If nullable and default null => default (null)
237
+            // If nullable and default set => default (value)
238
+
239
+            // if not nullable and default not set => error
240
+            // if not nullable and default null => error
241
+            // if not nullable and default st => default (value)
242
+            // => if not nullable and default null and value not set (or null) => error message in this method
243
+            if ($properties & ColumnProperty::NOT_NULL
244
+                && $default === null
245
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
246
+                && (!array_key_exists($colName, $input) 
247
+                    || $input[$colName] === null 
248
+                    || (is_string($input[$colName]) && $input[$colName] === '') )
249
+                && ($value === null
250
+                    || (is_string($value) && $value === ''))) {
251
+                $errors[$colName] = [
252
+                    'type' => 'missing',
253
+                    'message' => sprintf("The required field \"%s\" is missing", $colName)
254
+                ];
255
+            } 
256
+        }
257
+
258
+        return $errors;
259
+    }
260
+
261
+    /**
262
+     * Copies the values for entries in the input with matching variable names in the record definition
263
+     * @param Array $input The input data to be loaded into $this record
264
+     */
265
+    private function loadData($input)
266
+    {
267
+        foreach ($this->tableDefinition as $colName => $definition) {
268
+            // Skip if this table column does not appear in the input
269
+            if (!array_key_exists($colName, $input)) {
270
+                continue;
271
+            }
272
+
273
+            // Use setter if known, otherwise set value directly
274
+            $fn = $definition['setter'] ?? null;
275
+            if (is_callable($fn)) {
276
+                $fn($input[$colName]);
277
+            } else {
278
+                $definition['value'] = $input[$colName];
279
+            }
280
+        }
281
+    }
282
+
283
+    /**
284
+     * @param Array $input Associative array of input values
285
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
286
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
287
+     * 					of the modified data.
288
+     */
289
+    public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
290
+    {
291
+        // Clone $this to new instance (for restoring if validation goes wrong)
292
+        $transaction = $this->newInstance();
293
+        $errors = [];
294
+
295
+        // Filter out all non-whitelisted input values
296
+        $input = $this->filterInputColumns($input, $createWhitelist);
297
+
298
+        // Validate excess keys
299
+        $errors += $transaction->validateExcessKeys($input);
300
+
301
+        // Validate input values (using validation function)
302
+        $errors += $transaction->validateInputValues($input);
303
+
304
+        // "Copy" data into transaction
305
+        $transaction->loadData($input);
306
+
307
+        // Run create hooks
308
+        foreach ($transaction->createHooks as $colName => $fn) {
309
+            $fn();
310
+        }
311
+
312
+        // Validate missing keys
313
+        $errors += $transaction->validateMissingKeys($input);
314
+
315
+        // If no errors, commit the pending data
316
+        if (empty($errors)) {
317
+            $this->syncInstanceFrom($transaction);
318
+
319
+            // Insert default values for not-null fields
320
+            $this->insertDefaults();
321
+
322
+            try {
323
+                (new Query($this->getPdo(), $this->getTableName()))
324
+                    ->insert($this->getActiveRecordColumns())
325
+                    ->execute();
326
+
327
+                $this->setId(intval($this->getPdo()->lastInsertId()));
328
+            } catch (\PDOException $e) {
329
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
330
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
331
+            }
332
+
333
+            return [null, $this->toArray($readWhitelist)];
334
+        } else {
335
+            return [$errors, null];
336
+        }
337
+    }
338
+
339
+    /**
340
+     * @param Array $input Associative array of input values
341
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
342
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
343
+     * 					of the modified data.
344
+     */
345
+    public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
346
+    {
347
+        $transaction = $this->newInstance();
348
+        $transaction->syncInstanceFrom($this);
349
+        $errors = [];
350
+
351
+        // Filter out all non-whitelisted input values
352
+        $input = $this->filterInputColumns($input, $updateWhitelist);
353
+
354
+        // Check for excess keys
355
+        $errors += $transaction->validateExcessKeys($input);
356
+
357
+        // Check for immutable keys
358
+        $errors += $transaction->validateImmutableColumns($input);
359
+
360
+        // Validate input values (using validation function)
361
+        $errors += $transaction->validateInputValues($input);
362
+
363
+        // "Copy" data into transaction
364
+        $transaction->loadData($input);
365
+
366
+        // Run create hooks
367
+        foreach ($transaction->updateHooks as $colName => $fn) {
368
+            $fn();
369
+        }
370
+
371
+        // Validate missing keys
372
+        $errors += $transaction->validateMissingKeys($input);
373
+
374
+        // Update database
375
+        if (empty($errors)) {
376
+            $this->syncInstanceFrom($transaction);
377
+
378
+            try {
379
+                (new Query($this->getPdo(), $this->getTableName()))
380
+                    ->update($this->getActiveRecordColumns())
381
+                    ->where(Query::Equal('id', $this->getId()))
382
+                    ->execute();
383
+            } catch (\PDOException $e) {
384
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
385
+            }
386
+
387
+            return [null, $this->toArray($readWhitelist)];
388
+        } else {
389
+            return [$errors, null];
390
+        }
391
+    }
392
+
393
+    /**
394
+     * Returns this active record after reading the attributes from the entry with the given identifier.
395
+     *
396
+     * @param mixed $id
397
+     * @return $this
398
+     * @throws ActiveRecordException on failure.
399
+     */
400
+    abstract public function read($id);
401
+
402
+    /**
403
+     * Returns the PDO.
404
+     *
405
+     * @return \PDO the PDO.
406
+     */
407
+    abstract public function getPdo();
408
+
409
+    /**
410
+     * Set the ID.
411
+     *
412
+     * @param int $id
413
+     * @return $this
414
+     */
415
+    abstract protected function setId($id);
416
+
417
+    /**
418
+     * Returns the ID.
419
+     *
420
+     * @return null|int The ID.
421
+     */
422
+    abstract protected function getId();
423
+
424
+
425
+    /**
426
+     * Returns the serialized form of the specified columns
427
+     * 
428
+     * @return Array
429
+     */
430
+    abstract public function toArray(Array $fieldWhitelist);
431
+
432
+    /**
433
+     * Returns the active record table.
434
+     *
435
+     * @return string the active record table name.
436
+     */
437
+    abstract public function getTableName();
438
+
439
+    /**
440
+     * Returns the name -> variable mapping for the table definition.
441
+     * @return Array The mapping
442
+     */
443
+    abstract protected function getActiveRecordColumns();
444 444
 }
Please login to merge, or discard this patch.