Completed
Push — feature/pixie-port ( 7c82d8...3c72f8 )
by Vladimir
07:47 queued 10s
created

BaseModel::getActiveModels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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
     * Whether or not this model has been soft deleted.
43
     * @var bool
44
     */
45
    protected $is_deleted;
46
47
    /**
48
     * The database variable used for queries
49
     * @var Database
50
     */
51
    protected $db;
52
53
    /**
54
     * Whether the lazy parameters of the model have been loaded
55
     * @var bool
56
     */
57
    protected $loaded = false;
58
59
    /**
60
     * The default status value for deletable models
61
     *
62
     * @deprecated 0.10.3 The `status` SET columns are deprecated. Using boolean columns is now the new standard.
63
     */
64
    const DEFAULT_STATUS = 'active';
65
66
    /**
67
     * The column name in the database that is used for marking a row as soft deleted.
68
     */
69
    const DELETED_COLUMN = null;
70
71
    /**
72
     * The value that's used in `self::DELETED_COLUMN` to mark something as soft deleted.
73
     */
74
    const DELETED_VALUE = true;
75
76
    /**
77
     * The name of the database table used for queries
78
     * You can use this constant in static methods as such:
79
     * static::TABLE
80
     */
81
    const TABLE = "";
82
83
    /**
84
     * Get a Model based on its ID
85
     *
86
     * @param  int|static $id The ID of the object to look for, or the object
87
     *                        itself
88
     * @throws InvalidArgumentException If $id is an object of an incorrect type
89
     * @return static
90
     */
91
    public static function get($id)
92
    {
93
        if ($id instanceof static) {
94
            return $id;
95
        }
96
97
        if (is_object($id)) {
98
            // Throw an exception if $id is an object of the incorrect class
99
            throw new InvalidArgumentException("The object provided is not of the correct type");
100
        }
101
102
        $id = (int) $id;
103
104
        return static::chooseModelFromDatabase($id);
105
    }
106
107
    /**
108
     * Assign the MySQL result array to the individual properties of the model
109
     *
110
     * @param  array $result MySQL's result array
111
     * @return null
112
     */
113
    abstract protected function assignResult($result);
114
115
    /**
116
     * Fetch the columns of a model
117
     *
118
     * This method takes the ID of the object to look for and creates a
119
     * $this->db object which can be used to communicate with the database and
120
     * calls $this->assignResult() so that the child class can populate the
121
     * properties of the Model based on the database data
122
     *
123
     * If the $id is specified as 0, then an invalid object will be returned
124
     *
125
     * @param int $id The ID of the model
126
     * @param array|null $results The column values of the model, or NULL to
127
     *                            generate them using $this->fetchColumnValues()
128
     */
129
    protected function __construct($id, $results = null)
130
    {
131
        $this->db = Database::getInstance();
132
        $this->table = static::TABLE;
133
134
        if ($id == 0) {
135
            $this->valid = false;
136
137
            return;
138
        }
139
140
        $this->id = $id;
141
142
        if ($results == null) {
143
            $results = $this->fetchColumnValues($id);
144
        }
145
146
        if ($results === null) {
147
            $this->valid = false;
148
        } else {
149
            $this->valid = true;
150
            $this->assignResult($results);
0 ignored issues
show
Bug introduced by
It seems like $results can also be of type object<stdClass>; however, BaseModel::assignResult() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
151
        }
152
    }
153
154
    /**
155
     * Update a database field
156
     *
157
     * @param string $name  The name of the column
158
     * @param mixed  $value The value to set the column to
159
     *
160
     * @return void
161
     */
162
    public function update($name, $value)
163
    {
164
        $this->db->execute("UPDATE " . static::TABLE . " SET `$name` = ? WHERE id = ?", array($value, $this->id));
165
    }
166
167
    /**
168
     * Delete the object
169
     *
170
     * Please note that this does not delete the object entirely from the database,
171
     * it only hides it from users. You should overload this function if your object
172
     * does not have a 'status' column which can be set to 'deleted'.
173
     */
174
    public function delete()
