1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file contains the skeleton for all of the database objects |
4
|
|
|
* |
5
|
|
|
* @package BZiON\Models |
6
|
|
|
* @license https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3 |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* A base database object (e.g. A player or a team) |
11
|
|
|
* @package BZiON\Models |
12
|
|
|
*/ |
13
|
|
|
abstract class BaseModel implements ModelInterface |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* The Database ID of the object |
17
|
|
|
* @var int |
18
|
|
|
*/ |
19
|
|
|
protected $id; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* The name of the database table used for queries |
23
|
|
|
* @var string |
24
|
|
|
*/ |
25
|
|
|
protected $table; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* False if there isn't any row in the database representing |
29
|
|
|
* the requested object ID |
30
|
|
|
* @var bool |
31
|
|
|
*/ |
32
|
|
|
protected $valid; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* The status of this model in the database; active, deleted, etc. |
36
|
|
|
* |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
protected $status; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* The database variable used for queries |
43
|
|
|
* @var Database |
44
|
|
|
*/ |
45
|
|
|
protected $db; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Whether the lazy parameters of the model have been loaded |
49
|
|
|
* @var bool |
50
|
|
|
*/ |
51
|
|
|
protected $loaded = false; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* The default status value for deletable models |
55
|
|
|
*/ |
56
|
|
|
const DEFAULT_STATUS = 'active'; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* The name of the database table used for queries |
60
|
|
|
* You can use this constant in static methods as such: |
61
|
78 |
|
* static::TABLE |
62
|
|
|
*/ |
63
|
78 |
|
const TABLE = ""; |
64
|
2 |
|
|
65
|
|
|
/** |
66
|
|
|
* Get a Model based on its ID |
67
|
77 |
|
* |
68
|
|
|
* @param int|static $id The ID of the object to look for, or the object |
69
|
1 |
|
* itself |
70
|
|
|
* @throws InvalidArgumentException If $id is an object of an incorrect type |
71
|
|
|
* @return static |
72
|
76 |
|
*/ |
73
|
|
|
public static function get($id) |
74
|
76 |
|
{ |
75
|
|
|
if ($id instanceof static) { |
76
|
|
|
return $id; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
if (is_object($id)) { |
80
|
|
|
// Throw an exception if $id is an object of the incorrect class |
81
|
|
|
throw new InvalidArgumentException("The object provided is not of the correct type"); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
$id = (int) $id; |
85
|
|
|
|
86
|
|
|
return static::chooseModelFromDatabase($id); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Assign the MySQL result array to the individual properties of the model |
91
|
|
|
* |
92
|
|
|
* @param array $result MySQL's result array |
93
|
|
|
* @return null |
94
|
|
|
*/ |
95
|
|
|
abstract protected function assignResult($result); |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Fetch the columns of a model |
99
|
76 |
|
* |
100
|
|
|
* This method takes the ID of the object to look for and creates a |
101
|
76 |
|
* $this->db object which can be used to communicate with the database and |
102
|
76 |
|
* calls $this->assignResult() so that the child class can populate the |
103
|
|
|
* properties of the Model based on the database data |
104
|
76 |
|
* |
105
|
76 |
|
* If the $id is specified as 0, then an invalid object will be returned |
106
|
|
|
* |
107
|
76 |
|
* @param int $id The ID of the model |
108
|
|
|
* @param array|null $results The column values of the model, or NULL to |
109
|
|
|
* generate them using $this->fetchColumnValues() |
110
|
76 |
|
*/ |
111
|
|
|
protected function __construct($id, $results = null) |
112
|
76 |
|
{ |
113
|
76 |
|
$this->db = Database::getInstance(); |
114
|
|
|
$this->table = static::TABLE; |
115
|
|
|
|
116
|
76 |
|
if ($id == 0) { |
117
|
|
|
$this->valid = false; |
118
|
|
|
|
119
|
76 |
|
return; |
120
|
76 |
|
} |
121
|
|
|
|
122
|
76 |
|
$this->id = $id; |
123
|
|
|
|
124
|
|
|
if ($results == null) { |
125
|
|
|
$results = $this->fetchColumnValues($id); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
if ($results === null) { |
129
|
|
|
$this->valid = false; |
130
|
|
|
} else { |
131
|
|
|
$this->valid = true; |
132
|
76 |
|
$this->assignResult($results); |
133
|
|
|
} |
134
|
76 |
|
} |
135
|
76 |
|
|
136
|
|
|
/** |
137
|
|
|
* Update a database field |
138
|
|
|
* |
139
|
|
|
* @param string $name The name of the column |
140
|
|
|
* @param mixed $value The value to set the column to |
141
|
|
|
* |
142
|
|
|
* @return void |
143
|
|
|
*/ |
144
|
3 |
|
public function update($name, $value) |
145
|
|
|
{ |
146
|
3 |
|
$this->db->execute("UPDATE " . static::TABLE . " SET `$name` = ? WHERE id = ?", array($value, $this->id)); |
147
|
3 |
|
} |
148
|
3 |
|
|
149
|
|
|
/** |
150
|
|
|
* Delete the object |
151
|
|
|
* |
152
|
|
|
* Please note that this does not delete the object entirely from the database, |
153
|
76 |
|
* it only hides it from users. You should overload this function if your object |
154
|
|
|
* does not have a 'status' column which can be set to 'deleted'. |
155
|
76 |
|
*/ |
156
|
76 |
|
public function delete() |
157
|
|
|
{ |
158
|
|
|
$this->status = 'deleted'; |
159
|
|
|
$this->update('status', 'deleted'); |
160
|
|
|
} |
161
|
|
|
|
162
|
76 |
|
/** |
163
|
|
|
* Permanently delete the object from the database |
164
|
76 |
|
*/ |
165
|
|
|
public function wipe() |
166
|
|
|
{ |
167
|
|
|
$this->db->execute("DELETE FROM " . static::TABLE . " WHERE id = ?", array($this->id)); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
76 |
|
* If a model has been marked as deleted in the database, this'll go through the process of marking the model |
172
|
|
|
* "active" again. |
173
|
76 |
|
*/ |
174
|
|
|
public function restore() |
175
|
|
|
{ |
176
|
|
|
$this->status = static::DEFAULT_STATUS; |
177
|
|
|
$this->update('status', static::DEFAULT_STATUS); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Get an object's database ID |
182
|
76 |
|
* @return int The ID |
183
|
|
|
*/ |
184
|
76 |
|
public function getId() |
185
|
|
|
{ |
186
|
|
|
return $this->id; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* See if an object is valid |
191
|
|
|
* @return bool |
192
|
|
|
*/ |
193
|
76 |
|
public function isValid() |
194
|
|
|
{ |
195
|
76 |
|
return $this->valid; |
196
|
76 |
|
} |
197
|
|
|
|
198
|
76 |
|
/** |
199
|
76 |
|
* Fetch a model based on its ID, useful for abstract model classes |
200
|
|
|
* |
201
|
76 |
|
* @param int $id The ID of the model |
202
|
|
|
* @return Model |
203
|
|
|
*/ |
204
|
|
|
protected static function chooseModelFromDatabase($id) |
205
|
76 |
|
{ |
206
|
|
|
return new static($id); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Query the database to get the eager column values for the Model |
211
|
|
|
* |
212
|
|
|
* @param $id int The ID of the model to fetch |
213
|
|
|
* @return array|null The results or null if a model wasn't found |
214
|
|
|
*/ |
215
|
|
|
protected static function fetchColumnValues($id) |
216
|
|
|
{ |
217
|
1 |
|
$table = static::TABLE; |
218
|
|
|
$columns = static::getEagerColumns(); |
219
|
1 |
|
|
220
|
1 |
|
$results = Database::getInstance() |
221
|
|
|
->query("SELECT $columns FROM $table WHERE id = ? LIMIT 1", array($id)); |
222
|
1 |
|
|
223
|
1 |
|
if (count($results) < 1) { |
224
|
|
|
return null; |
225
|
|
|
} |
226
|
1 |
|
|
227
|
|
|
return $results[0]; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Counts the elements of the database that match a specific query |
232
|
|
|
* |
233
|
|
|
* @param string $additional_query The MySQL query string (e.g. `WHERE id = ?`) |
234
|
|
|
* @param array $params The parameter values that will be passed to Database::query() |
235
|
26 |
|
* @param string $table The database table that will be searched, defaults to the model's table |
236
|
|
|
* @param string $column Only count the entries where `$column` is not `NULL` (or all if `$column` is `*`) |
237
|
26 |
|
* @return int |
238
|
|
|
*/ |
239
|
|
|
protected static function fetchCount($additional_query = '', $params = array(), $table = '', $column = '*') |
240
|
26 |
|
{ |
241
|
|
|
$table = (empty($table)) ? static::TABLE : $table; |
242
|
|
|
$db = Database::getInstance(); |
243
|
|
|
|
244
|
|
|
$result = $db->query("SELECT COUNT($column) AS count FROM $table $additional_query", |
245
|
|
|
$params |
246
|
|
|
); |
247
|
|
|
|
248
|
|
|
return $result[0]['count']; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Gets the id of a database row which has a specific value on a column |
253
|
76 |
|
* @param string $value The value which the column should be equal to |
254
|
|
|
* @param string $column The name of the database column |
255
|
76 |
|
* @return int The ID of the object |
256
|
76 |
|
*/ |
257
|
|
|
protected static function fetchIdFrom($value, $column) |
258
|
|
|
{ |
259
|
76 |
|
$results = self::fetchIdsFrom($column, $value, false, "LIMIT 1"); |
260
|
|
|
|
261
|
|
|
// Return the id or 0 if nothing was found |
262
|
|
|
return (isset($results[0])) ? $results[0] : 0; |
263
|
76 |
|
} |
264
|
|
|
|
265
|
76 |
|
/** |
266
|
76 |
|
* Gets an array of object IDs from the database |
267
|
|
|
* |
268
|
|
|
* @param string $additional_query Additional query snippet passed to the MySQL query after the SELECT statement (e.g. `WHERE id = ?`) |
269
|
76 |
|
* @param array $params The parameter values that will be passed to Database::query() |
270
|
|
|
* @param string $table The database table that will be searched |
271
|
|
|
* @param string|string[] $select The column that will be returned |
272
|
|
|
* |
273
|
|
|
* @return int[] A list of values, if $select was only one column, or the return array of $db->query if it was more |
274
|
|
|
*/ |
275
|
|
|
protected static function fetchIds($additional_query = '', $params = array(), $table = "", $select = 'id') |
276
|
|
|
{ |
277
|
|
|
$table = (empty($table)) ? static::TABLE : $table; |
278
|
|
|
$db = Database::getInstance(); |
279
|
|
|
|
280
|
|
|
// If $select is an array, convert it into a comma-separated list that MySQL will accept |
281
|
|
|
if (is_array($select)) { |
282
|
|
|
$select = implode(",", $select); |
283
|
|
|
} |
284
|
76 |
|
|
285
|
|
|
$results = $db->query("SELECT $select FROM $table $additional_query", $params); |
286
|
76 |
|
|
287
|
76 |
|
if (!$results) { |
|
|
|
|
288
|
|
|
return array(); |
289
|
76 |
|
} |
290
|
29 |
|
|
291
|
|
|
return array_column($results, 0); |
292
|
|
|
} |
293
|
76 |
|
|
294
|
76 |
|
/** |
295
|
|
|
* Gets an array of object IDs from the database that have a column equal to something else |
296
|
|
|
* |
297
|
76 |
|
* @param string $column The name of the column that should be tested |
298
|
|
|
* @param array|mixed $possible_values List of acceptable values |
299
|
|
|
* @param bool $negate Whether to search if the value of $column does NOT belong to the $possible_values array |
300
|
|
|
* @param string|string[] $select The name of the column(s) that the returned array should contain |
301
|
|
|
* @param string $additional_query Additional parameters to be passed to the MySQL query (e.g. `WHERE id = 5`) |
302
|
|
|
* @param string $table The database table which will be used for queries |
303
|
|
|
* |
304
|
|
|
* @return int[] A list of values, if $select was only one column, or the return array of $db->query if it was more |
305
|
|
|
*/ |
306
|
76 |
|
protected static function fetchIdsFrom($column, $possible_values, $negate = false, $additional_query = "", $table = "", $select = 'id') |
307
|
|
|
{ |
308
|
|
|
$question_marks = array(); |
309
|
76 |
|
$negation = ($negate) ? "NOT" : ""; |
310
|
|
|
|
311
|
|
|
if (!is_array($possible_values)) { |
312
|
|
|
$possible_values = array($possible_values); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
foreach ($possible_values as $p) { |
316
|
|
|
$question_marks[] = '?'; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
if (empty($possible_values)) { |
320
|
|
|
if (!$negate) { |
321
|
72 |
|
// There isn't any value that $column can have so |
322
|
|
|
// that it matches the criteria - return nothing. |
323
|
72 |
|
return array(); |
324
|
|
|
} else { |
325
|
|
|
$conditionString = $additional_query; |
326
|
|
|
} |
327
|
|
|
} else { |
328
|
|
|
$conditionString = "WHERE $column $negation IN (" . implode(",", $question_marks) . ") $additional_query"; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
return self::fetchIds($conditionString, $possible_values, $table, $select); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Get the MySQL columns that will be loaded as soon as the model is created |
336
|
|
|
* |
337
|
|
|
* @todo Make this protected |
338
|
|
|
* |
339
|
|
|
* @param string $prefix The prefix that'll be prefixed to column names |
340
|
|
|
* |
341
|
|
|
* @return string The columns in a format readable by MySQL |
342
|
|
|
*/ |
343
|
|
|
public static function getEagerColumns($prefix = null) |
344
|
|
|
{ |
345
|
|
|
return self::formatColumns($prefix, ['*']); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
76 |
|
* Get the MySQL columns that will be loaded only when a corresponding |
350
|
|
|
* parameter of the model is requested |
351
|
76 |
|
* |
352
|
76 |
|
* This is done in order to reduce the time needed to load parameters that |
353
|
|
|
* will not be requested (e.g player activation codes or permissions) |
354
|
|
|
* |
355
|
25 |
|
* @return string|null The columns in a format readable by MySQL or null to |
356
|
|
|
* fetch no columns at all |
357
|
|
|
*/ |
358
|
|
|
protected static function getLazyColumns() |
359
|
|
|
{ |
360
|
|
|
throw new Exception("You need to specify a Model::getLazyColumns() method"); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Get a formatted string with a comma separated column list with table/alias prefixes if necessary. |
365
|
|
|
* |
366
|
|
|
* @param string|null $prefix The table name or SQL alias to be prepend to these columns |
367
|
|
|
* @param array $columns The columns to format |
368
|
|
|
* |
369
|
|
|
* @return string |
370
|
|
|
*/ |
371
|
|
|
protected static function formatColumns($prefix = null, $columns = ['*']) |
372
|
|
|
{ |
373
|
|
|
if ($prefix === null) { |
374
|
|
|
return implode(',', $columns); |
375
|
|
|
} |
376
|
76 |
|
|
377
|
|
|
return (($prefix . '.') . implode(sprintf(',%s.', $prefix), $columns)); |
378
|
76 |
|
} |
379
|
76 |
|
|
380
|
|
|
/** |
381
|
76 |
|
* Load all the parameters of the model that were not loaded during the first |
382
|
|
|
* fetch from the database |
383
|
76 |
|
* |
384
|
76 |
|
* @param array $result MySQL's result set |
385
|
|
|
* @return void |
386
|
76 |
|
*/ |
387
|
|
|
protected function assignLazyResult($result) |
388
|
|
|
{ |
389
|
|
|
throw new Exception("You need to specify a Model::lazyLoad() method"); |
390
|
76 |
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Load all the properties of the model that haven't been loaded yet |
394
|
|
|
* |
395
|
|
|
* @param bool $force Whether to force a reload |
396
|
76 |
|
* @return self |
397
|
|
|
*/ |
398
|
|
|
protected function lazyLoad($force = false) |
399
|
|
|
{ |
400
|
|
|
if ((!$this->loaded || $force) && $this->valid) { |
401
|
|
|
$this->loaded = true; |
402
|
|
|
|
403
|
|
|
$columns = $this->getLazyColumns(); |
404
|
1 |
|
|
405
|
|
|
if ($columns !== null) { |
406
|
1 |
|
$results = $this->db->query("SELECT $columns FROM {$this->table} WHERE id = ? LIMIT 1", array($this->id)); |
407
|
|
|
|
408
|
|
|
if (count($results) < 1) { |
409
|
|
|
throw new Exception("The model has mysteriously disappeared"); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
$this->assignLazyResult($results[0]); |
413
|
|
|
} else { |
414
|
|
|
$this->assignLazyResult(array()); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
return $this; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Gets an entity from the supplied slug, which can either be an alias or an ID |
423
|
76 |
|
* @param string|int $slug The object's slug |
424
|
|
|
* @return static |
425
|
76 |
|
*/ |
426
|
76 |
|
public static function fetchFromSlug($slug) |
427
|
|
|
{ |
428
|
76 |
|
return static::get((int) $slug); |
429
|
76 |
|
} |
430
|
|
|
|
431
|
76 |
|
/** |
432
|
76 |
|
* Creates a new entry in the database |
433
|
|
|
* |
434
|
76 |
|
* <code> |
435
|
45 |
|
* Model::create(array( 'author'=>15, 'content'=>"Lorem ipsum..." )); |
436
|
|
|
* </code> |
437
|
42 |
|
* |
438
|
|
|
* @param array $params An associative array, with the keys (columns) pointing to the |
439
|
|
|
* values you want to put on each |
440
|
45 |
|
* @param array|string $now Column(s) to update with the current timestamp |
441
|
45 |
|
* @param string $table The table to perform the query on, defaults to the Model's |
442
|
45 |
|
* table |
443
|
|
|
* @return static The new entry |
444
|
|
|
*/ |
445
|
|
|
protected static function create($params, $now = null, $table = '') |
446
|
76 |
|
{ |
447
|
76 |
|
$table = (empty($table)) ? static::TABLE : $table; |
448
|
|
|
$db = Database::getInstance(); |
449
|
76 |
|
$id = $db->insert($table, $params, $now); |
450
|
|
|
|
451
|
|
|
return static::get($id); |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* Fetch a model's data from the database again |
456
|
|
|
* @return static The new model |
457
|
|
|
*/ |
458
|
|
|
public function refresh() |
459
|
|
|
{ |
460
|
|
|
self::__construct($this->id); |
461
|
|
|
|
462
|
|
|
if ($this->loaded) { |
463
|
|
|
// Load the lazy parameters of the model if they're loaded already |
464
|
|
|
$this->lazyLoad(true); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
return $this; |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
/** |
471
|
|
|
* Generate an invalid object |
472
|
|
|
* |
473
|
|
|
* <code> |
474
|
|
|
* <?php |
475
|
|
|
* $object = Team::invalid(); |
476
|
|
|
* |
477
|
|
|
* get_class($object); // Team |
478
|
|
|
* $object->isValid(); // false |
479
|
|
|
* </code> |
480
|
13 |
|
* @return static |
481
|
|
|
*/ |
482
|
13 |
|
public static function invalid() |
483
|
|
|
{ |
484
|
|
|
return new static(0); |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.