Completed
Push — v2 ( 4d3385...e36f84 )
by Berend
04:08
created
src/SchemaBuilder.php 1 patch
Indentation   +145 added lines, -145 removed lines patch added patch discarded remove patch
@@ -18,157 +18,157 @@
 block discarded – undo
18 18
  */
19 19
 class SchemaBuilder
20 20
 {
21
-	/**
22
-	 * builds a MySQL constraint statement for the given parameters
23
-	 * @param string $parentTable
24
-	 * @param string $parentColumn
25
-	 * @param string $childTable
26
-	 * @param string $childColumn
27
-	 * @return string The MySQL table constraint string
28
-	 */
29
-	public static function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
30
-	{
31
-		$template = <<<SQL
21
+    /**
22
+     * builds a MySQL constraint statement for the given parameters
23
+     * @param string $parentTable
24
+     * @param string $parentColumn
25
+     * @param string $childTable
26
+     * @param string $childColumn
27
+     * @return string The MySQL table constraint string
28
+     */
29
+    public static function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
30
+    {
31
+        $template = <<<SQL
32 32
 ALTER TABLE `%s`
33 33
 ADD CONSTRAINT
34 34
 FOREIGN KEY (`%s`)
35 35
 REFERENCES `%s`(`%s`)
36 36
 ON DELETE CASCADE;
37 37
 SQL;
38
-		return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
39
-	}
40
-
41
-	/**
42
-	 * Returns the type string as it should appear in the mysql create table statement for the given column
43
-	 * @return string The type string
44
-	 */
45
-	public static function getDatabaseTypeString($colName, $type, $length)
46
-	{
47
-		switch (strtoupper($type)) {
48
-			case '':
49
-				throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
38
+        return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
39
+    }
40
+
41
+    /**
42
+     * Returns the type string as it should appear in the mysql create table statement for the given column
43
+     * @return string The type string
44
+     */
45
+    public static function getDatabaseTypeString($colName, $type, $length)
46
+    {
47
+        switch (strtoupper($type)) {
48
+            case '':
49
+                throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
50 50
 			
51
-			case 'BOOL';
52
-			case 'BOOLEAN':
53
-			case 'DATETIME':
54
-			case 'DATE':
55
-			case 'TIME':
56
-			case 'TEXT':
57
-			case 'INT UNSIGNED':
58
-				return $type;
59
-
60
-			case 'VARCHAR':
61
-				if ($length === null) {
62
-					throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
63
-				} else {
64
-					return sprintf('%s(%d)', $type, $length);	
65
-				}
66
-
67
-			case 'INT':
68
-			case 'TINYINT':
69
-			case 'BIGINT':
70
-			default: 	
71
-				// Implicitly assuming that non-specified cases are correct without a length parameter
72
-				if ($length === null) {
73
-					return $type;
74
-				} else {
75
-					return sprintf('%s(%d)', $type, $length);	
76
-				}
77
-		}
78
-	}
79
-
80
-	/**
81
-	 * Builds the part of a MySQL create table statement that corresponds to the supplied column
82
-	 * @param string $colName 	Name of the database column
83
-	 * @param string $type 		The type of the string
84
-	 * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
85
-	 * @return string
86
-	 */
87
-	public static function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
88
-	{
89
-		$stmnt = sprintf('`%s` %s ', $colName, self::getDatabaseTypeString($colName, $type, $length));
90
-		if ($properties & ColumnProperty::NOT_NULL) {
91
-			$stmnt .= 'NOT NULL ';
92
-		} else {
93
-			$stmnt .= 'NULL ';
94
-		}
95
-
96
-		if ($default !== NULL) {
97
-			$stmnt .= 'DEFAULT ' . var_export($default, true) . ' ';
98
-		}
99
-
100
-		if ($properties & ColumnProperty::AUTO_INCREMENT) {
101
-			$stmnt .= 'AUTO_INCREMENT ';
102
-		}
103
-
104
-		if ($properties & ColumnProperty::UNIQUE) {
105
-			$stmnt .= 'UNIQUE ';
106
-		}
107
-
108
-		if ($properties & ColumnProperty::PRIMARY_KEY) {
109
-			$stmnt .= 'PRIMARY KEY ';
110
-		}
111
-
112
-		return $stmnt;
113
-	}
114
-
115
-	/**
116
-	 * Sorts the column statement components in the order such that the id appears first, 
117
-	 * 		followed by all other columns in alphabetical ascending order
118
-	 * @param   Array $colStatements Array of column statements
119
-	 * @return  Array
120
-	 */
121
-	private static function sortColumnStatements($colStatements)
122
-	{
123
-		// Find ID statement and put it first
124
-		$sortedStatements = [];
125
-
126
-		$sortedStatements[] = $colStatements[AbstractActiveRecord::COLUMN_NAME_ID];
127
-		unset($colStatements[AbstractActiveRecord::COLUMN_NAME_ID]);
128
-
129
-		// Sort remaining columns in alphabetical order
130
-		$columns = array_keys($colStatements);
131
-		sort($columns);
132
-		foreach ($columns as $colName) {
133
-			$sortedStatements[] = $colStatements[$colName];
134
-		}
135
-
136
-		return $sortedStatements;
137
-	}
138
-
139
-	/**
140
-	 * Builds the MySQL Create Table statement for the internal table definition
141
-	 * @return string
142
-	 */
143
-	public static function buildCreateTableSQL($tableName, $tableDefinition)
144
-	{
145
-		$columnStatements = [];
146
-		foreach ($tableDefinition as $colName => $definition) {
147
-			// Destructure column definition
148
-			$type    = $definition['type'] ?? null;
149
-			$default = $definition['default'] ?? null;
150
-			$length  = $definition['length'] ?? null;
151
-			$properties = $definition['properties'] ?? null;
152
-
153
-			if (isset($definition['relation']) && $type !== null) {
154
-				$msg = sprintf("Column \"%s\" on table \"%s\": ", $colName, $tableName);
155
-				$msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
156
-				throw new ActiveRecordException($msg);
157
-			} else if (isset($definition['relation'])) {
158
-				$type = AbstractActiveRecord::COLUMN_TYPE_ID;
159
-			}
160
-
161
-			$columnStatements[$colName] = self::buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
162
-		}
163
-
164
-		// Sort table (first column is id, the remaining are alphabetically sorted)
165
-		$columnStatements = self::sortColumnStatements($columnStatements);
166
-
167
-		$sql = sprintf("CREATE TABLE %s (\n%s\n) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 
168
-			$tableName, 
169
-			implode(",\n", $columnStatements));
170
-
171
-		return $sql;
172
-	}
51
+            case 'BOOL';
52
+            case 'BOOLEAN':
53
+            case 'DATETIME':
54
+            case 'DATE':
55
+            case 'TIME':
56
+            case 'TEXT':
57
+            case 'INT UNSIGNED':
58
+                return $type;
59
+
60
+            case 'VARCHAR':
61
+                if ($length === null) {
62
+                    throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
63
+                } else {
64
+                    return sprintf('%s(%d)', $type, $length);	
65
+                }
66
+
67
+            case 'INT':
68
+            case 'TINYINT':
69
+            case 'BIGINT':
70
+            default: 	
71
+                // Implicitly assuming that non-specified cases are correct without a length parameter
72
+                if ($length === null) {
73
+                    return $type;
74
+                } else {
75
+                    return sprintf('%s(%d)', $type, $length);	
76
+                }
77
+        }
78
+    }
79
+
80
+    /**
81
+     * Builds the part of a MySQL create table statement that corresponds to the supplied column
82
+     * @param string $colName 	Name of the database column
83
+     * @param string $type 		The type of the string
84
+     * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
85
+     * @return string
86
+     */
87
+    public static function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
88
+    {
89
+        $stmnt = sprintf('`%s` %s ', $colName, self::getDatabaseTypeString($colName, $type, $length));
90
+        if ($properties & ColumnProperty::NOT_NULL) {
91
+            $stmnt .= 'NOT NULL ';
92
+        } else {
93
+            $stmnt .= 'NULL ';
94
+        }
95
+
96
+        if ($default !== NULL) {
97
+            $stmnt .= 'DEFAULT ' . var_export($default, true) . ' ';
98
+        }
99
+
100
+        if ($properties & ColumnProperty::AUTO_INCREMENT) {
101
+            $stmnt .= 'AUTO_INCREMENT ';
102
+        }
103
+
104
+        if ($properties & ColumnProperty::UNIQUE) {
105
+            $stmnt .= 'UNIQUE ';
106
+        }
107
+
108
+        if ($properties & ColumnProperty::PRIMARY_KEY) {
109
+            $stmnt .= 'PRIMARY KEY ';
110
+        }
111
+
112
+        return $stmnt;
113
+    }
114
+
115
+    /**
116
+     * Sorts the column statement components in the order such that the id appears first, 
117
+     * 		followed by all other columns in alphabetical ascending order
118
+     * @param   Array $colStatements Array of column statements
119
+     * @return  Array
120
+     */
121
+    private static function sortColumnStatements($colStatements)
122
+    {
123
+        // Find ID statement and put it first
124
+        $sortedStatements = [];
125
+
126
+        $sortedStatements[] = $colStatements[AbstractActiveRecord::COLUMN_NAME_ID];
127
+        unset($colStatements[AbstractActiveRecord::COLUMN_NAME_ID]);
128
+
129
+        // Sort remaining columns in alphabetical order
130
+        $columns = array_keys($colStatements);
131
+        sort($columns);
132
+        foreach ($columns as $colName) {
133
+            $sortedStatements[] = $colStatements[$colName];
134
+        }
135
+
136
+        return $sortedStatements;
137
+    }
138
+
139
+    /**
140
+     * Builds the MySQL Create Table statement for the internal table definition
141
+     * @return string
142
+     */
143
+    public static function buildCreateTableSQL($tableName, $tableDefinition)
144
+    {
145
+        $columnStatements = [];
146
+        foreach ($tableDefinition as $colName => $definition) {
147
+            // Destructure column definition
148
+            $type    = $definition['type'] ?? null;
149
+            $default = $definition['default'] ?? null;
150
+            $length  = $definition['length'] ?? null;
151
+            $properties = $definition['properties'] ?? null;
152
+
153
+            if (isset($definition['relation']) && $type !== null) {
154
+                $msg = sprintf("Column \"%s\" on table \"%s\": ", $colName, $tableName);
155
+                $msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
156
+                throw new ActiveRecordException($msg);
157
+            } else if (isset($definition['relation'])) {
158
+                $type = AbstractActiveRecord::COLUMN_TYPE_ID;
159
+            }
160
+
161
+            $columnStatements[$colName] = self::buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
162
+        }
163
+
164
+        // Sort table (first column is id, the remaining are alphabetically sorted)
165
+        $columnStatements = self::sortColumnStatements($columnStatements);
166
+
167
+        $sql = sprintf("CREATE TABLE %s (\n%s\n) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 
168
+            $tableName, 
169
+            implode(",\n", $columnStatements));
170
+
171
+        return $sql;
172
+    }
173 173
 
174 174
 }
