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