175
    {
176
        if (static::DELETED_COLUMN === null) {
177
            @trigger_error(sprintf('The %s class is using the deprecated `status` column and needs to be updated.', get_called_class()), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
178
179
            $this->status = 'deleted';
180
            $this->update('status', 'deleted');
181
182
            return;
183
        }
184
185
        $this->is_deleted = true;
186
        $this->update('is_deleted', true);
187
    }
188
189
    /**
190
     * Permanently delete the object from the database
191
     */
192
    public function wipe()
193
    {
194
        $this->db->execute("DELETE FROM " . static::TABLE . " WHERE id = ?", array($this->id));
195
    }
196
197
    /**
198
     * If a model has been marked as deleted in the database, this'll go through the process of marking the model
199
     * "active" again.
200
     */
201
    public function restore()
202
    {
203
        $this->status = static::DEFAULT_STATUS;
0 ignored issues
show
Deprecated Code introduced by
The constant BaseModel::DEFAULT_STATUS has been deprecated with message: 0.10.3 The `status` SET columns are deprecated. Using boolean columns is now the new standard.

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
204
        $this->update('status', static::DEFAULT_STATUS);
0 ignored issues
show
Deprecated Code introduced by
The constant BaseModel::DEFAULT_STATUS has been deprecated with message: 0.10.3 The `status` SET columns are deprecated. Using boolean columns is now the new standard.

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
205
    }
206
207
    /**
208
     * Get an object's database ID
209
     * @return int The ID
210
     */
211
    public function getId()
212
    {
213
        return $this->id;
214
    }
215
216
    /**
217
     * See if an object is valid
218
     * @return bool
219
     */
220
    public function isValid()
221
    {
222
        return $this->valid;
223
    }
224
225
    /**
226
     * Fetch a model based on its ID, useful for abstract model classes
227
     *
228
     * @param int $id The ID of the model
229
     * @return Model
230
     */
231
    protected static function chooseModelFromDatabase($id)
232
    {
233
        return new static($id);
234
    }
235
236
    /**
237
     * Query the database to get the eager column values for the Model
238
     *
239
     * @param $id int The ID of the model to fetch
240
     * @return array|null The results or null if a model wasn't found
241
     */
242
    protected static function fetchColumnValues($id)
243
    {
244
        $qb = QueryBuilderFlex::createForTable(static::TABLE);
245
        $results = $qb
246
            ->select(static::getEagerColumnsList())
247
            ->find($id)
248
        ;
249
250
        if (count($results) < 1) {
251
            return null;
252
        }
253
254
        return $results;
255
    }
256
257
    /**
258
     * Counts the elements of the database that match a specific query
259
     *
260
     * @param  string $additional_query The MySQL query string (e.g. `WHERE id = ?`)
261
     * @param  array  $params           The parameter values that will be passed to Database::query()
262
     * @param  string $table            The database table that will be searched, defaults to the model's table
263
     * @param  string $column           Only count the entries where `$column` is not `NULL` (or all if `$column` is `*`)
264
     * @return int
265
     */
266
    protected static function fetchCount($additional_query = '', $params = array(), $table = '', $column = '*')
267
    {
268
        $table = (empty($table)) ? static::TABLE : $table;
269
        $db = Database::getInstance();
270
271
        $result = $db->query("SELECT COUNT($column) AS count FROM $table $additional_query",
272
            $params
273
        );
274
275
        return $result[0]['count'];
276
    }
277
278
    /**
279
     * Gets the id of a database row which has a specific value on a column
280
     * @param  string $value  The value which the column should be equal to
281
     * @param  string $column The name of the database column
282
     * @return int    The ID of the object
283
     */
284
    protected static function fetchIdFrom($value, $column)
285
    {
286
        $results = self::fetchIdsFrom($column, $value, false, "LIMIT 1");
287
288
        // Return the id or 0 if nothing was found
289
        return (isset($results[0])) ? $results[0] : 0;
290
    }
291
292
    /**
293
     * Gets an array of object IDs from the database
294
     *
295
     * @param string          $additional_query Additional query snippet passed to the MySQL query after the SELECT statement (e.g. `WHERE id = ?`)
296
     * @param array           $params           The parameter values that will be passed to Database::query()
297
     * @param string          $table            The database table that will be searched
298
     * @param string|string[] $select           The column that will be returned
299
     *
300
     * @return int[] A list of values, if $select was only one column, or the return array of $db->query if it was more
301
     */
302
    protected static function fetchIds($additional_query = '', $params = array(), $table = "", $select = 'id')
303
    {
304
        $table = (empty($table)) ? static::TABLE : $table;
305
        $db = Database::getInstance();
306
307
        // If $select is an array, convert it into a comma-separated list that MySQL will accept
308
        if (is_array($select)) {
309
            $select = implode(",", $select);
310
        }
311
312
        $results = $db->query("SELECT $select FROM $table $additional_query", $params);
313
314
        if (!$results) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
315
            return array();
316
        }
317
318
        return array_column($results, 0);
319
    }