Please login to merge, or discard this patch.
src/AbstractActiveRecord.php 1 patch
Indentation   +484 added lines, -484 removed lines patch added patch discarded remove patch
@@ -18,489 +18,489 @@
 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
-	public 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
-	/**
222
-	 * Creates the entity as a table in the database
223
-	 */
224
-	public function createTable()
225
-	{
226
-		$this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
227
-	}
228
-
229
-	/**
230
-	 * Iterates over the specified constraints in the table definition, 
231
-	 * 		and applies these to the database.
232
-	 */
233
-	public function createTableConstraints()
234
-	{
235
-		// Iterate over columns, check whether "relation" field exists, if so create constraint
236
-		foreach ($this->tableDefinition as $colName => $definition) {
237
-			if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
238
-				// Forge new relation
239
-				$target = $definition['relation'];
240
-				$constraintSql = SchemaBuilder::buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
241
-
242
-				$this->pdo->query($constraintSql);
243
-			} else if (isset($definition['relation'])) {
244
-				$msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
245
-					$colName,
246
-					$this->getTableName());
247
-				throw new ActiveRecordException($msg);
248
-			}
249
-		}
250
-	}
251
-
252
-	/**
253
-	 * Returns the name -> variable mapping for the table definition.
254
-	 * @return Array The mapping
255
-	 */
256
-	protected function getActiveRecordColumns()
257
-	{
258
-		$bindings = [];
259
-		foreach ($this->tableDefinition as $colName => $definition) {
260
-
261
-			// Ignore the id column (key) when inserting or updating
262
-			if ($colName == self::COLUMN_NAME_ID) {
263
-				continue;
264
-			}
265
-
266
-			$bindings[$colName] = &$definition['value'];
267
-		}
268
-		return $bindings;
269
-	}
270
-
271
-	protected function insertDefaults()
272
-	{
273
-		// Insert default values for not-null fields
274
-		foreach ($this->tableDefinition as $colName => $colDef) {
275
-			if ($colDef['value'] === null
276
-				&& ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
277
-				&& isset($colDef['default'])) {
278
-				$this->tableDefinition[$colName]['value'] = $colDef['default'];
279
-			}
280
-		}		
281
-	}
282
-
283
-	/**
284
-	 * {@inheritdoc}
285
-	 */
286
-	public function create()
287
-	{
288
-		foreach ($this->createHooks as $colName => $fn) {
289
-			$fn();
290
-		}
291
-
292
-		$this->insertDefaults();
293
-
294
-		try {
295
-			(new Query($this->getPdo(), $this->getTableName()))
296
-				->insert($this->getActiveRecordColumns())
297
-				->execute();
298
-
299
-			$this->setId(intval($this->getPdo()->lastInsertId()));
300
-		} catch (\PDOException $e) {
301
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
302
-		}
303
-
304
-		return $this;
305
-	}
306
-
307
-	/**
308
-	 * {@inheritdoc}
309
-	 */
310
-	public function read($id)
311
-	{
312
-		$whereConditions = [
313
-			Query::Equal('id', $id)
314
-		];
315
-		foreach ($this->readHooks as $colName => $fn) {
316
-			$cond = $fn();
317
-			if ($cond !== null) {
318
-				$whereConditions[] = $cond;
319
-			}
320
-		}
321
-
322
-		try {
323
-			$row = (new Query($this->getPdo(), $this->getTableName()))
324
-				->select()
325
-				->where(Query::AndArray($whereConditions))
326
-				->execute()
327
-				->fetch();
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
+    public 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
+    /**
222
+     * Creates the entity as a table in the database
223
+     */
224
+    public function createTable()
225
+    {
226
+        $this->pdo->query(SchemaBuilder::buildCreateTableSQL($this->getTableName(), $this->tableDefinition));
227
+    }
228
+
229
+    /**
230
+     * Iterates over the specified constraints in the table definition, 
231
+     * 		and applies these to the database.
232
+     */
233
+    public function createTableConstraints()
234
+    {
235
+        // Iterate over columns, check whether "relation" field exists, if so create constraint
236
+        foreach ($this->tableDefinition as $colName => $definition) {
237
+            if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
238
+                // Forge new relation
239
+                $target = $definition['relation'];
240
+                $constraintSql = SchemaBuilder::buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
241
+
242
+                $this->pdo->query($constraintSql);
243
+            } else if (isset($definition['relation'])) {
244
+                $msg = sprintf("Relation constraint on column \"%s\" of table \"%s\" does not contain a valid ActiveRecord instance", 
245
+                    $colName,
246
+                    $this->getTableName());
247
+                throw new ActiveRecordException($msg);
248
+            }
249
+        }
250
+    }
251
+
252
+    /**
253
+     * Returns the name -> variable mapping for the table definition.
254
+     * @return Array The mapping
255
+     */
256
+    protected function getActiveRecordColumns()
257
+    {
258
+        $bindings = [];
259
+        foreach ($this->tableDefinition as $colName => $definition) {
260
+
261
+            // Ignore the id column (key) when inserting or updating
262
+            if ($colName == self::COLUMN_NAME_ID) {
263
+                continue;
264
+            }
265
+
266
+            $bindings[$colName] = &$definition['value'];
267
+        }
268
+        return $bindings;
269
+    }
270
+
271
+    protected function insertDefaults()
272
+    {
273
+        // Insert default values for not-null fields
274
+        foreach ($this->tableDefinition as $colName => $colDef) {
275
+            if ($colDef['value'] === null
276
+                && ($colDef['properties'] ?? 0) & ColumnProperty::NOT_NULL
277
+                && isset($colDef['default'])) {
278
+                $this->tableDefinition[$colName]['value'] = $colDef['default'];
279
+            }
280
+        }		
281
+    }
282
+
283
+    /**
284
+     * {@inheritdoc}
285
+     */
286
+    public function create()
287
+    {
288
+        foreach ($this->createHooks as $colName => $fn) {
289
+            $fn();
290
+        }
291
+
292
+        $this->insertDefaults();
293
+
294
+        try {
295
+            (new Query($this->getPdo(), $this->getTableName()))
296
+                ->insert($this->getActiveRecordColumns())
297
+                ->execute();
298
+
299
+            $this->setId(intval($this->getPdo()->lastInsertId()));
300
+        } catch (\PDOException $e) {
301
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
302
+        }
303
+
304
+        return $this;
305
+    }
306
+
307
+    /**
308
+     * {@inheritdoc}
309
+     */
310
+    public function read($id)
311
+    {
312
+        $whereConditions = [
313
+            Query::Equal('id', $id)
314
+        ];
315
+        foreach ($this->readHooks as $colName => $fn) {
316
+            $cond = $fn();
317
+            if ($cond !== null) {
318
+                $whereConditions[] = $cond;
319
+            }
320
+        }
321
+
322
+        try {
323
+            $row = (new Query($this->getPdo(), $this->getTableName()))
324
+                ->select()
325
+                ->where(Query::AndArray($whereConditions))
326
+                ->execute()
327
+                ->fetch();
328 328
 			
329
-			if ($row === false) {
330
-				throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));	
331
-			}
332
-
333
-			$this->fill($row)->setId($id);
334
-		} catch (\PDOException $e) {
335
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
336
-		}
337
-
338
-		return $this;
339
-	}
340
-
341
-	/**
342
-	 * {@inheritdoc}
343
-	 */
344
-	public function update()
345
-	{
346
-		foreach ($this->updateHooks as $colName => $fn) {
347
-			$fn();
348
-		}
349
-
350
-		try {
351
-			(new Query($this->getPdo(), $this->getTableName()))
352
-				->update($this->getActiveRecordColumns())
353
-				->where(Query::Equal('id', $this->getId()))
354
-				->execute();
355
-		} catch (\PDOException $e) {
356
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
357
-		}
358
-
359
-		return $this;
360
-	}
361
-
362
-	/**
363
-	 * {@inheritdoc}
364
-	 */
365
-	public function delete()
366
-	{
367
-		foreach ($this->deleteHooks as $colName => $fn) {
368
-			$fn();
369
-		}
370
-
371
-		try {
372
-			(new Query($this->getPdo(), $this->getTableName()))
373
-				->delete()
374
-				->where(Query::Equal('id', $this->getId()))
375
-				->execute();
376
-
377
-			$this->setId(null);
378
-		} catch (\PDOException $e) {
379
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
380
-		}
381
-
382
-		return $this;
383
-	}
384
-
385
-	/**
386
-	 * {@inheritdoc}
387
-	 */
388
-	public function sync()
389
-	{
390
-		if (!$this->exists()) {
391
-			return $this->create();
392
-		}
393
-
394
-		return $this->update();
395
-	}
396
-
397
-	/**
398
-	 * {@inheritdoc}
399
-	 */
400
-	public function exists()
401
-	{
402
-		return $this->getId() !== null;
403
-	}
404
-
405
-	/**
406
-	 * {@inheritdoc}
407
-	 */
408
-	public function fill(array $attributes)
409
-	{
410
-		$columns = $this->getActiveRecordColumns();
411
-		$columns['id'] = &$this->id;
412
-
413
-		foreach ($attributes as $key => $value) {
414
-			if (array_key_exists($key, $columns)) {
415
-				$columns[$key] = $value;
416
-			}
417
-		}
418
-
419
-		return $this;
420
-	}
421
-
422
-	/**
423
-	 * {@inheritdoc}
424
-	 */
425
-	public function search(array $ignoredTraits = [])
426
-	{
427
-		$clauses = [];
428
-		foreach ($this->searchHooks as $column => $fn) {
429
-			if (!in_array($column, $ignoredTraits)) {
430
-				$clauses[] = $fn();
431
-			}
432
-		}
433
-
434
-		return new ActiveRecordQuery($this, $clauses);
435
-	}
436
-
437
-	/**
438
-	 * Returns the PDO.
439
-	 *
440
-	 * @return \PDO the PDO.
441
-	 */
442
-	public function getPdo()
443
-	{
444
-		return $this->pdo;
445
-	}
446
-
447
-	/**
448
-	 * Set the PDO.
449
-	 *
450
-	 * @param \PDO $pdo
451
-	 * @return $this
452
-	 */
453
-	protected function setPdo($pdo)
454
-	{
455
-		$this->pdo = $pdo;
456
-
457
-		return $this;
458
-	}
459
-
460
-	/**
461
-	 * Returns the ID.
462
-	 *
463
-	 * @return null|int The ID.
464
-	 */
465
-	public function getId()
466
-	{
467
-		return $this->id;
468
-	}
469
-
470
-	/**
471
-	 * Set the ID.
472
-	 *
473
-	 * @param int $id
474
-	 * @return $this
475
-	 */
476
-	protected function setId($id)
477
-	{
478
-		$this->id = $id;
479
-
480
-		return $this;
481
-	}
482
-
483
-	public function getFinalTableDefinition()
484
-	{
485
-		return $this->tableDefinition;
486
-	}
487
-
488
-	public function newInstance()
489
-	{
490
-		return new static($this->pdo);
491
-	}
492
-
493
-	/**
494
-	 * Returns the active record table.
495
-	 *
496
-	 * @return string the active record table name.
497
-	 */
498
-	abstract public function getTableName(): string;
499
-
500
-	/**
501
-	 * Returns the active record columns.
502
-	 *
503
-	 * @return array the active record columns.
504
-	 */
505
-	abstract protected function getTableDefinition(): Array;
329
+            if ($row === false) {
330
+                throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));	
331
+            }
332
+
333
+            $this->fill($row)->setId($id);
334
+        } catch (\PDOException $e) {
335
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
336
+        }
337
+
338
+        return $this;
339
+    }
340
+
341
+    /**
342
+     * {@inheritdoc}
343
+     */
344
+    public function update()
345
+    {
346
+        foreach ($this->updateHooks as $colName => $fn) {
347
+            $fn();
348
+        }
349
+
350
+        try {
351
+            (new Query($this->getPdo(), $this->getTableName()))
352
+                ->update($this->getActiveRecordColumns())
353
+                ->where(Query::Equal('id', $this->getId()))
354
+                ->execute();
355
+        } catch (\PDOException $e) {
356
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
357
+        }
358
+
359
+        return $this;
360
+    }
361
+
362
+    /**
363
+     * {@inheritdoc}
364
+     */
365
+    public function delete()
366
+    {
367
+        foreach ($this->deleteHooks as $colName => $fn) {
368
+            $fn();
369
+        }
370
+
371
+        try {
372
+            (new Query($this->getPdo(), $this->getTableName()))
373
+                ->delete()
374
+                ->where(Query::Equal('id', $this->getId()))
375
+                ->execute();
376
+
377
+            $this->setId(null);
378
+        } catch (\PDOException $e) {
379
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
380
+        }
381
+
382
+        return $this;
383
+    }
384
+
385
+    /**
386
+     * {@inheritdoc}
387
+     */
388
+    public function sync()
389
+    {
390
+        if (!$this->exists()) {
391
+            return $this->create();
392
+        }
393
+
394
+        return $this->update();
395
+    }
396
+
397
+    /**
398
+     * {@inheritdoc}
399
+     */
400
+    public function exists()
401
+    {
402
+        return $this->getId() !== null;
403
+    }
404
+
405
+    /**
406
+     * {@inheritdoc}
407
+     */
408
+    public function fill(array $attributes)
409
+    {
410
+        $columns = $this->getActiveRecordColumns();
411
+        $columns['id'] = &$this->id;
412
+
413
+        foreach ($attributes as $key => $value) {
414
+            if (array_key_exists($key, $columns)) {
415
+                $columns[$key] = $value;
416
+            }
417
+        }
418
+
419
+        return $this;
420
+    }
421
+
422
+    /**
423
+     * {@inheritdoc}
424
+     */
425
+    public function search(array $ignoredTraits = [])
426
+    {
427
+        $clauses = [];
428
+        foreach ($this->searchHooks as $column => $fn) {
429
+            if (!in_array($column, $ignoredTraits)) {
430
+                $clauses[] = $fn();
431
+            }
432
+        }
433
+
434
+        return new ActiveRecordQuery($this, $clauses);
435
+    }
436
+
437
+    /**
438
+     * Returns the PDO.
439
+     *
440
+     * @return \PDO the PDO.
441
+     */
442
+    public function getPdo()
443
+    {
444
+        return $this->pdo;
445
+    }
446
+
447
+    /**
448
+     * Set the PDO.
449
+     *
450
+     * @param \PDO $pdo
451
+     * @return $this
452
+     */
453
+    protected function setPdo($pdo)
454
+    {
455
+        $this->pdo = $pdo;
456
+
457
+        return $this;
458
+    }
459
+
460
+    /**
461
+     * Returns the ID.
462
+     *
463
+     * @return null|int The ID.
464
+     */
465
+    public function getId()
466
+    {
467
+        return $this->id;
468
+    }
469
+
470
+    /**
471
+     * Set the ID.
472
+     *
473
+     * @param int $id
474
+     * @return $this
475
+     */
476
+    protected function setId($id)
477
+    {
478
+        $this->id = $id;
479
+
480
+        return $this;
481
+    }
482
+
483
+    public function getFinalTableDefinition()
484
+    {
485
+        return $this->tableDefinition;
486
+    }
487
+
488
+    public function newInstance()
489
+    {
490
+        return new static($this->pdo);
491
+    }
492
+
493
+    /**
494
+     * Returns the active record table.
495
+     *
496
+     * @return string the active record table name.
497
+     */
498
+    abstract public function getTableName(): string;
499
+
500
+    /**
501
+     * Returns the active record columns.
502
+     *
503
+     * @return array the active record columns.
504
+     */
505
+    abstract protected function getTableDefinition(): Array;
506 506
 }
