1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the miBadger package. |
5
|
|
|
* |
6
|
|
|
* @author Michael Webbers <[email protected]> |
7
|
|
|
* @license http://opensource.org/licenses/Apache-2.0 Apache v2 License |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace miBadger\ActiveRecord; |
11
|
|
|
|
12
|
|
|
use miBadger\Query\Query; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* The abstract active record class. |
16
|
|
|
* |
17
|
|
|
* @since 1.0.0 |
18
|
|
|
*/ |
19
|
|
|
abstract class AbstractActiveRecord implements ActiveRecordInterface |
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
|
71 |
|
public function __construct(\PDO $pdo) |
54
|
|
|
{ |
55
|
71 |
|
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); |
56
|
71 |
|
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); |
57
|
|
|
|
58
|
71 |
|
$this->setPdo($pdo); |
59
|
71 |
|
$this->tableDefinition = $this->getActiveRecordTableDefinition(); |
60
|
71 |
|
$this->registeredCreateHooks = []; |
61
|
71 |
|
$this->registeredReadHooks = []; |
62
|
71 |
|
$this->registeredUpdateHooks = []; |
63
|
71 |
|
$this->registeredDeleteHooks = []; |
64
|
71 |
|
$this->registeredSearchHooks = []; |
65
|
|
|
|
66
|
|
|
// Extend table definition with default ID field, throw exception if field already exists |
67
|
71 |
|
if (array_key_exists('id', $this->tableDefinition)) { |
68
|
|
|
$message = "Table definition in record contains a field with name \"id\""; |
69
|
|
|
$message .= ", which is a reserved name by ActiveRecord"; |
70
|
|
|
throw new ActiveRecordException($message, 0); |
71
|
|
|
} |
72
|
|
|
|
73
|
71 |
|
$this->tableDefinition[self::COLUMN_NAME_ID] = |
74
|
|
|
[ |
75
|
71 |
|
'value' => &$this->id, |
76
|
|
|
'validate' => null, |
77
|
71 |
|
'type' => self::COLUMN_TYPE_ID, |
78
|
71 |
|
'properties' => ColumnProperty::NOT_NULL | ColumnProperty::IMMUTABLE | ColumnProperty::AUTO_INCREMENT | ColumnProperty::PRIMARY_KEY |
79
|
|
|
]; |
80
|
71 |
|
} |
81
|
|
|
|
82
|
28 |
|
private function checkHookConstraints($columnName, $hookMap) |
83
|
|
|
{ |
84
|
|
|
// Check whether column exists |
85
|
28 |
|
if (!array_key_exists($columnName, $this->tableDefinition)) |
86
|
|
|
{ |
87
|
5 |
|
throw new ActiveRecordException("Hook is trying to register on non-existing column \"$columnName\"", 0); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
// Enforcing 1 hook per table column |
91
|
23 |
View Code Duplication |
if (array_key_exists($columnName, $hookMap)) { |
|
|
|
|
92
|
5 |
|
$message = "Hook is trying to register on an already registered column \"$columnName\", "; |
93
|
5 |
|
$message .= "do you have conflicting traits?"; |
94
|
5 |
|
throw new ActiveRecordException($message, 0); |
95
|
|
|
} |
96
|
23 |
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Register a new hook for a specific column that gets called before execution of the create() method |
100
|
|
|
* Only one hook per column can be registered at a time |
101
|
|
|
* @param string $columnName The name of the column that is registered. |
102
|
|
|
* @param string|callable $fn Either a callable, or the name of a method on the inheriting object. |
103
|
|
|
*/ |
104
|
6 |
View Code Duplication |
public function registerCreateHook($columnName, $fn) |
|
|
|
|
105
|
|
|
{ |
106
|
6 |
|
$this->checkHookConstraints($columnName, $this->registeredCreateHooks); |
107
|
|
|
|
108
|
5 |
|
if (is_string($fn) && is_callable([$this, $fn])) { |
109
|
3 |
|
$this->registeredCreateHooks[$columnName] = [$this, $fn]; |
110
|
2 |
|
} else if (is_callable($fn)) { |
111
|
2 |
|
$this->registeredCreateHooks[$columnName] = $fn; |
112
|
|
|
} else { |
113
|
|
|
throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0); |
114
|
|
|
} |
115
|
5 |
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Register a new hook for a specific column that gets called before execution of the read() method |
119
|
|
|
* Only one hook per column can be registered at a time |
120
|
|
|
* @param string $columnName The name of the column that is registered. |
121
|
|
|
* @param string|callable $fn Either a callable, or the name of a method on the inheriting object. |
122
|
|
|
*/ |
123
|
3 |
View Code Duplication |
public function registerReadHook($columnName, $fn) |
|
|
|
|
124
|
|
|
{ |
125
|
3 |
|
$this->checkHookConstraints($columnName, $this->registeredReadHooks); |
126
|
|
|
|
127
|
2 |
|
if (is_string($fn) && is_callable([$this, $fn])) { |
128
|
|
|
$this->registeredReadHooks[$columnName] = [$this, $fn]; |
129
|
2 |
|
} else if (is_callable($fn)) { |
130
|
2 |
|
$this->registeredReadHooks[$columnName] = $fn; |
131
|
|
|
} else { |
132
|
|
|
throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0); |
133
|
|
|
} |
134
|
2 |
|
} |
135
|
|
|
|
136
|
|
|
|
137
|
|
|
|
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Register a new hook for a specific column that gets called before execution of the update() method |
141
|
|
|
* Only one hook per column can be registered at a time |
142
|
|
|
* @param string $columnName The name of the column that is registered. |
143
|
|
|
* @param string|callable $fn Either a callable, or the name of a method on the inheriting object. |
144
|
|
|
*/ |
145
|
6 |
View Code Duplication |
public function registerUpdateHook($columnName, $fn) |
|
|
|
|
146
|
|
|
{ |
147
|
6 |
|
$this->checkHookConstraints($columnName, $this->registeredUpdateHooks); |
148
|
|
|
|
149
|
5 |
|
if (is_string($fn) && is_callable([$this, $fn])) { |
150
|
3 |
|
$this->registeredUpdateHooks[$columnName] = [$this, $fn]; |
151
|
2 |
|
} else if (is_callable($fn)) { |
152
|
2 |
|
$this->registeredUpdateHooks[$columnName] = $fn; |
153
|
|
|
} else { |
154
|
|
|
throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0); |
155
|
|
|
} |
156
|
5 |
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Register a new hook for a specific column that gets called before execution of the delete() 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
|
3 |
View Code Duplication |
public function registerDeleteHook($columnName, $fn) |
|
|
|
|
165
|
|
|
{ |
166
|
3 |
|
$this->checkHookConstraints($columnName, $this->registeredDeleteHooks); |
167
|
|
|
|
168
|
2 |
|
if (is_string($fn) && is_callable([$this, $fn])) { |
169
|
|
|
$this->registeredDeleteHooks[$columnName] = [$this, $fn]; |
170
|
2 |
|
} else if (is_callable($fn)) { |
171
|
2 |
|
$this->registeredDeleteHooks[$columnName] = $fn; |
172
|
|
|
} else { |
173
|
|
|
throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0); |
174
|
|
|
} |
175
|
2 |
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Register a new hook for a specific column that gets called before execution of the search() method |
179
|
|
|
* Only one hook per column can be registered at a time |
180
|
|
|
* @param string $columnName The name of the column that is registered. |
181
|
|
|
* @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; |
182
|
|
|
*/ |
183
|
13 |
View Code Duplication |
public function registerSearchHook($columnName, $fn) |
|
|
|
|
184
|
|
|
{ |
185
|
13 |
|
$this->checkHookConstraints($columnName, $this->registeredSearchHooks); |
186
|
|
|
|
187
|
12 |
|
if (is_string($fn) && is_callable([$this, $fn])) { |
188
|
10 |
|
$this->registeredSearchHooks[$columnName] = [$this, $fn]; |
189
|
2 |
|
} else if (is_callable($fn)) { |
190
|
2 |
|
$this->registeredSearchHooks[$columnName] = $fn; |
191
|
|
|
} else { |
192
|
|
|
throw new ActiveRecordException("Provided hook on column \"$columnName\" is not callable", 0); |
193
|
|
|
} |
194
|
12 |
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Adds a new column definition to the table. |
198
|
|
|
* @param string $columnName The name of the column that is registered. |
199
|
|
|
* @param Array $definition The definition of that column. |
200
|
|
|
*/ |
201
|
38 |
|
public function extendTableDefinition($columnName, $definition) |
202
|
|
|
{ |
203
|
38 |
|
if ($this->tableDefinition === null) { |
204
|
|
|
throw new ActiveRecordException("tableDefinition is null, most likely due to parent class not having been initialized in constructor"); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
// Enforcing table can only be extended with new columns |
208
|
38 |
View Code Duplication |
if (array_key_exists($columnName, $this->tableDefinition)) { |
|
|
|
|
209
|
1 |
|
$message = "Table is being extended with a column that already exists, "; |
210
|
1 |
|
$message .= "\"$columnName\" conflicts with your table definition"; |
211
|
1 |
|
throw new ActiveRecordException($message, 0); |
212
|
|
|
} |
213
|
|
|
|
214
|
38 |
|
$this->tableDefinition[$columnName] = $definition; |
215
|
38 |
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Returns the type string as it should appear in the mysql create table statement for the given column |
219
|
|
|
* @return string The type string |
220
|
|
|
*/ |
221
|
22 |
|
private function getDatabaseTypeString($colName, $type, $length) |
222
|
|
|
{ |
223
|
22 |
|
if ($type === null) |
224
|
|
|
{ |
225
|
|
|
throw new ActiveRecordException(sprintf("Column %s has invalid type \"NULL\"", $colName)); |
226
|
|
|
} |
227
|
|
|
|
228
|
22 |
|
switch (strtoupper($type)) { |
229
|
22 |
|
case 'DATETIME': |
230
|
22 |
|
case 'DATE': |
231
|
22 |
|
case 'TIME': |
232
|
22 |
|
case 'TEXT': |
233
|
22 |
|
case 'INT UNSIGNED': |
234
|
22 |
|
return $type; |
235
|
|
|
|
236
|
21 |
|
case 'VARCHAR': |
237
|
21 |
|
if ($length === null) { |
238
|
|
|
throw new ActiveRecordException(sprintf("field type %s requires specified column field \"LENGTH\"", $colName)); |
239
|
|
|
} else { |
240
|
21 |
|
return sprintf('%s(%d)', $type, $length); |
241
|
|
|
} |
242
|
|
|
|
243
|
9 |
|
case 'INT': |
244
|
|
|
case 'TINYINT': |
245
|
|
|
case 'BIGINT': |
246
|
|
|
default: |
|
|
|
|
247
|
|
|
// @TODO(Default): throw exception, or implicitly assume that type is correct? (For when using SQL databases with different types) |
248
|
9 |
|
if ($length === null) { |
249
|
|
|
return $type; |
250
|
|
|
} else { |
251
|
9 |
|
return sprintf('%s(%d)', $type, $length); |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Builds the part of a MySQL create table statement that corresponds to the supplied column |
258
|
|
|
* @param string $colName Name of the database column |
259
|
|
|
* @param string $type The type of the string |
260
|
|
|
* @param int $properties The set of Column properties that apply to this column (See ColumnProperty for options) |
261
|
|
|
* @return string |
262
|
|
|
*/ |
263
|
22 |
|
private function buildCreateTableColumnEntry($colName, $type, $length, $properties, $default) |
264
|
|
|
{ |
265
|
|
|
|
266
|
22 |
|
$stmnt = sprintf('`%s` %s ', $colName, $this->getDatabaseTypeString($colName, $type, $length)); |
267
|
22 |
|
if ($properties & ColumnProperty::NOT_NULL) { |
268
|
22 |
|
$stmnt .= 'NOT NULL '; |
269
|
|
|
} else { |
270
|
22 |
|
$stmnt .= 'NULL '; |
271
|
|
|
} |
272
|
|
|
|
273
|
22 |
|
if ($default !== NULL) { |
274
|
15 |
|
$stmnt .= ' DEFAULT ' . $default . ' '; |
275
|
|
|
} |
276
|
|
|
|
277
|
22 |
|
if ($properties & ColumnProperty::AUTO_INCREMENT) { |
278
|
22 |
|
$stmnt .= 'AUTO_INCREMENT '; |
279
|
|
|
} |
280
|
|
|
|
281
|
22 |
|
if ($properties & ColumnProperty::UNIQUE) { |
282
|
8 |
|
$stmnt .= 'UNIQUE '; |
283
|
|
|
} |
284
|
|
|
|
285
|
22 |
|
if ($properties & ColumnProperty::PRIMARY_KEY) { |
286
|
22 |
|
$stmnt .= 'PRIMARY KEY '; |
287
|
|
|
} |
288
|
|
|
|
289
|
22 |
|
return $stmnt; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Sorts the column statement components in the order such that the id appears first, |
294
|
|
|
* followed by all other columns in alphabetical ascending order |
295
|
|
|
* @param Array $colStatements Array of column statements |
296
|
|
|
* @return Array |
297
|
|
|
*/ |
298
|
22 |
|
private function sortColumnStatements($colStatements) |
299
|
|
|
{ |
300
|
|
|
// Find ID statement and put it first |
301
|
22 |
|
$sortedStatements = []; |
302
|
|
|
|
303
|
22 |
|
$sortedStatements[] = $colStatements[self::COLUMN_NAME_ID]; |
304
|
22 |
|
unset($colStatements[self::COLUMN_NAME_ID]); |
305
|
|
|
|
306
|
|
|
// Sort remaining columns in alphabetical order |
307
|
22 |
|
$columns = array_keys($colStatements); |
308
|
22 |
|
sort($columns); |
309
|
22 |
|
foreach ($columns as $colName) { |
310
|
22 |
|
$sortedStatements[] = $colStatements[$colName]; |
311
|
|
|
} |
312
|
|
|
|
313
|
22 |
|
return $sortedStatements; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Builds the MySQL Create Table statement for the internal table definition |
318
|
|
|
* @return string |
319
|
|
|
*/ |
320
|
22 |
|
public function buildCreateTableSQL() |
321
|
|
|
{ |
322
|
22 |
|
$columnStatements = []; |
323
|
22 |
|
foreach ($this->tableDefinition as $colName => $definition) { |
324
|
|
|
// Destructure column definition |
325
|
22 |
|
$type = $definition['type'] ?? null; |
326
|
22 |
|
$default = $definition['default'] ?? null; |
327
|
22 |
|
$length = $definition['length'] ?? null; |
328
|
22 |
|
$properties = $definition['properties'] ?? null; |
329
|
|
|
|
330
|
22 |
|
if (isset($definition['relation']) && $type !== null) { |
331
|
|
|
$msg = "Column \"$colName\": "; |
332
|
|
|
$msg .= "Relationship columns have an automatically inferred type, so type should be omitted"; |
333
|
|
|
throw new ActiveRecordException($msg); |
334
|
22 |
|
} else if (isset($definition['relation'])) { |
335
|
2 |
|
$type = self::COLUMN_TYPE_ID; |
336
|
|
|
} |
337
|
|
|
|
338
|
22 |
|
$columnStatements[$colName] = $this->buildCreateTableColumnEntry($colName, $type, $length, $properties, $default); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
// Sort table (first column is id, the remaining are alphabetically sorted) |
342
|
22 |
|
$columnStatements = $this->sortColumnStatements($columnStatements); |
343
|
|
|
|
344
|
22 |
|
$sql = 'CREATE TABLE ' . $this->getActiveRecordTable() . ' '; |
345
|
22 |
|
$sql .= "(\n"; |
346
|
22 |
|
$sql .= join($columnStatements, ",\n"); |
347
|
22 |
|
$sql .= "\n);"; |
348
|
|
|
|
349
|
22 |
|
return $sql; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* Creates the entity as a table in the database |
354
|
|
|
*/ |
355
|
22 |
|
public function createTable() |
356
|
|
|
{ |
357
|
22 |
|
$this->pdo->query($this->buildCreateTableSQL()); |
358
|
22 |
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* builds a MySQL constraint statement for the given parameters |
362
|
|
|
* @param string $parentTable |
363
|
|
|
* @param string $parentColumn |
364
|
|
|
* @param string $childTable |
365
|
|
|
* @param string $childColumn |
366
|
|
|
* @return string The MySQL table constraint string |
367
|
|
|
*/ |
368
|
4 |
|
protected function buildConstraint($parentTable, $parentColumn, $childTable, $childColumn) |
369
|
|
|
{ |
370
|
|
|
$template = <<<SQL |
371
|
4 |
|
ALTER TABLE `%s` |
372
|
|
|
ADD CONSTRAINT |
373
|
|
|
FOREIGN KEY (`%s`) |
374
|
|
|
REFERENCES `%s`(`%s`) |
375
|
|
|
ON DELETE CASCADE; |
376
|
|
|
SQL; |
377
|
4 |
|
return sprintf($template, $childTable, $childColumn, $parentTable, $parentColumn); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Iterates over the specified constraints in the table definition, |
382
|
|
|
* and applies these to the database. |
383
|
|
|
*/ |
384
|
1 |
|
public function createTableConstraints() |
385
|
|
|
{ |
386
|
|
|
// Iterate over columns, check whether "relation" field exists, if so create constraint |
387
|
1 |
|
foreach ($this->tableDefinition as $colName => $definition) { |
388
|
1 |
|
if (isset($definition['relation']) && $definition['relation'] instanceof AbstractActiveRecord) { |
389
|
|
|
// Forge new relation |
390
|
1 |
|
$target = $definition['relation']; |
391
|
1 |
|
$constraintSql = $this->buildConstraint($target->getActiveRecordTable(), 'id', $this->getActiveRecordTable(), $colName); |
392
|
|
|
|
393
|
1 |
|
$this->pdo->query($constraintSql); |
394
|
|
|
} |
395
|
|
|
} |
396
|
1 |
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Returns the name -> variable mapping for the table definition. |
400
|
|
|
* @return Array The mapping |
401
|
|
|
*/ |
402
|
44 |
|
protected function getActiveRecordColumns() |
403
|
|
|
{ |
404
|
44 |
|
$bindings = []; |
405
|
44 |
|
foreach ($this->tableDefinition as $colName => $definition) { |
406
|
|
|
|
407
|
|
|
// Ignore the id column (key) when inserting or updating |
408
|
44 |
|
if ($colName == self::COLUMN_NAME_ID) { |
409
|
44 |
|
continue; |
410
|
|
|
} |
411
|
|
|
|
412
|
44 |
|
$bindings[$colName] = &$definition['value']; |
413
|
|
|
} |
414
|
44 |
|
return $bindings; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* {@inheritdoc} |
419
|
|
|
*/ |
420
|
19 |
|
public function create() |
421
|
|
|
{ |
422
|
19 |
|
foreach ($this->registeredCreateHooks as $colName => $fn) { |
423
|
|
|
// @TODO: Would it be better to pass the Query to the function? |
424
|
3 |
|
$fn(); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
try { |
428
|
19 |
|
(new Query($this->getPdo(), $this->getActiveRecordTable())) |
429
|
19 |
|
->insert($this->getActiveRecordColumns()) |
430
|
19 |
|
->execute(); |
431
|
|
|
|
432
|
17 |
|
$this->setId(intval($this->getPdo()->lastInsertId())); |
433
|
2 |
|
} catch (\PDOException $e) { |
434
|
2 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
435
|
|
|
} |
436
|
|
|
|
437
|
17 |
|
return $this; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* {@inheritdoc} |
442
|
|
|
*/ |
443
|
17 |
|
public function read($id) |
444
|
|
|
{ |
445
|
17 |
|
foreach ($this->registeredReadHooks as $colName => $fn) { |
446
|
|
|
// @TODO: Would it be better to pass the Query to the function? |
447
|
1 |
|
$fn(); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
try { |
451
|
17 |
|
$row = (new Query($this->getPdo(), $this->getActiveRecordTable())) |
452
|
17 |
|
->select() |
453
|
17 |
|
->where('id', '=', $id) |
454
|
17 |
|
->execute() |
455
|
16 |
|
->fetch(); |
456
|
|
|
|
457
|
16 |
|
if ($row === false) { |
458
|
3 |
|
throw new ActiveRecordException(sprintf('Can not read the non-existent active record entry %d from the `%s` table.', $id, $this->getActiveRecordTable())); |
459
|
|
|
} |
460
|
|
|
|
461
|
13 |
|
$this->fill($row)->setId($id); |
462
|
4 |
|
} catch (\PDOException $e) { |
463
|
1 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
464
|
|
|
} |
465
|
|
|
|
466
|
13 |
|
return $this; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* {@inheritdoc} |
471
|
|
|
*/ |
472
|
8 |
View Code Duplication |
public function update() |
|
|
|
|
473
|
|
|
{ |
474
|
8 |
|
foreach ($this->registeredUpdateHooks as $colName => $fn) { |
475
|
|
|
// @TODO: Would it be better to pass the Query to the function? |
476
|
2 |
|
$fn(); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
try { |
480
|
8 |
|
(new Query($this->getPdo(), $this->getActiveRecordTable())) |
481
|
8 |
|
->update($this->getActiveRecordColumns()) |
482
|
8 |
|
->where('id', '=', $this->getId()) |
483
|
8 |
|
->execute(); |
484
|
2 |
|
} catch (\PDOException $e) { |
485
|
2 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
486
|
|
|
} |
487
|
|
|
|
488
|
6 |
|
return $this; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
/** |
492
|
|
|
* {@inheritdoc} |
493
|
|
|
*/ |
494
|
6 |
View Code Duplication |
public function delete() |
|
|
|
|
495
|
|
|
{ |
496
|
6 |
|
foreach ($this->registeredDeleteHooks as $colName => $fn) { |
497
|
|
|
// @TODO: Would it be better to pass the Query to the function? |
498
|
1 |
|
$fn(); |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
try { |
502
|
6 |
|
(new Query($this->getPdo(), $this->getActiveRecordTable())) |
503
|
6 |
|
->delete() |
504
|
6 |
|
->where('id', '=', $this->getId()) |
505
|
6 |
|
->execute(); |
506
|
|
|
|
507
|
5 |
|
$this->setId(null); |
508
|
1 |
|
} catch (\PDOException $e) { |
509
|
1 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
510
|
|
|
} |
511
|
|
|
|
512
|
5 |
|
return $this; |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
/** |
516
|
|
|
* {@inheritdoc} |
517
|
|
|
*/ |
518
|
2 |
|
public function sync() |
519
|
|
|
{ |
520
|
2 |
|
if (!$this->exists()) { |
521
|
1 |
|
return $this->create(); |
522
|
|
|
} |
523
|
|
|
|
524
|
1 |
|
return $this->update(); |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* {@inheritdoc} |
529
|
|
|
*/ |
530
|
3 |
|
public function exists() |
531
|
|
|
{ |
532
|
3 |
|
return $this->getId() !== null; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* {@inheritdoc} |
537
|
|
|
*/ |
538
|
26 |
|
public function fill(array $attributes) |
539
|
|
|
{ |
540
|
26 |
|
$columns = $this->getActiveRecordColumns(); |
541
|
26 |
|
$columns['id'] = &$this->id; |
542
|
|
|
|
543
|
26 |
|
foreach ($attributes as $key => $value) { |
544
|
26 |
|
if (array_key_exists($key, $columns)) { |
545
|
26 |
|
$columns[$key] = $value; |
546
|
|
|
} |
547
|
|
|
} |
548
|
|
|
|
549
|
26 |
|
return $this; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* {@inheritdoc} |
554
|
|
|
*/ |
555
|
3 |
|
public function searchOne(array $where = [], array $orderBy = []) |
556
|
|
|
{ |
557
|
|
|
try { |
558
|
3 |
|
$row = $this->getSearchQueryResult($where, $orderBy, 1)->fetch(); |
559
|
|
|
|
560
|
2 |
|
if ($row === false) { |
561
|
1 |
|
throw new ActiveRecordException(sprintf('Can not search one non-existent entry from the `%s` table.', $this->getActiveRecordTable())); |
562
|
|
|
} |
563
|
|
|
|
564
|
1 |
|
return $this->fill($row)->setId($row['id']); |
565
|
2 |
|
} catch (\PDOException $e) { |
566
|
1 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
567
|
|
|
} |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* {@inheritdoc} |
572
|
|
|
*/ |
573
|
13 |
|
public function search(array $where = [], array $orderBy = [], $limit = -1, $offset = 0) |
574
|
|
|
{ |
575
|
|
|
try { |
576
|
13 |
|
$queryResult = $this->getSearchQueryResult($where, $orderBy, $limit, $offset); |
577
|
11 |
|
$result = []; |
578
|
|
|
|
579
|
11 |
|
foreach ($queryResult as $row) { |
580
|
11 |
|
$new = clone $this; |
581
|
|
|
|
582
|
11 |
|
$result[] = $new->fill($row)->setId($row['id']); |
583
|
|
|
} |
584
|
|
|
|
585
|
11 |
|
return $result; |
586
|
2 |
|
} catch (\PDOException $e) { |
587
|
2 |
|
throw new ActiveRecordException($e->getMessage(), 0, $e); |
588
|
|
|
} |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* Returns the search query result with the given where, order by, limit and offset clauses. |
593
|
|
|
* |
594
|
|
|
* @param array $where = [] |
595
|
|
|
* @param array $orderBy = [] |
596
|
|
|
* @param int $limit = -1 |
597
|
|
|
* @param int $offset = 0 |
598
|
|
|
* @return \miBadger\Query\QueryResult the search query result with the given where, order by, limit and offset clauses. |
599
|
|
|
*/ |
600
|
16 |
|
private function getSearchQueryResult(array $where = [], array $orderBy = [], $limit = -1, $offset = 0) |
601
|
|
|
{ |
602
|
16 |
|
$query = (new Query($this->getPdo(), $this->getActiveRecordTable())) |
603
|
16 |
|
->select(); |
604
|
|
|
|
605
|
16 |
|
$this->getSearchQueryWhere($query, $where); |
606
|
16 |
|
$this->getSearchQueryOrderBy($query, $orderBy); |
607
|
16 |
|
$this->getSearchQueryLimit($query, $limit, $offset); |
608
|
|
|
|
609
|
|
|
// Ignore all trait modifiers for which a where clause was specified |
610
|
16 |
|
$registeredSearchHooks = $this->registeredSearchHooks; |
611
|
16 |
|
foreach ($where as $index => $clause) { |
612
|
9 |
|
$colName = $clause[0]; |
613
|
9 |
|
unset($registeredSearchHooks[$colName]); |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
// Allow traits to modify the query |
617
|
16 |
|
foreach ($registeredSearchHooks as $column => $searchFunction) { |
618
|
1 |
|
$searchFunction($query); |
619
|
|
|
} |
620
|
|
|
|
621
|
16 |
|
return $query->execute(); |
622
|
|
|
} |
623
|
|
|
|
624
|
|
|
/** |
625
|
|
|
* Returns the given query after adding the given where conditions. |
626
|
|
|
* |
627
|
|
|
* @param \miBadger\Query\Query $query |
628
|
|
|
* @param array $where |
629
|
|
|
* @return \miBadger\Query\Query the given query after adding the given where conditions. |
630
|
|
|
*/ |
631
|
16 |
|
private function getSearchQueryWhere($query, $where) |
632
|
|
|
{ |
633
|
16 |
|
foreach ($where as $key => $value) { |
634
|
9 |
|
$query->where($value[0], $value[1], $value[2]); |
635
|
|
|
} |
636
|
|
|
|
637
|
16 |
|
return $query; |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
/** |
641
|
|
|
* Returns the given query after adding the given order by conditions. |
642
|
|
|
* |
643
|
|
|
* @param \miBadger\Query\Query $query |
644
|
|
|
* @param array $orderBy |
645
|
|
|
* @return \miBadger\Query\Query the given query after adding the given order by conditions. |
646
|
|
|
*/ |
647
|
16 |
|
private function getSearchQueryOrderBy($query, $orderBy) |
648
|
|
|
{ |
649
|
16 |
|
foreach ($orderBy as $key => $value) { |
650
|
1 |
|
$query->orderBy($key, $value); |
651
|
|
|
} |
652
|
|
|
|
653
|
16 |
|
return $query; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* Returns the given query after adding the given limit and offset conditions. |
658
|
|
|
* |
659
|
|
|
* @param \miBadger\Query\Query $query |
660
|
|
|
* @param int $limit |
661
|
|
|
* @param int $offset |
662
|
|
|
* @return \miBadger\Query\Query the given query after adding the given limit and offset conditions. |
663
|
|
|
*/ |
664
|
16 |
|
private function getSearchQueryLimit($query, $limit, $offset) |
665
|
|
|
{ |
666
|
16 |
|
if ($limit > -1) { |
667
|
5 |
|
$query->limit($limit); |
668
|
5 |
|
$query->offset($offset); |
669
|
|
|
} |
670
|
|
|
|
671
|
16 |
|
return $query; |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* Returns the PDO. |
676
|
|
|
* |
677
|
|
|
* @return \PDO the PDO. |
678
|
|
|
*/ |
679
|
50 |
|
public function getPdo() |
680
|
|
|
{ |
681
|
50 |
|
return $this->pdo; |
682
|
|
|
} |
683
|
|
|
|
684
|
|
|
/** |
685
|
|
|
* Set the PDO. |
686
|
|
|
* |
687
|
|
|
* @param \PDO $pdo |
688
|
|
|
* @return $this |
689
|
|
|
*/ |
690
|
71 |
|
protected function setPdo($pdo) |
691
|
|
|
{ |
692
|
71 |
|
$this->pdo = $pdo; |
693
|
|
|
|
694
|
71 |
|
return $this; |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
/** |
698
|
|
|
* Returns the ID. |
699
|
|
|
* |
700
|
|
|
* @return null|int The ID. |
701
|
|
|
*/ |
702
|
21 |
|
public function getId() |
703
|
|
|
{ |
704
|
21 |
|
return $this->id; |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
/** |
708
|
|
|
* Set the ID. |
709
|
|
|
* |
710
|
|
|
* @param int $id |
711
|
|
|
* @return $this |
712
|
|
|
*/ |
713
|
39 |
|
protected function setId($id) |
714
|
|
|
{ |
715
|
39 |
|
$this->id = $id; |
716
|
|
|
|
717
|
39 |
|
return $this; |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
/** |
721
|
|
|
* Returns the active record table. |
722
|
|
|
* |
723
|
|
|
* @return string the active record table. |
724
|
|
|
*/ |
725
|
|
|
abstract protected function getActiveRecordTable(); |
726
|
|
|
|
727
|
|
|
/** |
728
|
|
|
* Returns the active record columns. |
729
|
|
|
* |
730
|
|
|
* @return array the active record columns. |
731
|
|
|
*/ |
732
|
|
|
abstract protected function getActiveRecordTableDefinition(); |
733
|
|
|
} |
734
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.