320
321
    /**
322
     * Gets an array of object IDs from the database that have a column equal to something else
323
     *
324
     * @param string          $column           The name of the column that should be tested
325
     * @param array|mixed     $possible_values  List of acceptable values
326
     * @param bool            $negate           Whether to search if the value of $column does NOT belong to the $possible_values array
327
     * @param string|string[] $select           The name of the column(s) that the returned array should contain
328
     * @param string          $additional_query Additional parameters to be passed to the MySQL query (e.g. `WHERE id = 5`)
329
     * @param string          $table            The database table which will be used for queries
330
     *
331
     * @return int[] A list of values, if $select was only one column, or the return array of $db->query if it was more
332
     */
333
    protected static function fetchIdsFrom($column, $possible_values, $negate = false, $additional_query = "", $table = "", $select = 'id')
334
    {
335
        $question_marks = array();
336
        $negation = ($negate) ? "NOT" : "";
337
338
        if (!is_array($possible_values)) {
339
            $possible_values = array($possible_values);
340
        }
341
342
        foreach ($possible_values as $p) {
343
            $question_marks[] = '?';
344
        }
345
346
        if (empty($possible_values)) {
347
            if (!$negate) {
348
                // There isn't any value that $column can have so
349
                // that it matches the criteria - return nothing.
350
                return array();
351
            } else {
352
                $conditionString = $additional_query;
353
            }
354
        } else {
355
            $conditionString = "WHERE $column $negation IN (" . implode(",", $question_marks) . ") $additional_query";
356
        }
357
358
        return self::fetchIds($conditionString, $possible_values, $table, $select);
359
    }
360
361
    /**
362
     * Get the MySQL columns that will be loaded as soon as the model is created
363
     *
364
     * @todo Make this protected
365
     *
366
     * @param string $prefix The prefix that'll be prefixed to column names
367
     *
368
     * @return string The columns in a format readable by MySQL
369
     */
370
    public static function getEagerColumns($prefix = null)
371
    {
372
        return self::formatColumns($prefix, ['*']);
373
    }
374
375
    /**
376
     * Get the MySQL columns that will be loaded only when a corresponding
377
     * parameter of the model is requested
378
     *
379
     * This is done in order to reduce the time needed to load parameters that
380
     * will not be requested (e.g player activation codes or permissions)
381
     *
382
     * @return string|null The columns in a format readable by MySQL or null to
383
     *                     fetch no columns at all
384
     */
385
    protected static function getLazyColumns()
386
    {
387
        throw new Exception("You need to specify a Model::getLazyColumns() method");
388
    }
389
390
    /**
391
     * Get a formatted string with a comma separated column list with table/alias prefixes if necessary.
392
     *
393
     * @param string|null $prefix  The table name or SQL alias to be prepend to these columns
394
     * @param array       $columns The columns to format
395
     *
396
     * @return string
397
     */
398
    protected static function formatColumns($prefix = null, $columns = ['*'])
399
    {
400
        if ($prefix === null) {
401
            return implode(',', $columns);
402
        }
403
404
        return (($prefix . '.') . implode(sprintf(',%s.', $prefix), $columns));
405
    }