Please login to merge, or discard this patch.
src/Traits/Password.php 1 patch
Indentation   +179 added lines, -179 removed lines patch added patch discarded remove patch
@@ -14,186 +14,186 @@
 block discarded – undo
14 14
 
15 15
 trait Password
16 16
 {
17
-	/** @var string The password hash. */
18
-	protected $password;
19
-
20
-	/** @var string|null The password reset token. */
21
-	protected $passwordResetToken;
22
-
23
-	/** @var string|null The password expiry date */
24
-	protected $passwordExpiryDate;
25
-
26
-	/**
27
-	 * this method is required to be called in the constructor for each class that uses this trait. 
28
-	 * It adds the fields necessary for the passwords struct to the table definition
29
-	 */
30
-	protected function initPassword()
31
-	{
32
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_PASSWORD, [
33
-			'value' => &$this->password,
34
-			'validate' => [$this, 'validatePassword'],
35
-			'type' => 'VARCHAR',
36
-			'length' => 1024,
37
-			'properties' => null
38
-		]);
39
-
40
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN, [
41
-			'value' => &$this->passwordResetToken,
42
-			'validate' => null,
43
-			'default' => 0,
44
-			'type' => 'VARCHAR',
45
-			'length' => 1024
46
-		]);
47
-
48
-		$this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN_EXPIRY, [
49
-			'value' => &$this->passwordExpiryDate,
50
-			'validate' => null,
51
-			'type' => 'DATETIME',
52
-		]);
53
-	}
54
-
55
-
56
-	/**
57
-	 * Returns whether the users password has been set
58
-	 * @return boolean true if the user has a password
59
-	 */
60
-	public function hasPasswordBeenSet()
61
-	{
62
-		return $this->password !== null;
63
-	}
64
-
65
-	/**
66
-	 * Returns true if the credentials are correct.
67
-	 *
68
-	 * @param string $password
69
-	 * @return boolean true if the credentials are correct
70
-	 */
71
-	public function isPassword($password)
72
-	{ 
73
-		if (!$this->hasPasswordBeenSet())
74
-		{
75
-			throw new ActiveRecordTraitException("Password field has not been set");
76
-		}
77
-
78
-		if (!password_verify($password, $this->password)) {
79
-			return false;
80
-		}
81
-
82
-		if (password_needs_rehash($this->password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH])) {
83
-			$this->setPassword($password)->sync();
84
-		}
85
-
86
-		return true;
87
-	}
88
-
89
-	public function validatePassword($password) {
90
-		if (strlen($password) < TRAIT_PASSWORD_MIN_LENGTH) {
91
-			$message = sprintf('\'Password\' must be atleast %s characters long. %s characters provided.', TRAIT_PASSWORD_MIN_LENGTH, strlen($password));
92
-			return [false, $message];
93
-		}
94
-		return [true, ''];
95
-	}
96
-
97
-	/**
98
-	 * Set the password.
99
-	 *
100
-	 * @param string $password
101
-	 * @return $this
102
-	 * @throws \Exception
103
-	 */
104
-	public function setPassword($password)
105
-	{
106
-		[$status, $error] = $this->validatePassword($password);
107
-		if (!$status) {
108
-			throw new ActiveRecordTraitException($error);
109
-		}
110
-
111
-		$passwordHash = \password_hash($password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH]);
112
-
113
-		if ($passwordHash === false) {
114
-			throw new ActiveRecordTraitException('\'Password\' hash failed.');
115
-		}
116
-
117
-		$this->password = $passwordHash;
118
-
119
-		return $this;
120
-	}
121
-
122
-	/**
123
-	 * @return string The Hash of the password
124
-	 */
125
-	public function getPasswordHash()
126
-	{
127
-		return $this->password;
128
-	}
129
-
130
-	/**
131
-	 * Returns the currently set password token for the entity, or null if not set
132
-	 * @return string|null The password reset token
133
-	 */
134
-	public function getPasswordResetToken()
135
-	{
136
-		return $this->passwordResetToken;
137
-	}
138
-
139
-	/**
140
-	 * Generates a new password reset token for the user
141
-	 */
142
-	public function generatePasswordResetToken()
143
-	{
144
-		$this->passwordResetToken = md5(uniqid(mt_rand(), true));
145
-
146
-		$validityDuration = new \DateInterval('PT24H');
147
-
148
-		$this->passwordExpiryDate = (new \DateTime('now'))->add($validityDuration)->format('Y-m-d H:i:s');
149
-		return $this;
150
-	}
151
-
152
-	/**
153
-	 * Clears the current password reset token
154
-	 */
155
-	public function clearPasswordResetToken()
156
-	{
157
-		$this->passwordResetToken = null;
158
-		$this->passwordExpiryDate = null;
159
-		return $this;
160
-	}
161
-
162
-	public function validatePasswordResetToken(string $token)
163
-	{
164
-		return $this->passwordResetToken !== null
165
-			&& $token === $this->passwordResetToken
166
-			&& (new \DateTime('now')) < (new \DateTime($this->passwordExpiryDate));
167
-	}
17
+    /** @var string The password hash. */
18
+    protected $password;
19
+
20
+    /** @var string|null The password reset token. */
21
+    protected $passwordResetToken;
22
+
23
+    /** @var string|null The password expiry date */
24
+    protected $passwordExpiryDate;
25
+
26
+    /**
27
+     * this method is required to be called in the constructor for each class that uses this trait. 
28
+     * It adds the fields necessary for the passwords struct to the table definition
29
+     */
30
+    protected function initPassword()
31
+    {
32
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_PASSWORD, [
33
+            'value' => &$this->password,
34
+            'validate' => [$this, 'validatePassword'],
35
+            'type' => 'VARCHAR',
36
+            'length' => 1024,
37
+            'properties' => null
38
+        ]);
39
+
40
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN, [
41
+            'value' => &$this->passwordResetToken,
42
+            'validate' => null,
43
+            'default' => 0,
44
+            'type' => 'VARCHAR',
45
+            'length' => 1024
46
+        ]);
47
+
48
+        $this->extendTableDefinition(TRAIT_PASSWORD_FIELD_RESET_TOKEN_EXPIRY, [
49
+            'value' => &$this->passwordExpiryDate,
50
+            'validate' => null,
51
+            'type' => 'DATETIME',
52
+        ]);
53
+    }
54
+
55
+
56
+    /**
57
+     * Returns whether the users password has been set
58
+     * @return boolean true if the user has a password
59
+     */
60
+    public function hasPasswordBeenSet()
61
+    {
62
+        return $this->password !== null;
63
+    }
64
+
65
+    /**
66
+     * Returns true if the credentials are correct.
67
+     *
68
+     * @param string $password
69
+     * @return boolean true if the credentials are correct
70
+     */
71
+    public function isPassword($password)
72
+    { 
73
+        if (!$this->hasPasswordBeenSet())
74
+        {
75
+            throw new ActiveRecordTraitException("Password field has not been set");
76
+        }
77
+
78
+        if (!password_verify($password, $this->password)) {
79
+            return false;
80
+        }
81
+
82
+        if (password_needs_rehash($this->password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH])) {
83
+            $this->setPassword($password)->sync();
84
+        }
85
+
86
+        return true;
87
+    }
88
+
89
+    public function validatePassword($password) {
90
+        if (strlen($password) < TRAIT_PASSWORD_MIN_LENGTH) {
91
+            $message = sprintf('\'Password\' must be atleast %s characters long. %s characters provided.', TRAIT_PASSWORD_MIN_LENGTH, strlen($password));
92
+            return [false, $message];
93
+        }
94
+        return [true, ''];
95
+    }
96
+
97
+    /**
98
+     * Set the password.
99
+     *
100
+     * @param string $password
101
+     * @return $this
102
+     * @throws \Exception
103
+     */
104
+    public function setPassword($password)
105
+    {
106
+        [$status, $error] = $this->validatePassword($password);
107
+        if (!$status) {
108
+            throw new ActiveRecordTraitException($error);
109
+        }
110
+
111
+        $passwordHash = \password_hash($password, TRAIT_PASSWORD_ENCRYPTION, ['cost' => TRAIT_PASSWORD_STRENTH]);
112
+
113
+        if ($passwordHash === false) {
114
+            throw new ActiveRecordTraitException('\'Password\' hash failed.');
115
+        }
116
+
117
+        $this->password = $passwordHash;
118
+
119
+        return $this;
120
+    }
121
+
122
+    /**
123
+     * @return string The Hash of the password
124
+     */
125
+    public function getPasswordHash()
126
+    {
127
+        return $this->password;
128
+    }
129
+
130
+    /**
131
+     * Returns the currently set password token for the entity, or null if not set
132
+     * @return string|null The password reset token
133
+     */
134
+    public function getPasswordResetToken()
135
+    {
136
+        return $this->passwordResetToken;
137
+    }
138
+
139
+    /**
140
+     * Generates a new password reset token for the user
141
+     */
142
+    public function generatePasswordResetToken()
143
+    {
144
+        $this->passwordResetToken = md5(uniqid(mt_rand(), true));
145
+
146
+        $validityDuration = new \DateInterval('PT24H');
147
+
148
+        $this->passwordExpiryDate = (new \DateTime('now'))->add($validityDuration)->format('Y-m-d H:i:s');
149
+        return $this;
150
+    }
151
+
152
+    /**
153
+     * Clears the current password reset token
154
+     */
155
+    public function clearPasswordResetToken()
156
+    {
157
+        $this->passwordResetToken = null;
158
+        $this->passwordExpiryDate = null;
159
+        return $this;
160
+    }
161
+
162
+    public function validatePasswordResetToken(string $token)
163
+    {
164
+        return $this->passwordResetToken !== null
165
+            && $token === $this->passwordResetToken
166
+            && (new \DateTime('now')) < (new \DateTime($this->passwordExpiryDate));
167
+    }
168 168
 	
