Test Failed
Push — v2 ( a0d121...604284 )
by Berend
02:50
created
src/ActiveRecordInterface.php 1 patch
Indentation   +60 added lines, -60 removed lines patch added patch discarded remove patch
@@ -19,72 +19,72 @@
 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 this active record after creating an entry with the records attributes.
26
-	 *
27
-	 * @return $this
28
-	 * @throws ActiveRecordException on failure.
29
-	 */
30
-	public function create();
24
+    /**
25
+     * Returns this active record after creating an entry with the records attributes.
26
+     *
27
+     * @return $this
28
+     * @throws ActiveRecordException on failure.
29
+     */
30
+    public function create();
31 31
 
32
-	/**
33
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
34
-	 *
35
-	 * @param mixed $id
36
-	 * @return $this
37
-	 * @throws ActiveRecordException on failure.
38
-	 */
39
-	public function read($id);
32
+    /**
33
+     * Returns this active record after reading the attributes from the entry with the given identifier.
34
+     *
35
+     * @param mixed $id
36
+     * @return $this
37
+     * @throws ActiveRecordException on failure.
38
+     */
39
+    public function read($id);
40 40
 
41
-	/**
42
-	 * Returns this active record after updating the attributes to the corresponding entry.
43
-	 *
44
-	 * @return $this
45
-	 * @throws ActiveRecordException on failure.
46
-	 */
47
-	public function update();
41
+    /**
42
+     * Returns this active record after updating the attributes to the corresponding entry.
43
+     *
44
+     * @return $this
45
+     * @throws ActiveRecordException on failure.
46
+     */
47
+    public function update();
48 48
 
49
-	/**
50
-	 * Returns this record after deleting the corresponding entry.
51
-	 *
52
-	 * @return $this
53
-	 * @throws ActiveRecordException on failure.
54
-	 */
55
-	public function delete();
49
+    /**
50
+     * Returns this record after deleting the corresponding entry.
51
+     *
52
+     * @return $this
53
+     * @throws ActiveRecordException on failure.
54
+     */
55
+    public function delete();
56 56
 
57
-	/**
58
-	 * Returns this record after synchronizing it with the corresponding entry.
59
-	 * A new entry is created if this active record does not have a corresponding entry.
60
-	 *
61
-	 * @return $this
62
-	 * @throws ActiveRecordException on failure.
63
-	 */
64
-	public function sync();
57
+    /**
58
+     * Returns this record after synchronizing it with the corresponding entry.
59
+     * A new entry is created if this active record does not have a corresponding entry.
60
+     *
61
+     * @return $this
62
+     * @throws ActiveRecordException on failure.
63
+     */
64
+    public function sync();
65 65
 
66
-	/**
67
-	 * Returns true if this active record has a corresponding entry.
68
-	 *
69
-	 * @return bool true if this active record has a corresponding entry.
70
-	 */
71
-	public function exists();
66
+    /**
67
+     * Returns true if this active record has a corresponding entry.
68
+     *
69
+     * @return bool true if this active record has a corresponding entry.
70
+     */
71
+    public function exists();
72 72
 
73
-	/**
74
-	 * Returns this record after filling it with the given attributes.
75
-	 *
76
-	 * @param array $attributes = []
77
-	 * @return $this
78
-	 * @throws ActiveRecordException on failure.
79
-	 */
80
-	public function fill(array $attributes);
73
+    /**
74
+     * Returns this record after filling it with the given attributes.
75
+     *
76
+     * @param array $attributes = []
77
+     * @return $this
78
+     * @throws ActiveRecordException on failure.
79
+     */
80
+    public function fill(array $attributes);
81 81
 
82
-	/**
83
-	 * Returns the records with the given where, order by, limit and offset clauses.
84
-	 *
85
-	 * @param array $excludedTraits
86
-	 * @return ActiveRecordQuery the query representing the current search.
87
-	 * @throws ActiveRecordException on failure.
88
-	 */
89
-	public function search(Array $excludedTraits);
82
+    /**
83
+     * Returns the records with the given where, order by, limit and offset clauses.
84
+     *
85
+     * @param array $excludedTraits
86
+     * @return ActiveRecordQuery the query representing the current search.
87
+     * @throws ActiveRecordException on failure.
88
+     */
89
+    public function search(Array $excludedTraits);
90 90
 }
Please login to merge, or discard this patch.
src/Traits/ManyToManyRelation.php 1 patch
Indentation   +109 added lines, -109 removed lines patch added patch discarded remove patch
@@ -8,115 +8,115 @@
 block discarded – undo
8 8
 
9 9
 Trait ManyToManyRelation
10 10
 {
11
-	// These variables are relevant for internal bookkeeping (constraint generation etc)
12
-
13
-	/** @var string The name of the left column of the relation. */
14
-	private $_leftColumnName;
15
-
16
-	/** @var string The name of the right column of the relation. */
17
-	private $_rightColumnName;
18
-
19
-	/** @var string The name of the left table of the relation. */
20
-	private $_leftEntityTable;
21
-
22
-	/** @var string The name of the right table of the relation. */
23
-	private $_rightEntityTable;
24
-
25
-	/** @var \PDO The PDO object. */
26
-	protected $pdo;
27
-	/**
28
-	 * Initializes the the ManyToManyRelation trait on the included object
29
-	 * 
30
-	 * @param AbstractActiveRecord $leftEntity The left entity of the relation
31
-	 * @param int $leftVariable The reference to the variable where the id for the left entity will be stored
32
-	 * @param AbstractActiveRecord $rightEntity The left entity of the relation
33
-	 * @param int $leftVariable The reference to the variable where the id for the right entity will be stored
34
-	 * @return void
35
-	 */
36
-	protected function initManyToManyRelation(AbstractActiveRecord $leftEntity, &$leftVariable, AbstractActiveRecord $rightEntity, &$rightVariable)
37
-	{
38
-		$this->_leftEntityTable = $leftEntity->getTableName();
39
-		$this->_rightEntityTable = $rightEntity->getTableName();
40
-
41
-		if (get_class($leftEntity) === get_class($rightEntity)) {
42
-			$this->_leftColumnName = sprintf("id_%s_left", $leftEntity->getTableName());
43
-			$this->_rightColumnName = sprintf("id_%s_right", $rightEntity->getTableName());
44
-		} else {
45
-			$this->_leftColumnName = sprintf("id_%s", $leftEntity->getTableName());
46
-			$this->_rightColumnName = sprintf("id_%s", $rightEntity->getTableName());
47
-		}
48
-
49
-		$this->extendTableDefinition($this->_leftColumnName, [
50
-			'value' => &$leftVariable,
51
-			'validate' => null,
52
-			'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
53
-			'properties' => ColumnProperty::NOT_NULL
54
-		]);
55
-
56
-		$this->extendTableDefinition($this->_rightColumnName, [
57
-			'value' => &$rightVariable,
58
-			'validate' => null,
59
-			'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
60
-			'properties' => ColumnProperty::NOT_NULL
61
-		]);
62
-	}
63
-
64
-	/**
65
-	 * Build the constraints for the many-to-many relation table
66
-	 * @return void
67
-	 */
68
-	public function createTableConstraints()
69
-	{
70
-		$childTable = $this->getTableName();
71
-
72
-		$leftParentTable = $this->_leftEntityTable;
73
-		$rightParentTable = $this->_rightEntityTable;
74
-
75
-		$leftConstraint = $this->buildConstraint($leftParentTable, 'id', $childTable, $this->_leftColumnName);
76
-		$rightConstraint = $this->buildConstraint($rightParentTable, 'id', $childTable, $this->_rightColumnName);
77
-
78
-		$this->pdo->query($leftConstraint);
79
-		$this->pdo->query($rightConstraint);
80
-	}
81
-
82
-	/**
83
-	 * @return void
84
-	 */	
85
-	abstract protected function getTableName();
86
-
87
-	/**
88
-	 * @return void
89
-	 */
90
-	abstract protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn);
91
-
92
-	/**
93
-	 * @return void
94
-	 */
95
-	abstract protected function extendTableDefinition($columnName, $definition);
11
+    // These variables are relevant for internal bookkeeping (constraint generation etc)
12
+
13
+    /** @var string The name of the left column of the relation. */
14
+    private $_leftColumnName;
15
+
16
+    /** @var string The name of the right column of the relation. */
17
+    private $_rightColumnName;
18
+
19
+    /** @var string The name of the left table of the relation. */
20
+    private $_leftEntityTable;
21
+
22
+    /** @var string The name of the right table of the relation. */
23
+    private $_rightEntityTable;
24
+
25
+    /** @var \PDO The PDO object. */
26
+    protected $pdo;
27
+    /**
28
+     * Initializes the the ManyToManyRelation trait on the included object
29
+     * 
30
+     * @param AbstractActiveRecord $leftEntity The left entity of the relation
31
+     * @param int $leftVariable The reference to the variable where the id for the left entity will be stored
32
+     * @param AbstractActiveRecord $rightEntity The left entity of the relation
33
+     * @param int $leftVariable The reference to the variable where the id for the right entity will be stored
34
+     * @return void
35
+     */
36
+    protected function initManyToManyRelation(AbstractActiveRecord $leftEntity, &$leftVariable, AbstractActiveRecord $rightEntity, &$rightVariable)
37
+    {
38
+        $this->_leftEntityTable = $leftEntity->getTableName();
39
+        $this->_rightEntityTable = $rightEntity->getTableName();
40
+
41
+        if (get_class($leftEntity) === get_class($rightEntity)) {
42
+            $this->_leftColumnName = sprintf("id_%s_left", $leftEntity->getTableName());
43
+            $this->_rightColumnName = sprintf("id_%s_right", $rightEntity->getTableName());
44
+        } else {
45
+            $this->_leftColumnName = sprintf("id_%s", $leftEntity->getTableName());
46
+            $this->_rightColumnName = sprintf("id_%s", $rightEntity->getTableName());
47
+        }
48
+
49
+        $this->extendTableDefinition($this->_leftColumnName, [
50
+            'value' => &$leftVariable,
51
+            'validate' => null,
52
+            'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
53
+            'properties' => ColumnProperty::NOT_NULL
54
+        ]);
55
+
56
+        $this->extendTableDefinition($this->_rightColumnName, [
57
+            'value' => &$rightVariable,
58
+            'validate' => null,
59
+            'type' => AbstractActiveRecord::COLUMN_TYPE_ID,
60
+            'properties' => ColumnProperty::NOT_NULL
61
+        ]);
62
+    }
63
+
64
+    /**
65
+     * Build the constraints for the many-to-many relation table
66
+     * @return void
67
+     */
68
+    public function createTableConstraints()
69
+    {
70
+        $childTable = $this->getTableName();
71
+
72
+        $leftParentTable = $this->_leftEntityTable;
73
+        $rightParentTable = $this->_rightEntityTable;
74
+
75
+        $leftConstraint = $this->buildConstraint($leftParentTable, 'id', $childTable, $this->_leftColumnName);
76
+        $rightConstraint = $this->buildConstraint($rightParentTable, 'id', $childTable, $this->_rightColumnName);
77
+
78
+        $this->pdo->query($leftConstraint);
79
+        $this->pdo->query($rightConstraint);
80
+    }
81
+
82
+    /**
83
+     * @return void
84
+     */	
85
+    abstract protected function getTableName();
86
+
87
+    /**
88
+     * @return void
89
+     */
90
+    abstract protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn);
91
+
92
+    /**
93
+     * @return void
94
+     */
95
+    abstract protected function extendTableDefinition($columnName, $definition);
96 96
 	