406
407
    /**
408
     * Modify a QueryBuilderFlex instance to configure what conditions should be applied when fetching "active" entries.
409
     *
410
     * This function is called whenever a QueryBuilder calls `static::active()`. A reference to the QueryBuilderFlex
411
     * instance is passed to allow you to add any necessary conditions to the query. This function should return true if
412
     * the QueryBuilderInstance has been modified to stop propagation to other modifications; return false if nothing
413
     * has been modified.
414
     *
415
     * @internal For use of QueryBuilderFlex when fetching active entries.
416
     *
417
     * @param \QueryBuilderFlex $qb A reference to the QBF to allow modifications.
418
     *
419
     * @since 0.11.0
420
     *
421
     * @return bool Returns true if propagation should stop and the QueryBuilderFlex should not make further modifications
422
     *              in this regard.
423
     */
424
    public static function getActiveModels(QueryBuilderFlex &$qb)
425
    {
426
        return false;
427
    }
428
429
    public static function getEagerColumnsList()
430
    {
431
        return ['*'];
432
    }
433
434
    public static function getLazyColumnsList()
435
    {
436
        return [];
437
    }
438
439
    /**
440
     * Load all the parameters of the model that were not loaded during the first
441
     * fetch from the database
442
     *
443
     * @param  array $result MySQL's result set
444
     * @return void
445
     */
446
    protected function assignLazyResult($result)
447
    {
448
        throw new Exception("You need to specify a Model::lazyLoad() method");
449
    }
450
451
    /**
452
     * Load all the properties of the model that haven't been loaded yet
453
     *
454
     * @param  bool $force Whether to force a reload
455
     * @return self
456
     */
457
    protected function lazyLoad($force = false)
458
    {
459
        if ((!$this->loaded || $force) && $this->valid) {
460
            $this->loaded = true;
461
462
            $columns = $this->getLazyColumns();
463
464
            if ($columns !== null) {
465
                $results = $this->db->query("SELECT $columns FROM {$this->table} WHERE id = ? LIMIT 1", array($this->id));
466
467
                if (count($results) < 1) {
468
                    throw new Exception("The model has mysteriously disappeared");
469
                }
470
471
                $this->assignLazyResult($results[0]);
472
            } else {
473
                $this->assignLazyResult(array());
474
            }
475
        }
476
477
        return $this;
478
    }
479
480
    /**
481
     * Gets an entity from the supplied slug, which can either be an alias or an ID
482
     * @param  string|int $slug The object's slug
483
     * @return static
484
     */
485
    public static function fetchFromSlug($slug)
486
    {
487
        return static::get((int) $slug);
488
    }
489
490
    /**
491
     * Creates a new entry in the database
492
     *
493
     * <code>
494
     * Model::create(array( 'author'=>15, 'content'=>"Lorem ipsum..."  ));
495
     * </code>
496
     *
497
     * @param  array        $params An associative array, with the keys (columns) pointing to the
498
     *                              values you want to put on each
499
     * @param  array|string $now    Column(s) to update with the current timestamp
500
     * @param  string       $table  The table to perform the query on, defaults to the Model's
501
     *                              table
502
     * @return static       The new entry
503
     */
504
    protected static function create($params, $now = null, $table = '')
505
    {
506
        $table = (empty($table)) ? static::TABLE : $table;
507
        $db = Database::getInstance();
508
        $id = $db->insert($table, $params, $now);
509
510
        return static::get($id);
511
    }
512
513
    /**
514
     * Fetch a model's data from the database again
515
     * @return static The new model
516
     */
517
    public function refresh()
518
    {
519
        self::__construct($this->id);
520
521
        if ($this->loaded) {
522
            // Load the lazy parameters of the model if they're loaded already
523
            $this->lazyLoad(true);
524
        }
525
526
        return $this;
527
    }
528
529
    /**
530
     * Generate an invalid object
531
     *
532
     * <code>
533
     *     <?php
534
     *     $object = Team::invalid();
535
     *
536
     *     get_class($object); // Team
537
     *     $object->isValid(); // false
538
     * </code>
539
     * @return static
540
     */
541
    public static function invalid()
542
    {
543
        return new static(0);
544
    }
545
}
546