169
-	/**
170
-	 * @return void
171
-	 */
172
-	abstract protected function extendTableDefinition($columnName, $definition);
169
+    /**
170
+     * @return void
171
+     */
172
+    abstract protected function extendTableDefinition($columnName, $definition);
173 173
 	
174
-	/**
175
-	 * @return void
176
-	 */
177
-	abstract protected function registerSearchHook($columnName, $fn);
178
-
179
-	/**
180
-	 * @return void
181
-	 */
182
-	abstract protected function registerDeleteHook($columnName, $fn);
183
-
184
-	/**
185
-	 * @return void
186
-	 */
187
-	abstract protected function registerUpdateHook($columnName, $fn);
188
-
189
-	/**
190
-	 * @return void
191
-	 */
192
-	abstract protected function registerReadHook($columnName, $fn);
193
-
194
-	/**
195
-	 * @return void
196
-	 */
197
-	abstract protected function registerCreateHook($columnName, $fn);
174
+    /**
175
+     * @return void
176
+     */
177
+    abstract protected function registerSearchHook($columnName, $fn);
178
+
179
+    /**
180
+     * @return void
181
+     */
182
+    abstract protected function registerDeleteHook($columnName, $fn);
183
+
184
+    /**
185
+     * @return void
186
+     */
187
+    abstract protected function registerUpdateHook($columnName, $fn);
188
+
189
+    /**
190
+     * @return void
191
+     */
192
+    abstract protected function registerReadHook($columnName, $fn);
193
+
194
+    /**
195
+     * @return void
196
+     */
197
+    abstract protected function registerCreateHook($columnName, $fn);
198 198
 