97
-	/**
98
-	 * @return void
99
-	 */
100
-	abstract protected function registerSearchHook($columnName, $fn);
101
-
102
-	/**
103
-	 * @return void
104
-	 */
105
-	abstract protected function registerDeleteHook($columnName, $fn);
106
-
107
-	/**
108
-	 * @return void
109
-	 */
110
-	abstract protected function registerUpdateHook($columnName, $fn);
111
-
112
-	/**
113
-	 * @return void
114
-	 */
115
-	abstract protected function registerReadHook($columnName, $fn);
116
-
117
-	/**
118
-	 * @return void
119
-	 */
120
-	abstract protected function registerCreateHook($columnName, $fn);
97
+    /**
98
+     * @return void
99
+     */
100
+    abstract protected function registerSearchHook($columnName, $fn);
101
+
102
+    /**
103
+     * @return void
104
+     */
105
+    abstract protected function registerDeleteHook($columnName, $fn);
106
+
107
+    /**
108
+     * @return void
109
+     */
110
+    abstract protected function registerUpdateHook($columnName, $fn);
111
+
112
+    /**
113
+     * @return void
114
+     */
115
+    abstract protected function registerReadHook($columnName, $fn);
116
+
117
+    /**
118
+     * @return void
119
+     */
120
+    abstract protected function registerCreateHook($columnName, $fn);
121 121
 
122 122
 }
Please login to merge, or discard this patch.
src/Traits/AutoApi.php 1 patch
Indentation   +333 added lines, -333 removed lines patch added patch discarded remove patch
@@ -8,351 +8,351 @@
 block discarded – undo
8 8
 
9 9
 trait AutoApi
