Completed
Push — feature/pixie-port ( 3c72f8...4b0f1f )
by Vladimir
04:02
created

BaseModel::getQueryBuilder()   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 0
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
     * @deprecated 0.10.2 Replaced by static::getEagerColumnsList() in 0.11.0
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, ['*']);
0 ignored issues
show
Deprecated Code introduced by
The method BaseModel::formatColumns() has been deprecated with message: 0.10.2 This function has been removed and is no longer required with the new query builder

This method 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 method will be removed from the class and what other method or class to use instead.

Loading history...
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
     * @deprecated 0.10.2 Replaced by static::getLazyColumnsList() in 0.11.0
383
     *
384
     * @return string|null The columns in a format readable by MySQL or null to
385
     *                     fetch no columns at all
386
     */
387
    protected static function getLazyColumns()
388
    {
389
        throw new Exception("You need to specify a Model::getLazyColumns() method");
390
    }
391
392
    /**
393
     * Get a formatted string with a comma separated column list with table/alias prefixes if necessary.
394
     *
395
     * @deprecated 0.10.2 This function has been removed and is no longer required with the new query builder
396
     *
397
     * @param string|null $prefix  The table name or SQL alias to be prepend to these columns
398
     * @param array       $columns The columns to format
399
     *
400
     * @return string
401
     */
402
    protected static function formatColumns($prefix = null, $columns = ['*'])
403
    {
404
        if ($prefix === null) {
405
            return implode(',', $columns);
406
        }
407
408
        return (($prefix . '.') . implode(sprintf(',%s.', $prefix), $columns));
409
    }
410
411
    //
412
    // Query building for models
413
    //
414
415
    /**
416
     * Get a query builder instance for this model.
417
     *
418
     * @throws BadMethodCallException When this function has not been configured for a particular model
419
     * @throws Exception              When no database has been configured for BZiON
420
     *
421
     * @since 0.11.0 The expected return type has been changed from QueryBuilder to QueryBuilderFlex
422
     * @since 0.9.0
423
     *
424
     * @return QueryBuilderFlex
425
     */
426
    public static function getQueryBuilder()
427
    {
428
        throw new BadMethodCallException(sprintf('No Query Builder has been configured for the %s model', get_called_class()));
429
    }
430
431
    /**
432
     * Modify a QueryBuilderFlex instance to configure what conditions should be applied when fetching "active" entries.
433
     *
434
     * This function is called whenever a QueryBuilder calls `static::active()`. A reference to the QueryBuilderFlex
435
     * instance is passed to allow you to add any necessary conditions to the query. This function should return true if
436
     * the QueryBuilderInstance has been modified to stop propagation to other modifications; return false if nothing
437
     * has been modified.
438
     *
439
     * @internal For use of QueryBuilderFlex when fetching active entries.
440
     *
441
     * @param \QueryBuilderFlex $qb A reference to the QBF to allow modifications.
442
     *
443
     * @since 0.11.0
444
     *
445
     * @return bool Returns true if propagation should stop and the QueryBuilderFlex should not make further modifications
446
     *              in this regard.
447
     */
448
    public static function getActiveModels(QueryBuilderFlex &$qb)
449
    {
450
        return false;
451
    }
452
453
    /**
454
     * Get the list of columns that should be eagerly loaded when a model is created from database results.
455
     *
456
     * @since 0.11.0
457
     *
458
     * @return array
459
     */
460
    public static function getEagerColumnsList()
461
    {
462
        return ['*'];
463
    }
464
465
    /**
466
     * Get the list of columns that need to be lazily loaded in a model.
467
     *
468
     * This function should return an empty array if no columns need to be lazily loaded.
469
     *
470
     * @since 0.11.0
471
     *
472
     * @return array
473
     */
474
    public static function getLazyColumnsList()
475
    {
476
        return [];
477
    }
478
479
    //
480
    // Model creation from the database
481
    //
482
483
    /**
484
     * Load all the parameters of the model that were not loaded during the first
485
     * fetch from the database
486
     *
487
     * @param  array $result MySQL's result set
488
     * @return void
489
     */
490
    protected function assignLazyResult($result)
491
    {
492
        throw new Exception("You need to specify a Model::lazyLoad() method");
493
    }
494
495
    /**
496
     * Load all the properties of the model that haven't been loaded yet
497
     *
498
     * @param  bool $force Whether to force a reload
499
     * @return self
500
     */
501
    protected function lazyLoad($force = false)
502
    {
503
        if ((!$this->loaded || $force) && $this->valid) {
504
            $this->loaded = true;
505
506
            $columns = $this->getLazyColumns();
0 ignored issues
show
Deprecated Code introduced by
The method BaseModel::getLazyColumns() has been deprecated with message: 0.10.2 Replaced by static::getLazyColumnsList() in 0.11.0

This method 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 method will be removed from the class and what other method or class to use instead.

Loading history...
507
508
            if ($columns !== null) {
509
                $results = $this->db->query("SELECT $columns FROM {$this->table} WHERE id = ? LIMIT 1", array($this->id));
510
511
                if (count($results) < 1) {
512
                    throw new Exception("The model has mysteriously disappeared");
513
                }
514
515
                $this->assignLazyResult($results[0]);
516
            } else {
517
                $this->assignLazyResult(array());
518
            }
519
        }
520
521
        return $this;
522
    }
523
524
    /**
525
     * Gets an entity from the supplied slug, which can either be an alias or an ID
526
     * @param  string|int $slug The object's slug
527
     * @return static
528
     */
529
    public static function fetchFromSlug($slug)
530
    {
531
        return static::get((int) $slug);
532
    }
533
534
    /**
535
     * Creates a new entry in the database
536
     *
537
     * <code>
538
     * Model::create(array( 'author'=>15, 'content'=>"Lorem ipsum..."  ));
539
     * </code>
540
     *
541
     * @param  array        $params An associative array, with the keys (columns) pointing to the
542
     *                              values you want to put on each
543
     * @param  array|string $now    Column(s) to update with the current timestamp
544
     * @param  string       $table  The table to perform the query on, defaults to the Model's
545
     *                              table
546
     * @return static       The new entry
547
     */
548
    protected static function create($params, $now = null, $table = '')
549
    {
550
        $table = (empty($table)) ? static::TABLE : $table;
551
        $db = Database::getInstance();
552
        $id = $db->insert($table, $params, $now);
553
554
        return static::get($id);
555
    }
556
557
    /**
558
     * Fetch a model's data from the database again
559
     * @return static The new model
560
     */
561
    public function refresh()
562
    {
563
        self::__construct($this->id);
564
565
        if ($this->loaded) {
566
            // Load the lazy parameters of the model if they're loaded already
567
            $this->lazyLoad(true);
568
        }
569
570
        return $this;
571
    }
572
573
    /**
574
     * Generate an invalid object
575
     *
576
     * <code>
577
     *     <?php
578
     *     $object = Team::invalid();
579
     *
580
     *     get_class($object); // Team
581
     *     $object->isValid(); // false
582
     * </code>
583
     * @return static
584
     */
585
    public static function invalid()
586
    {
587
        return new static(0);
588
    }
589
}
590