199 199
 }
200 200
\ No newline at end of file
Please login to merge, or discard this patch.
src/Traits/AutoApi.php 1 patch
Indentation   +375 added lines, -375 removed lines patch added patch discarded remove patch
@@ -9,395 +9,395 @@
 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
-	public function toArray($fieldWhitelist)
90
-	{
91
-		$output = [];
92
-		foreach ($this->tableDefinition as $colName => $definition) {
93
-			if (in_array($colName, $fieldWhitelist)) {
94
-				$output[$colName] = $definition['value'];
95
-			}
96
-		}
97
-
98
-		return $output;
99
-	}
100
-
101
-	/**
102
-	 * @param string|int $id the id of the current entity
103
-	 * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
104
-	 * 
105
-	 * @param Array An associative array containing the data for this record, 
106
-	 * 				where the keys are entries in $fieldWhitelist
107
-	 */
108
-	public function apiRead($id, Array $fieldWhitelist): Array
109
-	{
110
-		// @TODO: Should apiRead throw exception or return null on fail?
111
-		$this->read($id);
112
-		return $this->toArray($fieldWhitelist);
113
-	}
114
-
115
-	/* =============================================================
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
+    public function toArray($fieldWhitelist)
90
+    {
91
+        $output = [];
92
+        foreach ($this->tableDefinition as $colName => $definition) {
93
+            if (in_array($colName, $fieldWhitelist)) {
94
+                $output[$colName] = $definition['value'];
95
+            }
96
+        }
97
+
98
+        return $output;
99
+    }
100
+
101
+    /**
102
+     * @param string|int $id the id of the current entity
103
+     * @param Array $fieldWhitelist an array of fields that are allowed to appear in the output
104
+     * 
105
+     * @param Array An associative array containing the data for this record, 
106
+     * 				where the keys are entries in $fieldWhitelist
107
+     */
108
+    public function apiRead($id, Array $fieldWhitelist): Array
109
+    {
110
+        // @TODO: Should apiRead throw exception or return null on fail?
111
+        $this->read($id);
112
+        return $this->toArray($fieldWhitelist);
113
+    }
114
+
115
+    /* =============================================================
116 116
 	 * ===================== Constraint validation =================
117 117
 	 * ============================================================= */
118 118
 