10 10
 {
11
-	/* =======================================================================
11
+    /* =======================================================================
12 12
 	 * ===================== Automatic API Support ===========================
13 13
 	 * ======================================================================= */
14 14
 
15
-	/** @var array A map of column name to functions that hook the insert function */
16
-	protected $registeredCreateHooks;
15
+    /** @var array A map of column name to functions that hook the insert function */
16
+    protected $registeredCreateHooks;
17 17
 
18
-	/** @var array A map of column name to functions that hook the read function */
19
-	protected $registeredReadHooks;
18
+    /** @var array A map of column name to functions that hook the read function */
19
+    protected $registeredReadHooks;
20 20
 
21
-	/** @var array A map of column name to functions that hook the update function */
22
-	protected $registeredUpdateHooks;
21
+    /** @var array A map of column name to functions that hook the update function */
22
+    protected $registeredUpdateHooks;
23 23
 
24
-	/** @var array A map of column name to functions that hook the update function */
25
-	protected $registeredDeleteHooks;	
24
+    /** @var array A map of column name to functions that hook the update function */
25
+    protected $registeredDeleteHooks;	
26 26
 
27
-	/** @var array A map of column name to functions that hook the search function */
28
-	protected $registeredSearchHooks;
27
+    /** @var array A map of column name to functions that hook the search function */
28
+    protected $registeredSearchHooks;
29 29
 
30
-	/** @var array A list of table column definitions */
31
-	protected $tableDefinition;
30
+    /** @var array A list of table column definitions */
31
+    protected $tableDefinition;
32 32
 
33
-	public function apiSearch(Array $queryparams, Array $fieldWhitelist)
34
-	{
35
-		// @TODO: Would it be better to not include the ignored_traits?
36
-		$ignoredTraits = $queryparams['ignored_traits'] ?? [];
37
-		$query = $this->search($ignoredTraits);
33
+    public function apiSearch(Array $queryparams, Array $fieldWhitelist)
34
+    {
35
+        // @TODO: Would it be better to not include the ignored_traits?
36
+        $ignoredTraits = $queryparams['ignored_traits'] ?? [];
37
+        $query = $this->search($ignoredTraits);
38 38
 
39
-		$orderColumn = $queryparams['order_by'] ?? null;
40
-		$orderDirection = $queryparams['order_direction'] ?? null;
41
-		if ($orderColumn !== null) {
42
-			$query->orderBy($orderColumn, $orderDirection);
43
-		}
39
+        $orderColumn = $queryparams['order_by'] ?? null;
40
+        $orderDirection = $queryparams['order_direction'] ?? null;
41
+        if ($orderColumn !== null) {
42
+            $query->orderBy($orderColumn, $orderDirection);
43
+        }
44 44
 		
45
-		$limit = $queryparams['limit'] ?? null;
46
-		if ($limit !== null) {
47
-			$query->limit($limit);
48
-		}
49
-
50
-		$offset = $queryparams['offset'] ?? null;
51
-		if ($offset !== null) {
52
-			$query->offset($offset);
53
-		}
54
-
55
-		$results = $query->fetchAll();
56
-
57
-		$resultsArray = [];
58
-		foreach ($results as $result) {
59
-			$resultsArray[] = $result->toArray($fieldWhitelist);
60
-		}
61
-
62
-		return $resultsArray;
63
-	}
64
-
65
-	public function toArray($fieldWhitelist)
66
-	{
67
-		$output = [];
68
-		foreach ($this->tableDefinition as $colName => $definition) {
69
-			if (in_array($colName, $fieldWhitelist)) {
70
-				$output[$colName] = $definition['value'];
71
-			}
72
-		}
73
-
74
-		return $output;
75
-	}
76
-
77
-	public function apiRead($id, Array $fieldWhitelist)
78
-	{
79
-		$this->read($id);
80
-		return $this->toArray($fieldWhitelist);
81
-	}
82
-
83
-	/* =============================================================
45
+        $limit = $queryparams['limit'] ?? null;
46
+        if ($limit !== null) {
47
+            $query->limit($limit);
48
+        }
49
+
50
+        $offset = $queryparams['offset'] ?? null;
51
+        if ($offset !== null) {
52
+            $query->offset($offset);
53
+        }
54
+
55
+        $results = $query->fetchAll();
56
+
57
+        $resultsArray = [];
58
+        foreach ($results as $result) {
59
+            $resultsArray[] = $result->toArray($fieldWhitelist);
60
+        }
61
+
62
+        return $resultsArray;
63
+    }
64
+
65
+    public function toArray($fieldWhitelist)
66
+    {
67
+        $output = [];
68
+        foreach ($this->tableDefinition as $colName => $definition) {
69
+            if (in_array($colName, $fieldWhitelist)) {
70
+                $output[$colName] = $definition['value'];
71
+            }
72
+        }
73
+
74
+        return $output;
75
+    }
76
+
77
+    public function apiRead($id, Array $fieldWhitelist)
78
+    {
79
+        $this->read($id);
80
+        return $this->toArray($fieldWhitelist);
81
+    }
82
+
83
+    /* =============================================================
84 84
 	 * ===================== Constraint validation =================
85 85
 	 * ============================================================= */
86 86
 
87
-	/**
88
-	 * Copy all table variables between two instances
89
-	 */
90
-	private function syncInstances($to, $from)
91
-	{
92
-		foreach ($to->tableDefinition as $colName => $definition) {
93
-			$definition['value'] = $from->tableDefinition[$colName]['value'];
94
-		}
95
-	}
96
-
97
-	private function filterInputColumns($input, $whitelist)
98
-	{
99
-		$filteredInput = $input;
100
-		foreach ($input as $colName => $value) {
101
-			if (!in_array($colName, $whitelist)) {
102
-				unset($filteredInput[$colName]);
103
-			}
104
-		}
105
-		return $filteredInput;
106
-	}
107
-
108
-	private function validateExcessKeys($input)
109
-	{
110
-		$errors = [];
111
-		foreach ($input as $colName => $value) {
112
-			if (!array_key_exists($colName, $this->tableDefinition)) {
113
-				$errors[$colName] = "Unknown input field";
114
-				continue;
115
-			}
116
-		}
117
-		return $errors;
118
-	}
119
-
120
-	private function validateImmutableColumns($input)
121
-	{
122
-		$errors = [];
123
-		foreach ($this->tableDefinition as $colName => $definition) {
124
-			$property = $definition['properties'] ?? null;
125
-			if (array_key_exists($colName, $input)
126
-				&& $property & ColumnProperty::IMMUTABLE) {
127
-				$errors[$colName] = "Field cannot be changed";
128
-			}
129
-		}
130
-		return $errors;
131
-	}
132
-
133
-	private function validateInputValues($input)
134
-	{
135
-		$errors = [];
136
-		foreach ($this->tableDefinition as $colName => $definition) {
137
-			// Validation check 1: If validate function is present
138
-			if (array_key_exists($colName, $input) 
139
-				&& is_callable($definition['validate'] ?? null)) {
140
-				$inputValue = $input[$colName];
141
-
142
-				// If validation function fails
143
-				[$status, $message] = $definition['validate']($inputValue);
144
-				if (!$status) {
145
-					$errors[$colName] = $message;
146
-				}	
147
-			}
148
-
149
-			// Validation check 2: If relation column, check whether entity exists
150
-			$properties = $definition['properties'] ?? null;
151
-			if (isset($definition['relation'])
152
-				&& ($properties & ColumnProperty::NOT_NULL)) {
153
-				$instance = clone $definition['relation'];
154
-				try {
155
-					$instance->read($input[$colName] ?? null);
156
-				} catch (ActiveRecordException $e) {
157
-					$errors[$colName] = "Entity for this value doesn't exist";
158
-				}
159
-			}
160
-		}
161
-		return $errors;
162
-	}
163
-
164
-	/**
165
-	 * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
166
-	 */
167
-	private function validateMissingKeys()
168
-	{
169
-		$errors = [];
170
-
171
-		foreach ($this->tableDefinition as $colName => $colDefinition) {
172
-			$default = $colDefinition['default'] ?? null;
173
-			$properties = $colDefinition['properties'] ?? null;
174
-			$value = $colDefinition['value'];
175
-
176
-			// If nullable and default not set => null
177
-			// If nullable and default null => default (null)
178
-			// If nullable and default set => default (value)
179
-
180
-			// if not nullable and default not set => error
181
-			// if not nullable and default null => error
182
-			// if not nullable and default st => default (value)
183
-			// => if not nullable and default null and value not set => error message in this method
184
-			if ($properties & ColumnProperty::NOT_NULL
185
-				&& $default === null
186
-				&& !($properties & ColumnProperty::AUTO_INCREMENT)
187
-				// && !array_key_exists($colName, $input)
188
-				&& $value === null) {
189
-				$errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
190
-			}
191
-		}
192
-
193
-		return $errors;
194
-	}
195
-
196
-	/**
197
-	 * Copies the values for entries in the input with matching variable names in the record definition
198
-	 * @param Array $input The input data to be loaded into $this record
199
-	 */
200
-	private function loadData($input)
201
-	{
202
-		foreach ($this->tableDefinition as $colName => $definition) {
203
-			if (array_key_exists($colName, $input)) {
204
-				$definition['value'] = $input[$colName];
205
-			}
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * @param Array $input Associative array of input values
211
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
212
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
213
-	 * 					of the modified data.
214
-	 */
215
-	public function apiCreate($input, Array $fieldWhitelist)
216
-	{
217
-		// Clone $this to new instance (for restoring if validation goes wrong)
218
-		$transaction = $this->newInstance();
219
-		$errors = [];
220
-
221
-		// Filter out all non-whitelisted input values
222
-		$input = $this->filterInputColumns($input, $fieldWhitelist);
223
-
224
-		// Validate excess keys
225
-		$errors += $transaction->validateExcessKeys($input);
226
-
227
-		// Validate input values (using validation function)
228
-		$errors += $transaction->validateInputValues($input);
229
-
230
-		// "Copy" data into transaction
231
-		$transaction->loadData($input);
232
-
233
-		// Run create hooks
234
-		foreach ($this->registeredCreateHooks as $colName => $fn) {
235
-			$fn();
236
-		}
237
-
238
-		// Validate missing keys
239
-		$errors += $transaction->validateMissingKeys();
240
-
241
-		// If no errors, commit the pending data
242
-		if (empty($errors)) {
243
-			$this->syncInstances($this, $transaction);
244
-
245
-			try {
246
-				(new Query($this->getPdo(), $this->getTableName()))
247
-					->insert($this->getActiveRecordColumns())
248
-					->execute();
249
-
250
-				$this->setId(intval($this->getPdo()->lastInsertId()));
251
-			} catch (\PDOException $e) {
252
-				// @TODO: Potentially filter and store mysql messages (where possible) in error messages
253
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
254
-			}
255
-
256
-			return [null, $this->toArray($fieldWhitelist)];
257
-		} else {
258
-			return [$errors, null];
259
-		}
260
-	}
261
-
262
-	/**
263
-	 * @param Array $input Associative array of input values
264
-	 * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
265
-	 * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
266
-	 * 					of the modified data.
267
-	 */
268
-	public function apiUpdate($input, Array $fieldWhitelist)
269
-	{
270
-		$transaction = $this->newInstance();
271
-		$errors = [];
272
-
273
-		// Filter out all non-whitelisted input values
274
-		$input = $this->filterInputColumns($input, $fieldWhitelist);
275
-
276
-		// Check for excess keys
277
-		$errors += $transaction->validateExcessKeys($input);
278
-
279
-		// Check for immutable keys
280
-		$errors += $transaction->validateImmutableColumns($input);
281
-
282
-		// Validate input values (using validation function)
283
-		$errors += $transaction->validateInputValues($input);
284
-
285
-		// "Copy" data into transaction
286
-		$transaction->loadData($input);
287
-
288
-		// Run create hooks
289
-		foreach ($this->registeredUpdateHooks as $colName => $fn) {
290
-			$fn();
291
-		}
292
-
293
-		// Validate missing keys
294
-		$errors += $transaction->validateMissingKeys();
295
-
296
-		// Update database
297
-		if (empty($errors)) {
298
-			$this->syncInstances($this, $transaction);
299
-
300
-			try {
301
-				(new Query($this->getPdo(), $this->getTableName()))
302
-					->update($this->getActiveRecordColumns())
303
-					->where(Query::Equal('id', $this->getId()))
304
-					->execute();
305
-			} catch (\PDOException $e) {
306
-				throw new ActiveRecordException($e->getMessage(), 0, $e);
307
-			}
308
-
309
-			return [null, $this->toArray($fieldWhitelist)];
310
-		} else {
311
-			return [$errors, null];
312
-		}
313
-	}
314
-
315
-	/**
316
-	 * Returns this active record after reading the attributes from the entry with the given identifier.
317
-	 *
318
-	 * @param mixed $id
319
-	 * @return $this
320
-	 * @throws ActiveRecordException on failure.
321
-	 */
322
-	abstract public function read($id);
323
-
324
-	/**
325
-	 * Returns the PDO.
326
-	 *
327
-	 * @return \PDO the PDO.
328
-	 */
329
-	abstract public function getPdo();
330
-
331
-	/**
332
-	 * Set the ID.
333
-	 *
334
-	 * @param int $id
335
-	 * @return $this
336
-	 */
337
-	abstract protected function setId($id);
338
-
339
-	/**
340
-	 * Returns the ID.
341
-	 *
342
-	 * @return null|int The ID.
343
-	 */
344
-	abstract protected function getId();
345
-
346
-	/**
347
-	 * Returns the active record table.
348
-	 *
349
-	 * @return string the active record table name.
350
-	 */
351
-	abstract protected function getTableName();
352
-
353
-	/**
354
-	 * Returns the name -> variable mapping for the table definition.
355
-	 * @return Array The mapping
356
-	 */
357
-	abstract protected function getActiveRecordColumns();
87
+    /**
88
+     * Copy all table variables between two instances
89
+     */
90
+    private function syncInstances($to, $from)
91
+    {
92
+        foreach ($to->tableDefinition as $colName => $definition) {
93
+            $definition['value'] = $from->tableDefinition[$colName]['value'];
94
+        }
95
+    }
96
+
97
+    private function filterInputColumns($input, $whitelist)
98
+    {
99
+        $filteredInput = $input;
100
+        foreach ($input as $colName => $value) {
101
+            if (!in_array($colName, $whitelist)) {
102
+                unset($filteredInput[$colName]);
103
+            }
104
+        }
105
+        return $filteredInput;
106
+    }
107
+
108
+    private function validateExcessKeys($input)
109
+    {
110
+        $errors = [];
111
+        foreach ($input as $colName => $value) {
112
+            if (!array_key_exists($colName, $this->tableDefinition)) {
113
+                $errors[$colName] = "Unknown input field";
114
+                continue;
115
+            }
116
+        }
117
+        return $errors;
118
+    }
119
+
120
+    private function validateImmutableColumns($input)
121
+    {
122
+        $errors = [];
123
+        foreach ($this->tableDefinition as $colName => $definition) {
124
+            $property = $definition['properties'] ?? null;
125
+            if (array_key_exists($colName, $input)
126
+                && $property & ColumnProperty::IMMUTABLE) {
127
+                $errors[$colName] = "Field cannot be changed";
128
+            }
129
+        }
130
+        return $errors;
131
+    }
132
+
133
+    private function validateInputValues($input)
134
+    {
135
+        $errors = [];
136
+        foreach ($this->tableDefinition as $colName => $definition) {
137
+            // Validation check 1: If validate function is present
138
+            if (array_key_exists($colName, $input) 
139
+                && is_callable($definition['validate'] ?? null)) {
140
+                $inputValue = $input[$colName];
141
+
142
+                // If validation function fails
143
+                [$status, $message] = $definition['validate']($inputValue);
144
+                if (!$status) {
145
+                    $errors[$colName] = $message;
146
+                }	
147
+            }
148
+
149
+            // Validation check 2: If relation column, check whether entity exists
150
+            $properties = $definition['properties'] ?? null;
151
+            if (isset($definition['relation'])
152
+                && ($properties & ColumnProperty::NOT_NULL)) {
153
+                $instance = clone $definition['relation'];
154
+                try {
155
+                    $instance->read($input[$colName] ?? null);
156
+                } catch (ActiveRecordException $e) {
157
+                    $errors[$colName] = "Entity for this value doesn't exist";
158
+                }
159
+            }
160
+        }
161
+        return $errors;
162
+    }
163
+
164
+    /**
165
+     * This function is only used for API Update calls (direct getter/setter functions are unconstrained)
166
+     */
167
+    private function validateMissingKeys()
168
+    {
169
+        $errors = [];
170
+
171
+        foreach ($this->tableDefinition as $colName => $colDefinition) {
172
+            $default = $colDefinition['default'] ?? null;
173
+            $properties = $colDefinition['properties'] ?? null;
174
+            $value = $colDefinition['value'];
175
+
176
+            // If nullable and default not set => null
177
+            // If nullable and default null => default (null)
178
+            // If nullable and default set => default (value)
179
+
180
+            // if not nullable and default not set => error
181
+            // if not nullable and default null => error
182
+            // if not nullable and default st => default (value)
183
+            // => if not nullable and default null and value not set => error message in this method
184
+            if ($properties & ColumnProperty::NOT_NULL
185
+                && $default === null
186
+                && !($properties & ColumnProperty::AUTO_INCREMENT)
187
+                // && !array_key_exists($colName, $input)
188
+                && $value === null) {
189
+                $errors[$colName] = sprintf("The required field \"%s\" is missing", $colName);
190
+            }
191
+        }
192
+
193
+        return $errors;
194
+    }
195
+
196
+    /**
197
+     * Copies the values for entries in the input with matching variable names in the record definition
198
+     * @param Array $input The input data to be loaded into $this record
199
+     */
200
+    private function loadData($input)
201
+    {
202
+        foreach ($this->tableDefinition as $colName => $definition) {
203
+            if (array_key_exists($colName, $input)) {
204
+                $definition['value'] = $input[$colName];
205
+            }
206
+        }
207
+    }
208
+
209
+    /**
210
+     * @param Array $input Associative array of input values
211
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
212
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
213
+     * 					of the modified data.
214
+     */
215
+    public function apiCreate($input, Array $fieldWhitelist)
216
+    {
217
+        // Clone $this to new instance (for restoring if validation goes wrong)
218
+        $transaction = $this->newInstance();
219
+        $errors = [];
220
+
221
+        // Filter out all non-whitelisted input values
222
+        $input = $this->filterInputColumns($input, $fieldWhitelist);
223
+
224
+        // Validate excess keys
225
+        $errors += $transaction->validateExcessKeys($input);
226
+
227
+        // Validate input values (using validation function)
228
+        $errors += $transaction->validateInputValues($input);
229
+
230
+        // "Copy" data into transaction
231
+        $transaction->loadData($input);
232
+
233
+        // Run create hooks
234
+        foreach ($this->registeredCreateHooks as $colName => $fn) {
235
+            $fn();
236
+        }
237
+
238
+        // Validate missing keys
239
+        $errors += $transaction->validateMissingKeys();
240
+
241
+        // If no errors, commit the pending data
242
+        if (empty($errors)) {
243
+            $this->syncInstances($this, $transaction);
244
+
245
+            try {
246
+                (new Query($this->getPdo(), $this->getTableName()))
247
+                    ->insert($this->getActiveRecordColumns())
248
+                    ->execute();
249
+
250
+                $this->setId(intval($this->getPdo()->lastInsertId()));
251
+            } catch (\PDOException $e) {
252
+                // @TODO: Potentially filter and store mysql messages (where possible) in error messages
253
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
254
+            }
255
+
256
+            return [null, $this->toArray($fieldWhitelist)];
257
+        } else {
258
+            return [$errors, null];
259
+        }
260
+    }
261
+
262
+    /**
263
+     * @param Array $input Associative array of input values
264
+     * @param Array $fieldWhitelist array of column names that are allowed to be filled by the input array 
265
+     * @return Array Array containing the set of optional errors (associative array) and an optional array representation (associative)
266
+     * 					of the modified data.
267
+     */
268
+    public function apiUpdate($input, Array $fieldWhitelist)
269
+    {
270
+        $transaction = $this->newInstance();
271
+        $errors = [];
272
+
273
+        // Filter out all non-whitelisted input values
274
+        $input = $this->filterInputColumns($input, $fieldWhitelist);
275
+
276
+        // Check for excess keys
277
+        $errors += $transaction->validateExcessKeys($input);
278
+
279
+        // Check for immutable keys
280
+        $errors += $transaction->validateImmutableColumns($input);
281
+
282
+        // Validate input values (using validation function)
283
+        $errors += $transaction->validateInputValues($input);
284
+
285
+        // "Copy" data into transaction
286
+        $transaction->loadData($input);
287
+
288
+        // Run create hooks
289
+        foreach ($this->registeredUpdateHooks as $colName => $fn) {
290
+            $fn();
291
+        }
292
+
293
+        // Validate missing keys
294
+        $errors += $transaction->validateMissingKeys();
295
+
296
+        // Update database
297
+        if (empty($errors)) {
298
+            $this->syncInstances($this, $transaction);
299
+
300
+            try {
301
+                (new Query($this->getPdo(), $this->getTableName()))
302
+                    ->update($this->getActiveRecordColumns())
303
+                    ->where(Query::Equal('id', $this->getId()))
304
+                    ->execute();
305
+            } catch (\PDOException $e) {
306
+                throw new ActiveRecordException($e->getMessage(), 0, $e);
307
+            }
308
+
309
+            return [null, $this->toArray($fieldWhitelist)];
310
+        } else {
311
+            return [$errors, null];
312
+        }
313
+    }
314
+
315
+    /**
316
+     * Returns this active record after reading the attributes from the entry with the given identifier.
317
+     *
318
+     * @param mixed $id
319
+     * @return $this
320
+     * @throws ActiveRecordException on failure.
321
+     */
322
+    abstract public function read($id);
323
+
324
+    /**
325
+     * Returns the PDO.
326
+     *
327
+     * @return \PDO the PDO.
328
+     */
329
+    abstract public function getPdo();
330
+
331
+    /**
332
+     * Set the ID.
333
+     *
334
+     * @param int $id
335
+     * @return $this
336
+     */
337
+    abstract protected function setId($id);
338
+
339
+    /**
340
+     * Returns the ID.
341
+     *
342
+     * @return null|int The ID.
343
+     */
344
+    abstract protected function getId();
345
+
346
+    /**
347
+     * Returns the active record table.
348
+     *
349
+     * @return string the active record table name.
350
+     */
351
+    abstract protected function getTableName();
352
+
353
+    /**
354
+     * Returns the name -> variable mapping for the table definition.
355
+     * @return Array The mapping
356
+     */
357
+    abstract protected function getActiveRecordColumns();
358 358
 }
Please login to merge, or discard this patch.
src/AbstractActiveRecord.php 2 patches
Indentation   +599 added lines, -599 removed lines patch added patch discarded remove patch
@@ -18,610 +18,610 @@
 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
-	/** @var \PDO The PDO object. */
25
-	protected $pdo;
26
-
27
-	/** @var null|int The ID. */
28
-	private $id;
29
-
30
-	/** @var array A map of column name to functions that hook the insert function */
31
-	protected $registeredCreateHooks;
32
-
33
-	/** @var array A map of column name to functions that hook the read function */
34
-	protected $registeredReadHooks;
35
-
36
-	/** @var array A map of column name to functions that hook the update function */
37
-	protected $registeredUpdateHooks;
38
-
39
-	/** @var array A map of column name to functions that hook the update function */
40
-	protected $registeredDeleteHooks;	
41
-
42
-	/** @var array A map of column name to functions that hook the search function */
43
-	protected $registeredSearchHooks;
44
-
45
-	/** @var array A list of table column definitions */
46
-	protected $tableDefinition;
47
-
48
-	/**
49
-	 * Construct an abstract active record with the given PDO.
50
-	 *
51
-	 * @param \PDO $pdo
52
-	 */
53
-	public function __construct(\PDO $pdo )
54
-	{
55
-		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56
-		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
57
-
58
-		$this->setPdo($pdo);
59
-
60
-		$this->registeredCreateHooks = [];
61
-		$this->registeredReadHooks = [];
62
-		$this->registeredUpdateHooks = [];
63
-		$this->registeredDeleteHooks = [];
64
-		$this->registeredSearchHooks = [];
65
-		$this->tableDefinition = $this->getTableDefinition();
66
-
67
-		// Extend table definition with default ID field, throw exception if field already exists
68
-		if (array_key_exists('id', $this->tableDefinition)) {
69
-			$message = "Table definition in record contains a field with name \"id\"";
70
-			$message .= ", which is a reserved name by ActiveRecord";
71
-			throw new ActiveRecordException($message, 0);
72
-		}
73
-
74
-		$this->tableDefinition[self::COLUMN_NAME_ID] =
75
-		[
76
-			'value' => &$this->id,
77
-			'validate' => null,
78
-			'type' => self::COLUMN_TYPE_ID,
79
-			'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY
80
-		];
81
-	}
82
-
83
-	private function checkHookConstraints($columnName, $hookMap)
84
-	{
85
-		// Check whether column exists
86
-		if (!array_key_exists($columnName, $this->tableDefinition)) 
87
-		{
88
-			throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
89
-		}
90
-
91
-		// Enforcing 1 hook per table column
92
-		if (array_key_exists($columnName, $hookMap)) {
93
-			$message = "Hook is trying to register on an already registered column \"$columnName\", ";
94
-			$message .= "do you have conflicting traits?";
95
-			throw new ActiveRecordException($message, 0);
96
-		}
97
-	}
98
-
99
-	/**
100
-	 * Register a new hook for a specific column that gets called before execution of the create() method
101
-	 * Only one hook per column can be registered at a time
102
-	 * @param string $columnName The name of the column that is registered.
103
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
104
-	 */
105
-	public function registerCreateHook($columnName, $fn)
106
-	{
107
-		$this->checkHookConstraints($columnName, $this->registeredCreateHooks);
108
-
109
-		if (is_string($fn) && is_callable([$this, $fn])) {
110
-			$this->registeredCreateHooks[$columnName] = [$this, $fn];
111
-		} else if (is_callable($fn)) {
112
-			$this->registeredCreateHooks[$columnName] = $fn;
113
-		} else {
114
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
115
-		}
116
-	}
117
-
118
-	/**
119
-	 * Register a new hook for a specific column that gets called before execution of the read() method
120
-	 * Only one hook per column can be registered at a time
121
-	 * @param string $columnName The name of the column that is registered.
122
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
123
-	 */
124
-	public function registerReadHook($columnName, $fn)
125
-	{
126
-		$this->checkHookConstraints($columnName, $this->registeredReadHooks);
127
-
128
-		if (is_string($fn) && is_callable([$this, $fn])) {
129
-			$this->registeredReadHooks[$columnName] = [$this, $fn];
130
-		} else if (is_callable($fn)) {
131
-			$this->registeredReadHooks[$columnName] = $fn;
132
-		} else {
133
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
134
-		}
135
-	}
136
-
137
-	/**
138
-	 * Register a new hook for a specific column that gets called before execution of the update() method
139
-	 * Only one hook per column can be registered at a time
140
-	 * @param string $columnName The name of the column that is registered.
141
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
142
-	 */
143
-	public function registerUpdateHook($columnName, $fn)
144
-	{
145
-		$this->checkHookConstraints($columnName, $this->registeredUpdateHooks);
146
-
147
-		if (is_string($fn) && is_callable([$this, $fn])) {
148
-			$this->registeredUpdateHooks[$columnName] = [$this, $fn];
149
-		} else if (is_callable($fn)) {
150
-			$this->registeredUpdateHooks[$columnName] = $fn;
151
-		} else {
152
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * Register a new hook for a specific column that gets called before execution of the delete() method
158
-	 * Only one hook per column can be registered at a time
159
-	 * @param string $columnName The name of the column that is registered.
160
-	 * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
-	 */
162
-	public function registerDeleteHook($columnName, $fn)
163
-	{
164
-		$this->checkHookConstraints($columnName, $this->registeredDeleteHooks);
165
-
166
-		if (is_string($fn) && is_callable([$this, $fn])) {
167
-			$this->registeredDeleteHooks[$columnName] = [$this, $fn];
168
-		} else if (is_callable($fn)) {
169
-			$this->registeredDeleteHooks[$columnName] = $fn;
170
-		} else {
171
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
172
-		}
173
-	}
174
-
175
-	/**
176
-	 * Register a new hook for a specific column that gets called before execution of the search() method
177
-	 * Only one hook per column can be registered at a time
178
-	 * @param string $columnName The name of the column that is registered.
179
-	 * @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; 
180
-	 */
181
-	public function registerSearchHook($columnName, $fn)
182
-	{
183
-		$this->checkHookConstraints($columnName, $this->registeredSearchHooks);
184
-
185
-		if (is_string($fn) && is_callable([$this, $fn])) {
186
-			$this->registeredSearchHooks[$columnName] = [$this, $fn];
187
-		} else if (is_callable($fn)) {
188
-			$this->registeredSearchHooks[$columnName] = $fn;
189
-		} else {
190
-			throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
191
-		}
192
-	}
193
-
194
-	/**
195
-	 * Adds a new column definition to the table.
196
-	 * @param string $columnName The name of the column that is registered.
197
-	 * @param Array $definition The definition of that column.
198
-	 */
199
-	public function extendTableDefinition($columnName, $definition)
200
-	{
201
-		if ($this->tableDefinition === null) {
202
-			throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor");
203
-		}
204
-
205
-		// Enforcing table can only be extended with new columns
206
-		if (array_key_exists($columnName, $this->tableDefinition)) {
207
-			$message = "Table is being extended with a column that already exists, ";
208
-			$message .= "\"$columnName\" conflicts with your table definition";
209
-			throw new ActiveRecordException($message, 0);
210
-		}
211
-
212
-		$this->tableDefinition[$columnName] = $definition;
213
-	}
214
-
215
-	/**
216
-	 * Returns the type string as it should appear in the mysql create table statement for the given column
217
-	 * @return string The type string
218
-	 */
219
-	private function getDatabaseTypeString($colName, $type, $length)
220
-	{
221
-		switch (strtoupper($type)) {
222
-			case '':
223
-				throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
21
+    const COLUMN_NAME_ID = 'id';
22
+    const COLUMN_TYPE_ID = 'INT UNSIGNED';
23
+
24
+    /** @var \PDO The PDO object. */
25
+    protected $pdo;
26
+
27
+    /** @var null|int The ID. */
28
+    private $id;
29
+
30
+    /** @var array A map of column name to functions that hook the insert function */
31
+    protected $registeredCreateHooks;
32
+
33
+    /** @var array A map of column name to functions that hook the read function */
34
+    protected $registeredReadHooks;
35
+
36
+    /** @var array A map of column name to functions that hook the update function */
37
+    protected $registeredUpdateHooks;
38
+
39
+    /** @var array A map of column name to functions that hook the update function */
40
+    protected $registeredDeleteHooks;	
41
+
42
+    /** @var array A map of column name to functions that hook the search function */
43
+    protected $registeredSearchHooks;
44
+
45
+    /** @var array A list of table column definitions */
46
+    protected $tableDefinition;
47
+
48
+    /**
49
+     * Construct an abstract active record with the given PDO.
50
+     *
51
+     * @param \PDO $pdo
52
+     */
53
+    public function __construct(\PDO $pdo )
54
+    {
55
+        $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56
+        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
57
+
58
+        $this->setPdo($pdo);
59
+
60
+        $this->registeredCreateHooks = [];
61
+        $this->registeredReadHooks = [];
62
+        $this->registeredUpdateHooks = [];
63
+        $this->registeredDeleteHooks = [];
64
+        $this->registeredSearchHooks = [];
65
+        $this->tableDefinition = $this->getTableDefinition();
66
+
67
+        // Extend table definition with default ID field, throw exception if field already exists
68
+        if (array_key_exists('id', $this->tableDefinition)) {
69
+            $message = "Table definition in record contains a field with name \"id\"";
70
+            $message .= ", which is a reserved name by ActiveRecord";
71
+            throw new ActiveRecordException($message, 0);
72
+        }
73
+
74
+        $this->tableDefinition[self::COLUMN_NAME_ID] =
75
+        [
76
+            'value' => &$this->id,
77
+            'validate' => null,
78
+            'type' => self::COLUMN_TYPE_ID,
79
+            'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY
80
+        ];
81
+    }
82
+
83
+    private function checkHookConstraints($columnName, $hookMap)
84
+    {
85
+        // Check whether column exists
86
+        if (!array_key_exists($columnName, $this->tableDefinition)) 
87
+        {
88
+            throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0);
89
+        }
90
+
91
+        // Enforcing 1 hook per table column
92
+        if (array_key_exists($columnName, $hookMap)) {
93
+            $message = "Hook is trying to register on an already registered column \"$columnName\", ";
94
+            $message .= "do you have conflicting traits?";
95
+            throw new ActiveRecordException($message, 0);
96
+        }
97
+    }
98
+
99
+    /**
100
+     * Register a new hook for a specific column that gets called before execution of the create() method
101
+     * Only one hook per column can be registered at a time
102
+     * @param string $columnName The name of the column that is registered.
103
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
104
+     */
105
+    public function registerCreateHook($columnName, $fn)
106
+    {
107
+        $this->checkHookConstraints($columnName, $this->registeredCreateHooks);
108
+
109
+        if (is_string($fn) && is_callable([$this, $fn])) {
110
+            $this->registeredCreateHooks[$columnName] = [$this, $fn];
111
+        } else if (is_callable($fn)) {
112
+            $this->registeredCreateHooks[$columnName] = $fn;
113
+        } else {
114
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
115
+        }
116
+    }
117
+
118
+    /**
119
+     * Register a new hook for a specific column that gets called before execution of the read() method
120
+     * Only one hook per column can be registered at a time
121
+     * @param string $columnName The name of the column that is registered.
122
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
123
+     */
124
+    public function registerReadHook($columnName, $fn)
125
+    {
126
+        $this->checkHookConstraints($columnName, $this->registeredReadHooks);
127
+
128
+        if (is_string($fn) && is_callable([$this, $fn])) {
129
+            $this->registeredReadHooks[$columnName] = [$this, $fn];
130
+        } else if (is_callable($fn)) {
131
+            $this->registeredReadHooks[$columnName] = $fn;
132
+        } else {
133
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
134
+        }
135
+    }
136
+
137
+    /**
138
+     * Register a new hook for a specific column that gets called before execution of the update() method
139
+     * Only one hook per column can be registered at a time
140
+     * @param string $columnName The name of the column that is registered.
141
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
142
+     */
143
+    public function registerUpdateHook($columnName, $fn)
144
+    {
145
+        $this->checkHookConstraints($columnName, $this->registeredUpdateHooks);
146
+
147
+        if (is_string($fn) && is_callable([$this, $fn])) {
148
+            $this->registeredUpdateHooks[$columnName] = [$this, $fn];
149
+        } else if (is_callable($fn)) {
150
+            $this->registeredUpdateHooks[$columnName] = $fn;
151
+        } else {
152
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
153
+        }
154
+    }
155
+
156
+    /**
157
+     * Register a new hook for a specific column that gets called before execution of the delete() method
158
+     * Only one hook per column can be registered at a time
159
+     * @param string $columnName The name of the column that is registered.
160
+     * @param string|callable $fn Either a callable, or the name of a method on the inheriting object.
161
+     */
162
+    public function registerDeleteHook($columnName, $fn)
163
+    {
164
+        $this->checkHookConstraints($columnName, $this->registeredDeleteHooks);
165
+
166
+        if (is_string($fn) && is_callable([$this, $fn])) {
167
+            $this->registeredDeleteHooks[$columnName] = [$this, $fn];
168
+        } else if (is_callable($fn)) {
169
+            $this->registeredDeleteHooks[$columnName] = $fn;
170
+        } else {
171
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
172
+        }
173
+    }
174
+
175
+    /**
176
+     * Register a new hook for a specific column that gets called before execution of the search() method
177
+     * Only one hook per column can be registered at a time
178
+     * @param string $columnName The name of the column that is registered.
179
+     * @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; 
180
+     */
181
+    public function registerSearchHook($columnName, $fn)
182
+    {
183
+        $this->checkHookConstraints($columnName, $this->registeredSearchHooks);
184
+
185
+        if (is_string($fn) && is_callable([$this, $fn])) {
186
+            $this->registeredSearchHooks[$columnName] = [$this, $fn];
187
+        } else if (is_callable($fn)) {
188
+            $this->registeredSearchHooks[$columnName] = $fn;
189
+        } else {
190
+            throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0);
191
+        }
192
+    }
193
+
194
+    /**
195
+     * Adds a new column definition to the table.
196
+     * @param string $columnName The name of the column that is registered.
197
+     * @param Array $definition The definition of that column.
198
+     */
199
+    public function extendTableDefinition($columnName, $definition)
200
+    {
201
+        if ($this->tableDefinition === null) {
202
+            throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor");
203
+        }
204
+
205
+        // Enforcing table can only be extended with new columns
206
+        if (array_key_exists($columnName, $this->tableDefinition)) {
207
+            $message = "Table is being extended with a column that already exists, ";
208
+            $message .= "\"$columnName\" conflicts with your table definition";
209
+            throw new ActiveRecordException($message, 0);
210
+        }
211
+
212
+        $this->tableDefinition[$columnName] = $definition;
213
+    }
214
+
215
+    /**
216
+     * Returns the type string as it should appear in the mysql create table statement for the given column
217
+     * @return string The type string
218
+     */
219
+    private function getDatabaseTypeString($colName, $type, $length)
220
+    {
221
+        switch (strtoupper($type)) {
222
+            case '':
223
+                throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName));
224 224
 				
225
-			case 'DATETIME':
226
-			case 'DATE':
227
-			case 'TIME':
228
-			case 'TEXT':
229
-			case 'INT UNSIGNED':
230
-				return $type;
231
-
232
-			case 'VARCHAR':
233
-				if ($length === null) {
234
-					throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
235
-				} else {
236
-					return sprintf('%s(%d)', $type, $length);	
237
-				}
238
-
239
-			case 'INT':
240
-			case 'TINYINT':
241
-			case 'BIGINT':
242
-			default: 	
243
-				// Implicitly assuming that non-specified cases are correct without a length parameter
244
-				if ($length === null) {
245
-					return $type;
246
-				} else {
247
-					return sprintf('%s(%d)', $type, $length);	
248
-				}
249
-		}
250
-	}
251
-
252
-	/**
253
-	 * Builds the part of a MySQL create table statement that corresponds to the supplied column
254
-	 * @param string $colName 	Name of the database column
255
-	 * @param string $type 		The type of the string
256
-	 * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
257
-	 * @return string
258
-	 */
259
-	private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
260
-	{
261
-		$stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length));
262
-		if ($properties & ColumnProperty::NOT_NULL) {
263
-			$stmnt .= 'NOT NULL ';
264
-		} else {
265
-			$stmnt .= 'NULL ';
266
-		}
267
-
268
-		if ($default !== NULL) {
269
-			$stmnt .= ' DEFAULT ' . $default . ' ';
270
-		}
271
-
272
-		if ($properties & ColumnProperty::AUTO_INCREMENT) {
273
-			$stmnt .= 'AUTO_INCREMENT ';
274
-		}
275
-
276
-		if ($properties & ColumnProperty::UNIQUE) {
277
-			$stmnt .= 'UNIQUE ';
278
-		}
279
-
280
-		if ($properties & ColumnProperty::PRIMARY_KEY) {
281
-			$stmnt .= 'PRIMARY KEY ';
282
-		}
283
-
284
-		return $stmnt;
285
-	}
286
-
287
-	/**
288
-	 * Sorts the column statement components in the order such that the id appears first, 
289
-	 * 		followed by all other columns in alphabetical ascending order
290
-	 * @param   Array $colStatements Array of column statements
291
-	 * @return  Array
292
-	 */
293
-	private function sortColumnStatements($colStatements)
294
-	{
295
-		// Find ID statement and put it first
296
-		$sortedStatements = [];
297
-
298
-		$sortedStatements[] = $colStatements[self::COLUMN_NAME_ID];
299
-		unset($colStatements[self::COLUMN_NAME_ID]);
300
-
301
-		// Sort remaining columns in alphabetical order
302
-		$columns = array_keys($colStatements);
303
-		sort($columns);
304
-		foreach ($columns as $colName) {
305
-			$sortedStatements[] = $colStatements[$colName];
306
-		}
307
-
308
-		return $sortedStatements;
309
-	}
310
-
311
-	/**
312
-	 * Builds the MySQL Create Table statement for the internal table definition
313
-	 * @return string
314
-	 */
315
-	public function buildCreateTableSQL()
316
-	{
317
-		$columnStatements = [];
318
-		foreach ($this->tableDefinition as $colName => $definition) {
319
-			// Destructure column definition
320
-			$type    = $definition['type'] ?? null;
321
-			$default = $definition['default'] ?? null;
322
-			$length  = $definition['length'] ?? null;
323
-			$properties = $definition['properties'] ?? null;
324
-
325
-			if (isset($definition['relation']) && $type !== null) {
326
-				$msg = "Column \"$colName\": ";
327
-				$msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
328
-				throw new ActiveRecordException($msg);
329
-			} else if (isset($definition['relation'])) {
330
-				$type = self::COLUMN_TYPE_ID;
331
-			}
332
-
333
-			$columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
334
-		}
335
-
336
-		// Sort table (first column is id, the remaining are alphabetically sorted)
337
-		$columnStatements = $this->sortColumnStatements($columnStatements);
338
-
339
-		$sql = 'CREATE TABLE ' . $this->getTableName() . ' ';
340
-		$sql .= "(\n";
341
-		$sql .= implode(",\n", $columnStatements);
342
-		$sql .= "\n);";
343
-
344
-		return $sql;
345
-	}
346
-
347
-	/**
348
-	 * Creates the entity as a table in the database
349
-	 */
350
-	public function createTable()
351
-	{
352
-		$this->pdo->query($this->buildCreateTableSQL());
353
-	}
354
-
355
-	/**
356
-	 * builds a MySQL constraint statement for the given parameters
357
-	 * @param string $parentTable
358
-	 * @param string $parentColumn
359
-	 * @param string $childTable
360
-	 * @param string $childColumn
361
-	 * @return string The MySQL table constraint string
362
-	 */
363
-	protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
364
-	{
365
-		$template = <<<SQL
225
+            case 'DATETIME':
226
+            case 'DATE':
227
+            case 'TIME':
228
+            case 'TEXT':
229
+            case 'INT UNSIGNED':
230
+                return $type;
231
+
232
+            case 'VARCHAR':
233
+                if ($length === null) {
234
+                    throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName));
235
+                } else {
236
+                    return sprintf('%s(%d)', $type, $length);	
237
+                }
238
+
239
+            case 'INT':
240
+            case 'TINYINT':
241
+            case 'BIGINT':
242
+            default: 	
243
+                // Implicitly assuming that non-specified cases are correct without a length parameter
244
+                if ($length === null) {
245
+                    return $type;
246
+                } else {
247
+                    return sprintf('%s(%d)', $type, $length);	
248
+                }
249
+        }
250
+    }
251
+
252
+    /**
253
+     * Builds the part of a MySQL create table statement that corresponds to the supplied column
254
+     * @param string $colName 	Name of the database column
255
+     * @param string $type 		The type of the string
256
+     * @param int $properties 	The set of Column properties that apply to this column (See ColumnProperty for options)
257
+     * @return string
258
+     */
259
+    private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default)
260
+    {
261
+        $stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length));
262
+        if ($properties & ColumnProperty::NOT_NULL) {
263
+            $stmnt .= 'NOT NULL ';
264
+        } else {
265
+            $stmnt .= 'NULL ';
266
+        }
267
+
268
+        if ($default !== NULL) {
269
+            $stmnt .= ' DEFAULT ' . $default . ' ';
270
+        }
271
+
272
+        if ($properties & ColumnProperty::AUTO_INCREMENT) {
273
+            $stmnt .= 'AUTO_INCREMENT ';
274
+        }
275
+
276
+        if ($properties & ColumnProperty::UNIQUE) {
277
+            $stmnt .= 'UNIQUE ';
278
+        }
279
+
280
+        if ($properties & ColumnProperty::PRIMARY_KEY) {
281
+            $stmnt .= 'PRIMARY KEY ';
282
+        }
283
+
284
+        return $stmnt;
285
+    }
286
+
287
+    /**
288
+     * Sorts the column statement components in the order such that the id appears first, 
289
+     * 		followed by all other columns in alphabetical ascending order
290
+     * @param   Array $colStatements Array of column statements
291
+     * @return  Array
292
+     */
293
+    private function sortColumnStatements($colStatements)
294
+    {
295
+        // Find ID statement and put it first
296
+        $sortedStatements = [];
297
+
298
+        $sortedStatements[] = $colStatements[self::COLUMN_NAME_ID];
299
+        unset($colStatements[self::COLUMN_NAME_ID]);
300
+
301
+        // Sort remaining columns in alphabetical order
302
+        $columns = array_keys($colStatements);
303
+        sort($columns);
304
+        foreach ($columns as $colName) {
305
+            $sortedStatements[] = $colStatements[$colName];
306
+        }
307
+
308
+        return $sortedStatements;
309
+    }
310
+
311
+    /**
312
+     * Builds the MySQL Create Table statement for the internal table definition
313
+     * @return string
314
+     */
315
+    public function buildCreateTableSQL()
316
+    {
317
+        $columnStatements = [];
318
+        foreach ($this->tableDefinition as $colName => $definition) {
319
+            // Destructure column definition
320
+            $type    = $definition['type'] ?? null;
321
+            $default = $definition['default'] ?? null;
322
+            $length  = $definition['length'] ?? null;
323
+            $properties = $definition['properties'] ?? null;
324
+
325
+            if (isset($definition['relation']) && $type !== null) {
326
+                $msg = "Column \"$colName\": ";
327
+                $msg .= "Relationship columns have an automatically inferred type, so type should be omitted";
328
+                throw new ActiveRecordException($msg);
329
+            } else if (isset($definition['relation'])) {
330
+                $type = self::COLUMN_TYPE_ID;
331
+            }
332
+
333
+            $columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default);
334
+        }
335
+
336
+        // Sort table (first column is id, the remaining are alphabetically sorted)
337
+        $columnStatements = $this->sortColumnStatements($columnStatements);
338
+
339
+        $sql = 'CREATE TABLE ' . $this->getTableName() . ' ';
340
+        $sql .= "(\n";
341
+        $sql .= implode(",\n", $columnStatements);
342
+        $sql .= "\n);";
343
+
344
+        return $sql;
345
+    }
346
+
347
+    /**
348
+     * Creates the entity as a table in the database
349
+     */
350
+    public function createTable()
351
+    {
352
+        $this->pdo->query($this->buildCreateTableSQL());
353
+    }
354
+
355
+    /**
356
+     * builds a MySQL constraint statement for the given parameters
357
+     * @param string $parentTable
358
+     * @param string $parentColumn
359
+     * @param string $childTable
360
+     * @param string $childColumn
361
+     * @return string The MySQL table constraint string
362
+     */
363
+    protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn)
364
+    {
365
+        $template = <<<SQL
366 366
 ALTER TABLE `%s`
367 367
 ADD CONSTRAINT
368 368
 FOREIGN KEY (`%s`)
369 369
 REFERENCES `%s`(`%s`)
370 370
 ON DELETE CASCADE;
371 371
 SQL;
372
-		return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
373
-	}
374
-
375
-	/**
376
-	 * Iterates over the specified constraints in the table definition, 
377
-	 * 		and applies these to the database.
378
-	 */
379
-	public function createTableConstraints()
380
-	{
381
-		// Iterate over columns, check whether "relation" field exists, if so create constraint
382
-		foreach ($this->tableDefinition as $colName => $definition) {
383
-			if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
384
-				// Forge new relation
385
-				$target = $definition['relation'];
386
-				$constraintSql = $this->buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
387
-
388
-				$this->pdo->query($constraintSql);
389
-			}
390
-		}
391
-	}
392
-
393
-	/**
394
-	 * Returns the name -> variable mapping for the table definition.
395
-	 * @return Array The mapping
396
-	 */
397
-	protected function getActiveRecordColumns()
398
-	{
399
-		$bindings = [];
400
-		foreach ($this->tableDefinition as $colName => $definition) {
401
-
402
-			// Ignore the id column (key) when inserting or updating
403
-			if ($colName == self::COLUMN_NAME_ID) {
404
-				continue;
405
-			}
406
-
407
-			$bindings[$colName] = &$definition['value'];
408
-		}
409
-		return $bindings;
410
-	}
411
-
412
-	/**
413
-	 * {@inheritdoc}
414
-	 */
415
-	public function create()
416
-	{
417
-		foreach ($this->registeredCreateHooks as $colName => $fn) {
418
-			// @TODO: Would it be better to pass the Query to the function?
419
-			$fn();
420
-		}
421
-
422
-		try {
423
-			(new Query($this->getPdo(), $this->getTableName()))
424
-				->insert($this->getActiveRecordColumns())
425
-				->execute();
426
-
427
-			$this->setId(intval($this->getPdo()->lastInsertId()));
428
-		} catch (\PDOException $e) {
429
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
430
-		}
431
-
432
-		return $this;
433
-	}
434
-
435
-	/**
436
-	 * {@inheritdoc}
437
-	 */
438
-	public function read($id)
439
-	{
440
-		foreach ($this->registeredReadHooks as $colName => $fn) {
441
-			// @TODO: Would it be better to pass the Query to the function?
442
-			$fn();
443
-		}
444
-
445
-		try {
446
-			$row = (new Query($this->getPdo(), $this->getTableName()))
447
-				->select()
448
-				->where(Query::Equal('id', $id))
449
-				->execute()
450
-				->fetch();
451
-
452
-			if ($row === false) {
453
-				throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));
454
-			}
455
-
456
-			$this->fill($row)->setId($id);
457
-		} catch (\PDOException $e) {
458
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
459
-		}
460
-
461
-		return $this;
462
-	}
463
-
464
-	/**
465
-	 * {@inheritdoc}
466
-	 */
467
-	public function update()
468
-	{
469
-		foreach ($this->registeredUpdateHooks as $colName => $fn) {
470
-			// @TODO: Would it be better to pass the Query to the function?
471
-			$fn();
472
-		}
473
-
474
-		try {
475
-			(new Query($this->getPdo(), $this->getTableName()))
476
-				->update($this->getActiveRecordColumns())
477
-				->where(Query::Equal('id', $this->getId()))
478
-				->execute();
479
-		} catch (\PDOException $e) {
480
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
481
-		}
482
-
483
-		return $this;
484
-	}
485
-
486
-	/**
487
-	 * {@inheritdoc}
488
-	 */
489
-	public function delete()
490
-	{
491
-		foreach ($this->registeredDeleteHooks as $colName => $fn) {
492
-			// @TODO: Would it be better to pass the Query to the function?
493
-			$fn();
494
-		}
495
-
496
-		try {
497
-			(new Query($this->getPdo(), $this->getTableName()))
498
-				->delete()
499
-				->where(Query::Equal('id', $this->getId()))
500
-				->execute();
501
-
502
-			$this->setId(null);
503
-		} catch (\PDOException $e) {
504
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
505
-		}
506
-
507
-		return $this;
508
-	}
509
-
510
-	/**
511
-	 * {@inheritdoc}
512
-	 */
513
-	public function sync()
514
-	{
515
-		if (!$this->exists()) {
516
-			return $this->create();
517
-		}
518
-
519
-		return $this->update();
520
-	}
521
-
522
-	/**
523
-	 * {@inheritdoc}
524
-	 */
525
-	public function exists()
526
-	{
527
-		return $this->getId() !== null;
528
-	}
529
-
530
-	/**
531
-	 * {@inheritdoc}
532
-	 */
533
-	public function fill(array $attributes)
534
-	{
535
-		$columns = $this->getActiveRecordColumns();
536
-		$columns['id'] = &$this->id;
537
-
538
-		foreach ($attributes as $key => $value) {
539
-			if (array_key_exists($key, $columns)) {
540
-				$columns[$key] = $value;
541
-			}
542
-		}
543
-
544
-		return $this;
545
-	}
546
-
547
-	/**
548
-	 * {@inheritdoc}
549
-	 */
550
-	public function search(array $ignoredTraits = [])
551
-	{
552
-		$clauses = [];
553
-		foreach ($this->registeredSearchHooks as $column => $fn) {
554
-			if (!in_array($column, $ignoredTraits)) {
555
-				$clauses[] = $fn();
556
-			}
557
-		}
558
-
559
-		return new ActiveRecordQuery($this, $this->getTableName(), $clauses);
560
-	}
561
-
562
-	/**
563
-	 * Returns the PDO.
564
-	 *
565
-	 * @return \PDO the PDO.
566
-	 */
567
-	public function getPdo()
568
-	{
569
-		return $this->pdo;
570
-	}
571
-
572
-	/**
573
-	 * Set the PDO.
574
-	 *
575
-	 * @param \PDO $pdo
576
-	 * @return $this
577
-	 */
578
-	protected function setPdo($pdo)
579
-	{
580
-		$this->pdo = $pdo;
581
-
582
-		return $this;
583
-	}
584
-
585
-	/**
586
-	 * Returns the ID.
587
-	 *
588
-	 * @return null|int The ID.
589
-	 */
590
-	public function getId()
591
-	{
592
-		return $this->id;
593
-	}
594
-
595
-	/**
596
-	 * Set the ID.
597
-	 *
598
-	 * @param int $id
599
-	 * @return $this
600
-	 */
601
-	protected function setId($id)
602
-	{
603
-		$this->id = $id;
604
-
605
-		return $this;
606
-	}
607
-
608
-
609
-	public function newInstance()
610
-	{
611
-		return new static($this->pdo);
612
-	}
613
-
614
-	/**
615
-	 * Returns the active record table.
616
-	 *
617
-	 * @return string the active record table name.
618
-	 */
619
-	abstract protected function getTableName();
620
-
621
-	/**
622
-	 * Returns the active record columns.
623
-	 *
624
-	 * @return array the active record columns.
625
-	 */
626
-	abstract protected function getTableDefinition();
372
+        return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn);
373
+    }
374
+
375
+    /**
376
+     * Iterates over the specified constraints in the table definition, 
377
+     * 		and applies these to the database.
378
+     */
379
+    public function createTableConstraints()
380
+    {
381
+        // Iterate over columns, check whether "relation" field exists, if so create constraint
382
+        foreach ($this->tableDefinition as $colName => $definition) {
383
+            if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) {
384
+                // Forge new relation
385
+                $target = $definition['relation'];
386
+                $constraintSql = $this->buildConstraint($target->getTableName(), 'id', $this->getTableName(), $colName);
387
+
388
+                $this->pdo->query($constraintSql);
389
+            }
390
+        }
391
+    }
392
+
393
+    /**
394
+     * Returns the name -> variable mapping for the table definition.
395
+     * @return Array The mapping
396
+     */
397
+    protected function getActiveRecordColumns()
398
+    {
399
+        $bindings = [];
400
+        foreach ($this->tableDefinition as $colName => $definition) {
401
+
402
+            // Ignore the id column (key) when inserting or updating
403
+            if ($colName == self::COLUMN_NAME_ID) {
404
+                continue;
405
+            }
406
+
407
+            $bindings[$colName] = &$definition['value'];
408
+        }
409
+        return $bindings;
410
+    }
411
+
412
+    /**
413
+     * {@inheritdoc}
414
+     */
415
+    public function create()
416
+    {
417
+        foreach ($this->registeredCreateHooks as $colName => $fn) {
418
+            // @TODO: Would it be better to pass the Query to the function?
419
+            $fn();
420
+        }
421
+
422
+        try {
423
+            (new Query($this->getPdo(), $this->getTableName()))
424
+                ->insert($this->getActiveRecordColumns())
425
+                ->execute();
426
+
427
+            $this->setId(intval($this->getPdo()->lastInsertId()));
428
+        } catch (\PDOException $e) {
429
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
430
+        }
431
+
432
+        return $this;
433
+    }
434
+
435
+    /**
436
+     * {@inheritdoc}
437
+     */
438
+    public function read($id)
439
+    {
440
+        foreach ($this->registeredReadHooks as $colName => $fn) {
441
+            // @TODO: Would it be better to pass the Query to the function?
442
+            $fn();
443
+        }
444
+
445
+        try {
446
+            $row = (new Query($this->getPdo(), $this->getTableName()))
447
+                ->select()
448
+                ->where(Query::Equal('id', $id))
449
+                ->execute()
450
+                ->fetch();
451
+
452
+            if ($row === false) {
453
+                throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getTableName()));
454
+            }
455
+
456
+            $this->fill($row)->setId($id);
457
+        } catch (\PDOException $e) {
458
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
459
+        }
460
+
461
+        return $this;
462
+    }
463
+
464
+    /**
465
+     * {@inheritdoc}
466
+     */
467
+    public function update()
468
+    {
469
+        foreach ($this->registeredUpdateHooks as $colName => $fn) {
470
+            // @TODO: Would it be better to pass the Query to the function?
471
+            $fn();
472
+        }
473
+
474
+        try {
475
+            (new Query($this->getPdo(), $this->getTableName()))
476
+                ->update($this->getActiveRecordColumns())
477
+                ->where(Query::Equal('id', $this->getId()))
478
+                ->execute();
479
+        } catch (\PDOException $e) {
480
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
481
+        }
482
+
483
+        return $this;
484
+    }
485
+
486
+    /**
487
+     * {@inheritdoc}
488
+     */
489
+    public function delete()
490
+    {
491
+        foreach ($this->registeredDeleteHooks as $colName => $fn) {
492
+            // @TODO: Would it be better to pass the Query to the function?
493
+            $fn();
494
+        }
495
+
496
+        try {
497
+            (new Query($this->getPdo(), $this->getTableName()))
498
+                ->delete()
499
+                ->where(Query::Equal('id', $this->getId()))
500
+                ->execute();
501
+
502
+            $this->setId(null);
503
+        } catch (\PDOException $e) {
504
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
505
+        }
506
+
507
+        return $this;
508
+    }
509
+
510
+    /**
511
+     * {@inheritdoc}
512
+     */
513
+    public function sync()
514
+    {
515
+        if (!$this->exists()) {
516
+            return $this->create();
517
+        }
518
+
519
+        return $this->update();
520
+    }
521
+
522
+    /**
523
+     * {@inheritdoc}
524
+     */
525
+    public function exists()
526
+    {
527
+        return $this->getId() !== null;
528
+    }
529
+
530
+    /**
531
+     * {@inheritdoc}
532
+     */
533
+    public function fill(array $attributes)
534
+    {
535
+        $columns = $this->getActiveRecordColumns();
536
+        $columns['id'] = &$this->id;
537
+
538
+        foreach ($attributes as $key => $value) {
539
+            if (array_key_exists($key, $columns)) {
540
+                $columns[$key] = $value;
541
+            }
542
+        }
543
+
544
+        return $this;
545
+    }
546
+
547
+    /**
548
+     * {@inheritdoc}
549
+     */
550
+    public function search(array $ignoredTraits = [])
551
+    {
552
+        $clauses = [];
553
+        foreach ($this->registeredSearchHooks as $column => $fn) {
554
+            if (!in_array($column, $ignoredTraits)) {
555
+                $clauses[] = $fn();
556
+            }
557
+        }
558
+
559
+        return new ActiveRecordQuery($this, $this->getTableName(), $clauses);
560
+    }
561
+
562
+    /**
563
+     * Returns the PDO.
564
+     *
565
+     * @return \PDO the PDO.
566
+     */
567
+    public function getPdo()
568
+    {
569
+        return $this->pdo;
570
+    }
571
+
572
+    /**
573
+     * Set the PDO.
574
+     *
575
+     * @param \PDO $pdo
576
+     * @return $this
577
+     */
578
+    protected function setPdo($pdo)
579
+    {
580
+        $this->pdo = $pdo;
581
+
582
+        return $this;
583
+    }
584
+
585
+    /**
586
+     * Returns the ID.
587
+     *
588
+     * @return null|int The ID.
589
+     */
590
+    public function getId()
591
+    {
592
+        return $this->id;
593
+    }
594
+
595
+    /**
596
+     * Set the ID.
597
+     *
598
+     * @param int $id
599
+     * @return $this
600
+     */
601
+    protected function setId($id)
602
+    {
603
+        $this->id = $id;
604
+
605
+        return $this;
606
+    }
607
+
608
+
609
+    public function newInstance()
610
+    {
611
+        return new static($this->pdo);
612
+    }
613
+
614
+    /**
615
+     * Returns the active record table.
616
+     *
617
+     * @return string the active record table name.
618
+     */
619
+    abstract protected function getTableName();
620
+
621
+    /**
622
+     * Returns the active record columns.
623
+     *
624
+     * @return array the active record columns.
625
+     */
626
+    abstract protected function getTableDefinition();
627 627
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -50,7 +50,7 @@  discard block
 block discarded – undo
