Completed
Push — master ( 7aab3a...2b8a1f )
by Vladimir
18:21 queued 01:15
created

BaseModel::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 3
crap 2
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) {
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...
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