119
-	/**
120
-	 * Copy all table variables between two instances
121
-	 */
122
-	public function syncInstanceFrom($from)
123
-	{
124
-		foreach ($this->tableDefinition as $colName => $definition) {
125
-			$this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
126
-		}
127
-	}
128
-
129
-	private function filterInputColumns($input, $whitelist)
130
-	{
131
-		$filteredInput = $input;
132
-		foreach ($input as $colName => $value) {
133
-			if (!in_array($colName, $whitelist)) {
134
-				unset($filteredInput[$colName]);
135
-			}
136
-		}
137
-		return $filteredInput;
138
-	}
139
-
140
-	private function validateExcessKeys($input)
141
-	{
142
-		$errors = [];
143
-		foreach ($input as $colName => $value) {
144
-			if (!array_key_exists($colName, $this->tableDefinition)) {
145
-				$errors[$colName] = "Unknown input field";
146
-				continue;
147
-			}
148
-		}
149
-		return $errors;
150
-	}
151
-
152
-	private function validateImmutableColumns($input)
153
-	{
154
-		$errors = [];
155
-		foreach ($this->tableDefinition as $colName => $definition) {
156
-			$property = $definition['properties'] ?? null;
157
-			if (array_key_exists($colName, $input)
158
-				&& $property & ColumnProperty::IMMUTABLE) {
159
-				$errors[$colName] = "Field cannot be changed";
160
-			}
161
-		}
162
-		return $errors;
163
-	}
164
-
165
-	/**
166
-	 * Checks whether input values are correct:
167
-	 * 1. Checks whether a value passes the validation function for that column
168
-	 * 2. Checks whether a value supplied to a relationship column is a valid value
169
-	 */
170
-	private function validateInputValues($input)
171
-	{
172
-		$errors = [];
173
-		foreach ($this->tableDefinition as $colName => $definition) {
174
-			// Validation check 1: If validate function is present
175
-			if (array_key_exists($colName, $input) 
176
-				&& is_callable($definition['validate'] ?? null)) {
177
-				$inputValue = $input[$colName];
178
-
179
-				// If validation function fails
180
-				[$status, $message] = $definition['validate']($inputValue);
181
-				if (!$status) {
182
-					$errors[$colName] = $message;
183
-				}	
184
-			}
185
-
186
-			// Validation check 2: If relation column, check whether entity exists
187
-			$properties = $definition['properties'] ?? null;
188
-			if (isset($definition['relation'])
189
-				&& ($properties & ColumnProperty::NOT_NULL)) {
190
-				$instance = clone $definition['relation'];
191
-				try {
192
-					$instance->read($input[$colName] ?? $definition['value'] ?? null);
193
-				} catch (ActiveRecordException $e) {
194
-					$errors[$colName] = "Entity for this value doesn't exist";
195
-				}
196
-			}
197
-		}
198
-		return $errors;
199
-	}
200
-
201
-	/**
202
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
203
-	 * Determines whether there are required columns for which no data is provided
204
-	 */
205
-	private function validateMissingKeys($input)
206
-	{
207
-		$errors = [];
208
-
209
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
210
-			$default = $colDefinition['default'] ?? null;
211
-			$properties = $colDefinition['properties'] ?? null;
212
-			$value = $colDefinition['value'];
213
-
214
-			// If nullable and default not set => null
215
-			// If nullable and default null => default (null)
216
-			// If nullable and default set => default (value)
217
-
218
-			// if not nullable and default not set => error
219
-			// if not nullable and default null => error
220
-			// if not nullable and default st => default (value)
221
-			// => if not nullable and default null and value not set (or null) => error message in this method
222
-			if ($properties & ColumnProperty::NOT_NULL
223
-				&& $default === null
224
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
225
-				&& (!array_key_exists($colName, $input) 
226
-					|| $input[$colName] === null 
227
-					|| (is_string($input[$colName]) && $input[$colName] === '') )
228
-				&& ($value === null
229
-					|| (is_string($value) && $value === ''))) {
230
-				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
231
-			} 
232
-		}
233
-
234
-		return $errors;
235
-	}
236
-
237
-	/**
238
-	 * Copies the values for entries in the input with matching variable names in the record definition
239
-	 * @param Array $input The input data to be loaded into $this record
240
-	 */
241
-	private function loadData($input)
242
-	{
243
-		foreach ($this->tableDefinition as $colName => $definition) {
244
-			if (array_key_exists($colName, $input)) {
245
-				$definition['value'] = $input[$colName];
246
-			}
247
-		}
248
-	}
249
-
250
-	/**
251
-	 * @param Array $input Associative array of input values
252
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
253
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
254
-	 * 					of the modified data.
255
-	 */
256
-	public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
257
-	{
258
-		// Clone $this to new instance (for restoring if validation goes wrong)
259
-		$transaction = $this->newInstance();
260
-		$errors = [];
261
-
262
-		// Filter out all non-whitelisted input values
263
-		$input = $this->filterInputColumns($input, $createWhitelist);
264
-
265
-		// Validate excess keys
266
-		$errors += $transaction->validateExcessKeys($input);
267
-
268
-		// Validate input values (using validation function)
269
-		$errors += $transaction->validateInputValues($input);
270
-
271
-		// "Copy" data into transaction
272
-		$transaction->loadData($input);
273
-
274
-		// Run create hooks
275
-		foreach ($transaction->createHooks as $colName => $fn) {
276
-			$fn();
277
-		}
278
-
279
-		// Validate missing keys
280
-		$errors += $transaction->validateMissingKeys($input);
281
-
282
-		// If no errors, commit the pending data
283
-		if (empty($errors)) {
284
-			$this->syncInstanceFrom($transaction);
285
-
286
-			// Insert default values for not-null fields
287
-			$this->insertDefaults();
288
-
289
-			try {
290
-				(new Query($this->getPdo(), $this->getTableName()))
291
-					->insert($this->getActiveRecordColumns())
292
-					->execute();
293
-
294
-				$this->setId(intval($this->getPdo()->lastInsertId()));
295
-			} catch (\PDOException $e) {
296
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
297
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
298
-			}
299
-
300
-			return [null, $this->toArray($readWhitelist)];
301
-		} else {
302
-			return [$errors, null];
303
-		}
304
-	}
305
-
306
-	/**
307
-	 * @param Array $input Associative array of input values
308
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
309
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
310
-	 * 					of the modified data.
311
-	 */
312
-	public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
313
-	{
314
-		$transaction = $this->newInstance();
315
-		$transaction->syncInstanceFrom($this);
316
-		$errors = [];
317
-
318
-		// Filter out all non-whitelisted input values
319
-		$input = $this->filterInputColumns($input, $updateWhitelist);
320
-
321
-		// Check for excess keys
322
-		$errors += $transaction->validateExcessKeys($input);
323
-
324
-		// Check for immutable keys
325
-		$errors += $transaction->validateImmutableColumns($input);
326
-
327
-		// Validate input values (using validation function)
328
-		$errors += $transaction->validateInputValues($input);
329
-
330
-		// "Copy" data into transaction
331
-		$transaction->loadData($input);
332
-
333
-		// Run create hooks
334
-		foreach ($transaction->updateHooks as $colName => $fn) {
335
-			$fn();
336
-		}
337
-
338
-		// Validate missing keys
339
-		$errors += $transaction->validateMissingKeys($input);
340
-
341
-		// Update database
342
-		if (empty($errors)) {
343
-			$this->syncInstanceFrom($transaction);
344
-
345
-			try {
346
-				(new Query($this->getPdo(), $this->getTableName()))
347
-					->update($this->getActiveRecordColumns())
348
-					->where(Query::Equal('id', $this->getId()))
349
-					->execute();
350
-			} catch (\PDOException $e) {
351
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
352
-			}
353
-
354
-			return [null, $this->toArray($readWhitelist)];
355
-		} else {
356
-			return [$errors, null];
357
-		}
358
-	}
359
-
360
-	/**
361
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
362
-	 *
363
-	 * @param mixed $id
364
-	 * @return $this
365
-	 * @throws ActiveRecordException on failure.
366
-	 */
367
-	abstract public function read($id);
368
-
369
-	/**
370
-	 * Returns the PDO.
371
-	 *
372
-	 * @return \PDO the PDO.
373
-	 */
374
-	abstract public function getPdo();
375
-
376
-	/**
377
-	 * Set the ID.
378
-	 *
379
-	 * @param int $id
380
-	 * @return $this
381
-	 */
382
-	abstract protected function setId($id);
383
-
384
-	/**
385
-	 * Returns the ID.
386
-	 *
387
-	 * @return null|int The ID.
388
-	 */
389
-	abstract protected function getId();
390
-
391
-	/**
392
-	 * Returns the active record table.
393
-	 *
394
-	 * @return string the active record table name.
395
-	 */
396
-	abstract public function getTableName();
397
-
398
-	/**
399
-	 * Returns the name -> variable mapping for the table definition.
400
-	 * @return Array The mapping
401
-	 */
402
-	abstract protected function getActiveRecordColumns();
119
+    /**
120
+     * Copy all table variables between two instances
121
+     */
122
+    public function syncInstanceFrom($from)
123
+    {
124
+        foreach ($this->tableDefinition as $colName => $definition) {
125
+            $this->tableDefinition[$colName]['value'] = $from->tableDefinition[$colName]['value'];
126
+        }
127
+    }
128
+
129
+    private function filterInputColumns($input, $whitelist)
130
+    {
131
+        $filteredInput = $input;
132
+        foreach ($input as $colName => $value) {
133
+            if (!in_array($colName, $whitelist)) {
134
+                unset($filteredInput[$colName]);
135
+            }
136
+        }
137
+        return $filteredInput;
138
+    }
139
+
140
+    private function validateExcessKeys($input)
141
+    {
142
+        $errors = [];
143
+        foreach ($input as $colName => $value) {
144
+            if (!array_key_exists($colName, $this->tableDefinition)) {
145
+                $errors[$colName] = "Unknown input field";
146
+                continue;
147
+            }
148
+        }
149
+        return $errors;
150
+    }
151
+
152
+    private function validateImmutableColumns($input)
153
+    {
154
+        $errors = [];
155
+        foreach ($this->tableDefinition as $colName => $definition) {
156
+            $property = $definition['properties'] ?? null;
157
+            if (array_key_exists($colName, $input)
158
+                && $property & ColumnProperty::IMMUTABLE) {
159
+                $errors[$colName] = "Field cannot be changed";
160
+            }
161
+        }
162
+        return $errors;
163
+    }
164
+
165
+    /**
166
+     * Checks whether input values are correct:
167
+     * 1. Checks whether a value passes the validation function for that column
168
+     * 2. Checks whether a value supplied to a relationship column is a valid value
169
+     */
170
+    private function validateInputValues($input)
171
+    {
172
+        $errors = [];
173
+        foreach ($this->tableDefinition as $colName => $definition) {
174
+            // Validation check 1: If validate function is present
175
+            if (array_key_exists($colName, $input) 
176
+                && is_callable($definition['validate'] ?? null)) {
177
+                $inputValue = $input[$colName];
178
+
179
+                // If validation function fails
180
+                [$status, $message] = $definition['validate']($inputValue);
181
+                if (!$status) {
182
+                    $errors[$colName] = $message;
183
+                }	
184
+            }
185
+
186
+            // Validation check 2: If relation column, check whether entity exists
187
+            $properties = $definition['properties'] ?? null;
188
+            if (isset($definition['relation'])
189
+                && ($properties & ColumnProperty::NOT_NULL)) {
190
+                $instance = clone $definition['relation'];
191
+                try {
192
+                    $instance->read($input[$colName] ?? $definition['value'] ?? null);
193
+                } catch (ActiveRecordException $e) {
194
+                    $errors[$colName] = "Entity for this value doesn't exist";
195
+                }
196
+            }
197
+        }
198
+        return $errors;
199
+    }
200
+
201
+    /**
202
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
203
+     * Determines whether there are required columns for which no data is provided
204
+     */
205
+    private function validateMissingKeys($input)
206
+    {
207
+        $errors = [];
208
+
209
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
210
+            $default = $colDefinition['default'] ?? null;
211
+            $properties = $colDefinition['properties'] ?? null;
212
+            $value = $colDefinition['value'];
213
+
214
+            // If nullable and default not set => null
215
+            // If nullable and default null => default (null)
216
+            // If nullable and default set => default (value)
217
+
218
+            // if not nullable and default not set => error
219
+            // if not nullable and default null => error
220
+            // if not nullable and default st => default (value)
221
+            // => if not nullable and default null and value not set (or null) => error message in this method
222
+            if ($properties & ColumnProperty::NOT_NULL
223
+                && $default === null
224
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
225
+                && (!array_key_exists($colName, $input) 
226
+                    || $input[$colName] === null 
227
+                    || (is_string($input[$colName]) && $input[$colName] === '') )
228
+                && ($value === null
229
+                    || (is_string($value) && $value === ''))) {
230
+                $errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
231
+            } 
232
+        }
233
+
234
+        return $errors;
235
+    }
236
+
237
+    /**
238
+     * Copies the values for entries in the input with matching variable names in the record definition
239
+     * @param Array $input The input data to be loaded into $this record
240
+     */
241
+    private function loadData($input)
242
+    {
243
+        foreach ($this->tableDefinition as $colName => $definition) {
244
+            if (array_key_exists($colName, $input)) {
245
+                $definition['value'] = $input[$colName];
246
+            }
247
+        }
248
+    }
249
+
250
+    /**
251
+     * @param Array $input Associative array of input values
252
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
253
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
254
+     * 					of the modified data.
255
+     */
256
+    public function apiCreate(Array $input, Array $createWhitelist, Array $readWhitelist)
257
+    {
258
+        // Clone $this to new instance (for restoring if validation goes wrong)
259
+        $transaction = $this->newInstance();
260
+        $errors = [];
261
+
262
+        // Filter out all non-whitelisted input values
263
+        $input = $this->filterInputColumns($input, $createWhitelist);
264
+
265
+        // Validate excess keys
266
+        $errors += $transaction->validateExcessKeys($input);
267
+
268
+        // Validate input values (using validation function)
269
+        $errors += $transaction->validateInputValues($input);
270
+
271
+        // "Copy" data into transaction
272
+        $transaction->loadData($input);
273
+
274
+        // Run create hooks
275
+        foreach ($transaction->createHooks as $colName => $fn) {
276
+            $fn();
277
+        }
278
+
279
+        // Validate missing keys
280
+        $errors += $transaction->validateMissingKeys($input);
281
+
282
+        // If no errors, commit the pending data
283
+        if (empty($errors)) {
284
+            $this->syncInstanceFrom($transaction);
285
+
286
+            // Insert default values for not-null fields
287
+            $this->insertDefaults();
288
+
289
+            try {
290
+                (new Query($this->getPdo(), $this->getTableName()))
291
+                    ->insert($this->getActiveRecordColumns())
292
+                    ->execute();
293
+
294
+                $this->setId(intval($this->getPdo()->lastInsertId()));
295
+            } catch (\PDOException $e) {
296
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
297
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
298
+            }
299
+
300
+            return [null, $this->toArray($readWhitelist)];
301
+        } else {
302
+            return [$errors, null];
303
+        }
304
+    }
305
+
306
+    /**
307
+     * @param Array $input Associative array of input values
308
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
309
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
310
+     * 					of the modified data.
311
+     */
312
+    public function apiUpdate(Array $input, Array $updateWhitelist, Array $readWhitelist)
313
+    {
314
+        $transaction = $this->newInstance();
315
+        $transaction->syncInstanceFrom($this);
316
+        $errors = [];
317
+
318
+        // Filter out all non-whitelisted input values
319
+        $input = $this->filterInputColumns($input, $updateWhitelist);
320
+
321
+        // Check for excess keys
322
+        $errors += $transaction->validateExcessKeys($input);
323
+
324
+        // Check for immutable keys
325
+        $errors += $transaction->validateImmutableColumns($input);
326
+
327
+        // Validate input values (using validation function)
328
+        $errors += $transaction->validateInputValues($input);
329
+
330
+        // "Copy" data into transaction
331
+        $transaction->loadData($input);
332
+
333
+        // Run create hooks
334
+        foreach ($transaction->updateHooks as $colName => $fn) {
335
+            $fn();
336
+        }
337
+
338
+        // Validate missing keys
339
+        $errors += $transaction->validateMissingKeys($input);
340
+
341
+        // Update database
342
+        if (empty($errors)) {
343
+            $this->syncInstanceFrom($transaction);
344
+
345
+            try {
346
+                (new Query($this->getPdo(), $this->getTableName()))
347
+                    ->update($this->getActiveRecordColumns())
348
+                    ->where(Query::Equal('id', $this->getId()))
349
+                    ->execute();
350
+            } catch (\PDOException $e) {
351
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
352
+            }
353
+
354
+            return [null, $this->toArray($readWhitelist)];
355
+        } else {
356
+            return [$errors, null];
357
+        }
358
+    }
359
+
360
+    /**
361
+     * Returns this active record after reading the attributes from the entry with the given identifier.
362
+     *
363
+     * @param mixed $id
364
+     * @return $this
365
+     * @throws ActiveRecordException on failure.
366
+     */
367
+    abstract public function read($id);
368
+
369
+    /**
370
+     * Returns the PDO.
371
+     *
372
+     * @return \PDO the PDO.
373
+     */
374
+    abstract public function getPdo();
375
+
376
+    /**
377
+     * Set the ID.
378
+     *
379
+     * @param int $id
380
+     * @return $this
381
+     */
382
+    abstract protected function setId($id);
383
+
384
+    /**
385
+     * Returns the ID.
386
+     *
387
+     * @return null|int The ID.
388
+     */
389
+    abstract protected function getId();
390
+
391
+    /**
392
+     * Returns the active record table.
393
+     *
394
+     * @return string the active record table name.
395
+     */
396
+    abstract public function getTableName();
397
+
398
+    /**
399
+     * Returns the name -> variable mapping for the table definition.
400
+     * @return Array The mapping
401
+     */
402
+    abstract protected function getActiveRecordColumns();
403 403
 }