50 50
 	 *
51 51
 	 * @param \PDO $pdo
52 52
 	 */
53
-	public function __construct(\PDO $pdo )
53
+	public function __construct(\PDO $pdo)
54 54
 	{
55 55
 		$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
56 56
 		$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
@@ -266,7 +266,7 @@  discard block
 block discarded – undo
266 266
 		}
267 267
 
268 268
 		if ($default !== NULL) {
269
-			$stmnt .= ' DEFAULT ' . $default . ' ';
269
+			$stmnt .= ' DEFAULT '.$default.' ';
270 270
 		}
271 271
 
272 272
 		if ($properties & ColumnProperty::AUTO_INCREMENT) {
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 		// Sort table (first column is id, the remaining are alphabetically sorted)
337 337
 		$columnStatements = $this->sortColumnStatements($columnStatements);
338 338
 
339
-		$sql = 'CREATE TABLE ' . $this->getTableName() . ' ';
339
+		$sql = 'CREATE TABLE '.$this->getTableName().' ';
340 340
 		$sql .= "(\n";
341 341
 		$sql .= implode(",\n", $columnStatements);
342 342
 		$sql .= "\n);";
Please login to merge, or discard this patch.
src/ActiveRecordQuery.php 1 patch
Indentation   +157 added lines, -157 removed lines patch added patch discarded remove patch
@@ -21,166 +21,166 @@
 block discarded – undo
21 21
 class ActiveRecordQuery implements \IteratorAggregate
22 22
 {
23 23
 
24
-	private $clauses = [];
24
+    private $clauses = [];
25 25
 
26
-	private $query;
26
+    private $query;
27 27
 
28
-	private $results;
28
+    private $results;
29 29
 
30
-	private $table;
30
+    private $table;
31 31
 
32
-	private $type;
32
+    private $type;
33 33
 	
34
-	private $whereExpression = null;
35
-
36
-	public function __construct(AbstractActiveRecord $instance, $table, Array $additionalWhereClauses)
37
-	{
38
-		$this->table = $table;
39
-		$this->query = new Query($instance->getPdo(), $table);
40
-		$this->type = $instance;
41
-		$this->clauses = $additionalWhereClauses;
42
-	}
43
-
44
-	private function execute()
45
-	{
46
-		$clauses = $this->clauses;
47
-
48
-		// Optionally add user concatenated where expression
49
-		if ($this->whereExpression !== null)
50
-		{
51
-			$clauses[] = $this->whereExpression;
52
-		}
53
-
54
-		// Construct where clause
55
-		if (count($clauses) == 1)
56
-		{
57
-			$this->query->where($clauses[0]);
58
-		} else if (count($clauses) >= 2)
59
-		{
60
-			$rest = array_slice($clauses, 1);
61
-			$this->query->where(Query::And($clauses[0], ...$rest));
62
-		}
63
-
64
-		$this->query->select();
65
-
66
-		$this->results = $this->query->execute();
67
-
68
-		return $this;
69
-	}
70
-
71
-	public function getIterator()
72
-	{
73
-		return new \ArrayIterator($this->fetchAll());
74
-	}
75
-
76
-	public function fetchAll()
77
-	{
78
-		try {
79
-			$this->execute();
80
-
81
-			$entries = $this->results->fetchAll();
82
-			if ($entries === false) {
83
-				throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->table));
84
-			}
85
-
86
-			$typedResults = [];
87
-
88
-			foreach ($entries as $entry) {
89
-				$typedEntry = $this->type->newInstance();
90
-				$typedEntry->fill($entry);
91
-				$typedResults[] = $typedEntry;
92
-			}
93
-
94
-			return $typedResults;
95
-		} catch (\PDOException $e) {
96
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
97
-		}
98
-	}
99
-
100
-	public function fetch()
101
-	{
102
-		try {
103
-			$this->execute();
104
-
105
-			$typedResult = $this->type->newInstance();
106
-
107
-			$entry = $this->results->fetch();
108
-			if ($entry === false) {
109
-				throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->table));
110
-			}
111
-
112
-			$typedResult->fill($entry);
113
-
114
-			return $typedResult;
115
-		} catch (\PDOException $e) {
116
-			throw new ActiveRecordException($e->getMessage(), 0, $e);
117
-		}
118
-	}
119
-
120
-
121
-	/**
122
-	 * Set the where condition
123
-	 *
124
-	 * @param QueryExpression $expression the query expression
125
-	 * @return $this
126
-	 * @see https://en.wikipedia.org/wiki/SQL#Operators
127
-	 * @see https://en.wikipedia.org/wiki/Where_(SQL)
128
-	 */
129
-	public function where(QueryExpression $expression)
130
-	{
131
-		$this->whereExpression = $expression;
132
-		return $this;
133
-	}
134
-
135
-	/**
136
-	 * Set an additional group by.
137
-	 *
138
-	 * @param string $column
139
-	 * @return $this
140
-	 * @see https://en.wikipedia.org/wiki/SQL#Queries
141
-	 */
142
-	public function groupBy($column)
143
-	{
144
-		$this->query->groupBy($column);
145
-		return $this;
146
-	}
147
-
148
-	/**
149
-	 * Set an additional order condition.
150
-	 *
151
-	 * @param string $column
152
-	 * @param string|null $order
153
-	 * @return $this
154
-	 * @see https://en.wikipedia.org/wiki/SQL#Queries
155
-	 * @see https://en.wikipedia.org/wiki/Order_by
156
-	 */
157
-	public function orderBy($column, $order = null)
158
-	{
159
-		$this->query->orderBy($column, $order);	
160
-		return $this;
161
-	}
162
-
163
-	/**
164
-	 * Set the limit.
165
-	 *
166
-	 * @param mixed $limit
167
-	 * @return $this
168
-	 */
169
-	public function limit($limit)
170
-	{
171
-		$this->query->limit($limit);
172
-		return $this;
173
-	}
174
-
175
-	/**
176
-	 * Set the offset.
177
-	 *
178
-	 * @param mixed $offset
179
- 	 * @return $this
180
-	 */
181
-	public function offset($offset)
182
-	{
183
-		$this->query->offset($offset);
184
-		return $this;
185
-	}
34
+    private $whereExpression = null;
35
+
36
+    public function __construct(AbstractActiveRecord $instance, $table, Array $additionalWhereClauses)
37
+    {
38
+        $this->table = $table;
39
+        $this->query = new Query($instance->getPdo(), $table);
40
+        $this->type = $instance;
41
+        $this->clauses = $additionalWhereClauses;
42
+    }
43
+
44
+    private function execute()
45
+    {
46
+        $clauses = $this->clauses;
47
+
48
+        // Optionally add user concatenated where expression
49
+        if ($this->whereExpression !== null)
50
+        {
51
+            $clauses[] = $this->whereExpression;
52
+        }
53
+
54
+        // Construct where clause
55
+        if (count($clauses) == 1)
56
+        {
57
+            $this->query->where($clauses[0]);
58
+        } else if (count($clauses) >= 2)
59
+        {
60
+            $rest = array_slice($clauses, 1);
61
+            $this->query->where(Query::And($clauses[0], ...$rest));
62
+        }
63
+
64
+        $this->query->select();
65
+
66
+        $this->results = $this->query->execute();
67
+
68
+        return $this;
69
+    }
70
+
71
+    public function getIterator()
72
+    {
73
+        return new \ArrayIterator($this->fetchAll());
74
+    }
75
+
76
+    public function fetchAll()
77
+    {
78
+        try {
79
+            $this->execute();
80
+
81
+            $entries = $this->results->fetchAll();
82
+            if ($entries === false) {
83
+                throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->table));
84
+            }
85
+
86
+            $typedResults = [];
87
+
88
+            foreach ($entries as $entry) {
89
+                $typedEntry = $this->type->newInstance();
90
+                $typedEntry->fill($entry);
91
+                $typedResults[] = $typedEntry;
92
+            }
93
+
94
+            return $typedResults;
95
+        } catch (\PDOException $e) {
96
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
97
+        }
98
+    }
99
+
100
+    public function fetch()
101
+    {
102
+        try {
103
+            $this->execute();
104
+
105
+            $typedResult = $this->type->newInstance();
106
+
107
+            $entry = $this->results->fetch();
108
+            if ($entry === false) {
109
+                throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->table));
110
+            }
111
+
112
+            $typedResult->fill($entry);
113
+
114
+            return $typedResult;
115
+        } catch (\PDOException $e) {
116
+            throw new ActiveRecordException($e->getMessage(), 0, $e);
117
+        }
118
+    }
119
+
120
+
121
+    /**
122
+     * Set the where condition
123
+     *
124
+     * @param QueryExpression $expression the query expression
125
+     * @return $this
126
+     * @see https://en.wikipedia.org/wiki/SQL#Operators
127
+     * @see https://en.wikipedia.org/wiki/Where_(SQL)
128
+     */
129
+    public function where(QueryExpression $expression)
130
+    {
131
+        $this->whereExpression = $expression;
132
+        return $this;
133
+    }
134
+
135
+    /**
136
+     * Set an additional group by.
137
+     *
138
+     * @param string $column
139
+     * @return $this
140
+     * @see https://en.wikipedia.org/wiki/SQL#Queries
141
+     */
142
+    public function groupBy($column)
143
+    {
144
+        $this->query->groupBy($column);
145
+        return $this;
146
+    }
147
+
148
+    /**
149
+     * Set an additional order condition.
150
+     *
151
+     * @param string $column
152
+     * @param string|null $order
153
+     * @return $this
154
+     * @see https://en.wikipedia.org/wiki/SQL#Queries
155
+     * @see https://en.wikipedia.org/wiki/Order_by
156
+     */
157
+    public function orderBy($column, $order = null)
158
+    {
159
+        $this->query->orderBy($column, $order);	
160
+        return $this;
161
+    }
162
+
163
+    /**
164
+     * Set the limit.
165
+     *
166
+     * @param mixed $limit
167
+     * @return $this
168
+     */
169
+    public function limit($limit)
170
+    {
171
+        $this->query->limit($limit);
172
+        return $this;
173
+    }
174
+
175
+    /**
176
+     * Set the offset.
177
+     *
178
+     * @param mixed $offset
179
+     * @return $this
180
+     */
181
+    public function offset($offset)
182
+    {
183
+        $this->query->offset($offset);
184
+        return $this;
185
+    }
186 186
 }
Please login to merge, or discard this patch.