Please login to merge, or discard this patch.
src/Traits/Datefields.php 1 patch
Indentation   +104 added lines, -104 removed lines patch added patch discarded remove patch
@@ -10,111 +10,111 @@
 block discarded – undo
10 10
 
11 11
 trait Datefields
12 12
 {
13
-	/** @var string The timestamp representing the moment this record was created */
14
-	protected $created;
15
-
16
-	/** @var string The timestamp representing the moment this record was last updated */
17
-	protected $lastModified;
18
-
19
-	/**
20
-	 * this method is required to be called in the constructor for each class that uses this trait. 
21
-	 * It adds the datefields to the table definition and registers the callback hooks
22
-	 */
23
-	protected function initDatefields()
24
-	{
25
-		$this->extendTableDefinition(TRAIT_DATEFIELDS_CREATED, [
26
-			'value' => &$this->created,
27
-			'validate' => null,
28
-			'type' => 'DATETIME',
29
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
30
-		]);
31
-
32
-		$this->extendTableDefinition(TRAIT_DATEFIELDS_LAST_MODIFIED, [
33
-			'value' => &$this->lastModified,
34
-			'validate' => null,
35
-			'type' => 'DATETIME',
36
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
37
-		]);
13
+    /** @var string The timestamp representing the moment this record was created */
14
+    protected $created;
15
+
16
+    /** @var string The timestamp representing the moment this record was last updated */
17
+    protected $lastModified;
18
+
19
+    /**
20
+     * this method is required to be called in the constructor for each class that uses this trait. 
21
+     * It adds the datefields to the table definition and registers the callback hooks
22
+     */
23
+    protected function initDatefields()
24
+    {
25
+        $this->extendTableDefinition(TRAIT_DATEFIELDS_CREATED, [
26
+            'value' => &$this->created,
27
+            'validate' => null,
28
+            'type' => 'DATETIME',
29
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
30
+        ]);
31
+
32
+        $this->extendTableDefinition(TRAIT_DATEFIELDS_LAST_MODIFIED, [
33
+            'value' => &$this->lastModified,
34
+            'validate' => null,
35
+            'type' => 'DATETIME',
36
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE
37
+        ]);
38 38
 		
39
-		$this->registerCreateHook(TRAIT_DATEFIELDS_CREATED, 'DatefieldsLastModifiedCreateHook');
40
-		$this->registerCreateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsCreatedCreateHook');
41
-		$this->registerUpdateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsUpdateHook');
42
-
43
-		$this->created = null;
44
-		$this->lastModified = null;
45
-	}
46
-
47
-	/**
48
-	 * The hook that gets called to set the last modified timestamp whenever a new record is created
49
-	 */
50
-	protected function DatefieldsLastModifiedCreateHook()
51
-	{
52
-		$this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
53
-	}
54
-
55
-	/**
56
-	 * The hook that gets called to set the created timestamp whenever a new record is created
57
-	 */
58
-	protected function DatefieldsCreatedCreateHook()
59
-	{
60
-		$this->created = (new \DateTime('now'))->format('Y-m-d H:i:s');
61
-	}
62
-
63
-	/**
64
-	 * The hook that gets called to set the timestamp whenever a record gets updated
65
-	 */
66
-	protected function DatefieldsUpdateHook()
67
-	{
68
-		$this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
69
-	}
70
-
71
-	/**
72
-	 * Returns the timestamp of last update for this record
73
-	 * @return \DateTime
74
-	 */
75
-	public function getLastModifiedDate()
76
-	{
77
-		return new \DateTime($this->lastModified);
78
-	}
79
-
80
-	/**
81
-	 * Returns the timestamp of when this record was created
82
-	 * @return \DateTime
83
-	 */
84
-	public function getCreationDate()
85
-	{
86
-		return new \DateTime($this->created);
87
-	}
88
-
89
-	/**
90
-	 * @return void
91
-	 */
92
-	abstract protected function extendTableDefinition($columnName, $definition);
39
+        $this->registerCreateHook(TRAIT_DATEFIELDS_CREATED, 'DatefieldsLastModifiedCreateHook');
40
+        $this->registerCreateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsCreatedCreateHook');
41
+        $this->registerUpdateHook(TRAIT_DATEFIELDS_LAST_MODIFIED, 'DatefieldsUpdateHook');
42
+
43
+        $this->created = null;
44
+        $this->lastModified = null;
45
+    }
46
+
47
+    /**
48
+     * The hook that gets called to set the last modified timestamp whenever a new record is created
49
+     */
50
+    protected function DatefieldsLastModifiedCreateHook()
51
+    {
52
+        $this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
53
+    }
54
+
55
+    /**
56
+     * The hook that gets called to set the created timestamp whenever a new record is created
57
+     */
58
+    protected function DatefieldsCreatedCreateHook()
59
+    {
60
+        $this->created = (new \DateTime('now'))->format('Y-m-d H:i:s');
61
+    }
62
+
63
+    /**
64
+     * The hook that gets called to set the timestamp whenever a record gets updated
65
+     */
66
+    protected function DatefieldsUpdateHook()
67
+    {
68
+        $this->lastModified = (new \DateTime('now'))->format('Y-m-d H:i:s');
69
+    }
70
+
71
+    /**
72
+     * Returns the timestamp of last update for this record
73
+     * @return \DateTime
74
+     */
75
+    public function getLastModifiedDate()
76
+    {
77
+        return new \DateTime($this->lastModified);
78
+    }
79
+
80
+    /**
81
+     * Returns the timestamp of when this record was created
82
+     * @return \DateTime
83
+     */
84
+    public function getCreationDate()
85
+    {
86
+        return new \DateTime($this->created);
87
+    }
88
+
89
+    /**
90
+     * @return void
91
+     */
92
+    abstract protected function extendTableDefinition($columnName, $definition);
93 93
 	
94
-	/**
95
-	 * @return void
96
-	 */
97
-	abstract protected function registerSearchHook($columnName, $fn);
98
-
99
-	/**
100
-	 * @return void
101
-	 */
102
-	abstract protected function registerDeleteHook($columnName, $fn);
103
-
104
-	/**
105
-	 * @return void
106
-	 */
107
-	abstract protected function registerUpdateHook($columnName, $fn);
108
-
109
-	/**
110
-	 * @return void
111
-	 */
112
-	abstract protected function registerReadHook($columnName, $fn);
113
-
114
-	/**
115
-	 * @return void
116
-	 */
117
-	abstract protected function registerCreateHook($columnName, $fn);
94
+    /**
95
+     * @return void
96
+     */
97
+    abstract protected function registerSearchHook($columnName, $fn);
98
+
99
+    /**
100
+     * @return void
101
+     */
102
+    abstract protected function registerDeleteHook($columnName, $fn);
103
+
104
+    /**
105
+     * @return void
106
+     */
107
+    abstract protected function registerUpdateHook($columnName, $fn);
108
+
109
+    /**
110
+     * @return void
111
+     */
112
+    abstract protected function registerReadHook($columnName, $fn);
113
+
114
+    /**
115
+     * @return void
116
+     */
117
+    abstract protected function registerCreateHook($columnName, $fn);
118 118
 }
119 119
 
120
-	
121 120
\ No newline at end of file
121
+    
122 122
\ No newline at end of file
Please login to merge, or discard this patch.