Completed
Push — master ( 196a6a...314b46 )
by Stone
12s
created

Model   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 457
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 148
dl 0
loc 457
rs 8.96
c 0
b 0
f 0
wmc 43

23 Methods

Rating   Name   Duplication   Size   Complexity  
A query() 0 3 1
A bind() 0 21 6
A execute() 0 6 2
A __construct() 0 4 1
A getResultSet() 0 8 1
A isSlugUnique() 0 13 2
A getTable() 0 44 5
A fetchAll() 0 3 1
A finalExecute() 0 5 1
A getResultSetLimited() 0 9 1
A list() 0 12 1
A generateHash() 0 3 1
A closeCursor() 0 3 1
A getSlugFromId() 0 20 3
A getRowBySlug() 0 14 2
A generateToken() 0 3 1
A returnArray() 0 9 3
A count() 0 7 1
A getTablePrefix() 0 6 2
A getRowById() 0 10 1
A fetch() 0 3 1
A getIdFromSlug() 0 16 3
A getRowByColumn() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Core;
4
5
use Core\Traits\StringFunctions;
6
use Exception;
7
use PDO;
8
9
/**
10
 * Class Model here we have all the generic calls to be inherited by the App\Models
11
 * using PDO connections
12
 * @package Core
13
 *
14
 * PHP version 7
15
 */
16
abstract class Model
17
{
18
    use StringFunctions;
19
    /**
20
     * @var PDO the database handeler
21
     */
22
    protected $dbh;
23
24
    /**
25
     * @var \PDOStatement|boolean the prepared sql statement
26
     */
27
    protected $stmt;
28
29
    /**
30
     * @var Container the dependancy injector
31
     */
32
    private $container;
33
34
    /**
35
     * Model constructor. prepares the database connection
36
     * @param Container $container
37
     */
38
    public function __construct(Container $container)
39
    {
40
        $this->container = $container;
41
        $this->dbh = $this->container->setPdo();
42
    }
43
44
    /*
45
     * generic PDO query constructor
46
     * ---------------------------------------------
47
     */
48
49
    /**
50
     * creating and storing the query
51
     * @param $sql string creating the sql query
52
     */
53
    protected function query($sql): void
54
    {
55
        $this->stmt = $this->dbh->prepare($sql);
56
    }
57
58
    /**
59
     * binding the parameters to the query. Need the stmt to be declared before via query()
60
     * @param $param
61
     * @param $value
62
     * @param  $type
63
     * @throws Exception error if no sql query to bind to
64
     */
65
    protected function bind($param, $value, $type = null): void
66
    {
67
        if ($this->stmt == null) {
68
            throw new Exception("No query to bind to");
69
        }
70
        if (is_null($type)) { //need a bind value, so just check it in code. that way we can just call bind(param,value)
71
            switch (true) {
72
                case is_int($value):
73
                    $type = PDO::PARAM_INT;
74
                    break;
75
                case is_bool($value):
76
                    $type = PDO::PARAM_BOOL;
77
                    break;
78
                case is_null($value):
79
                    $type = PDO::PARAM_NULL;
80
                    break;
81
                default:
82
                    $type = PDO::PARAM_STR;
83
            }
84
        }
85
        $this->stmt->bindValue($param, $value, $type);
86
    }
87
88
    /**
89
     * Execute our constructed SQL statement
90
     * @return bool
91
     * @throws Exception if the statement is empty
92
     */
93
    protected function execute()
94
    {
95
        if ($this->stmt == null) {
96
            throw new Exception("No statement to execute");
97
        }
98
        return $this->stmt->execute();
99
    }
100
101
    /**
102
     * close the connection
103
     */
104
    protected function closeCursor()
105
    {
106
        $this->stmt->closeCursor();
107
    }
108
109
    /**
110
     * execute request then close
111
     * @return bool
112
     * @throws Exception
113
     */
114
    protected function finalExecute()
115
    {
116
        $result = $this->execute();
117
        $this->closeCursor();
118
        return $result;
119
    }
120
121
    /**
122
     * fetches the result from an executed query
123
     * @return array
124
     */
125
    protected function fetchAll()
126
    {
127
        return $this->stmt->fetchAll();
128
    }
129
130
    /**
131
     * returns a single line from the executed query
132
     * @return mixed
133
     */
134
    protected function fetch()
135
    {
136
        return $this->stmt->fetch();
137
    }
138
139
    /*
140
     * END generic PDO query constructor
141
     * ---------------------------------------------
142
     */
143
144
    /**
145
     * correlation between the model name and the table name
146
     * if we don't have a table name, get the table that has the same name as the model will be returned (else, we do nothing !!)
147
     * Also search if the table exists, if not do a check in the views (must be v_$table)
148
     * @param string|null $table the name of the table to get, if none the get the table of the models name
149
     * @return string the table name (with an s)
150
     * @throws \ReflectionException the model doesn't exist, should never happen
151
     * @throws Exception table or view doesn't exist
152
     * @return string table or view name
153
     */
154
    protected function getTable(String $table = null): String
155
    {
156
        //If no table is passed, get the calling model name
157
        if ($table === null) {
158
            $reflect = new \ReflectionClass(get_class($this));
159
            $table = $reflect->getShortName(); //this is to only get the model name, otherwise we get the full namespace
160
            //since our models all end with Model, we should remove it.
161
            $table = $this->removeFromEnd($table, 'Model');
162
            $table = $table . 's'; //adding the s since the table should be plural. Might be some special case where the plural isn't just with an s
163
            $table = strtolower($table); //the database names are in lowercase
164
        }
165
166
        //Check if we have already passed the prefix
167
        if (!$this->startsWith($table, Config::TABLE_PREFIX)) {
168
            $table = $this->getTablePrefix($table);
169
        }
170
171
172
        //see if table exists
173
        $sql = "SHOW TABLES LIKE :table";
174
        $stmt = $this->dbh->prepare($sql);
175
        $stmt->bindValue(':table', $table, PDO::PARAM_STR);
176
        $stmt->execute();
177
        $exists = $stmt->rowCount() > 0; //will return 1 if table exists or 0 if non existant
178
179
        if ($exists) {
180
            //the table exists
181
            return $table;
182
        }
183
184
        //if we are here, then table doesn't exist, check for view
185
        $view = 'v_' . $table;
186
        $stmt->bindValue(':table', $view, PDO::PARAM_STR);
187
        $stmt->execute();
188
        $exists = $stmt->rowCount() > 0; //will return 1 if table exists or 0 if non existant
189
190
        if ($exists) {
191
            //the view exists
192
            return $view;
193
        }
194
195
        //neither table or view exists
196
        //throw an error
197
        throw new Exception("Table or view $table doesn't exist");
198
    }
199
200
    /**
201
     * This function adds the table prefix if set and returns the name
202
     * Use this if we are sure of the table name. Avoids the DB calls
203
     * @param $table string the table name
204
     * @return string
205
     */
206
    protected function getTablePrefix($table)
207
    {
208
        if (Config::TABLE_PREFIX != '') {
209
            $table = Config::TABLE_PREFIX . '_' . $table;
210
        }
211
        return $table;
212
    }
213
214
    /**
215
     * checks if the result from a PDO query has any data.
216
     * If yes then we return the array
217
     * If debugging is enabled we throw an exception on no results
218
     * or we just return an empty array
219
     * @param mixed $result the PDO result of a query
220
     * @return array the result or empty
221
     * @throws Exception if debugging is on and no result
222
     */
223
    private function returnArray($result)
224
    {
225
        if ($result) {
226
            return $result;
227
        }
228
        if (Config::DEV_ENVIRONMENT) {
229
            throw new Exception("No results in database");
230
        }
231
        return [];
232
    }
233
234
    /**
235
     * gets the entire table or view and returns the array
236
     * @param string $table the table to search in, if empty then get the table based on model name
237
     * @return array the results from database
238
     * @throws \ReflectionException
239
     */
240
    protected function getResultSet($table = null): array
241
    {
242
        $tableName = $this->getTable($table);
243
        $sql = "SELECT * FROM $tableName"; //can not pass table name as :parameter. since we already have tested if the table exists, this var should be safe.
244
        $this->query($sql);
245
        $this->execute();
246
        $result = $this->stmt->fetchAll(); //returns an array or false if no results
247
        return $this->returnArray($result);
248
    }
249
250
    /**
251
     * gets the entire table or view and returns the array with a limit to the number of rows
252
     * @param string $table the table to search in, if empty then get the table based on model name
253
     * @param string $limit the limit of rows to return
254
     * @return array the results from database
255
     * @throws \ReflectionException
256
     */
257
    protected function getResultSetLimited($limit, $table = null): array
258
    {
259
        $tableName = $this->getTable($table);
260
        $sql = "SELECT * FROM $tableName LIMIT :limit";
261
        $this->query($sql);
262
        $this->bind(':limit', $limit);
263
        $this->execute();
264
        $result = $this->stmt->fetchAll(); //returns an array or false if no results
265
        return $this->returnArray($result);
266
    }
267
268
    /**
269
     * get's the result of SELECT * FROM table where idtable=$id
270
     * @param int $rowId searched id
271
     * @param string $table the table to search, if blank then we get the table or view based on the model name
272
     * @return array result or empty array
273
     * @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results)
274
     */
275
    protected function getRowById($rowId, $table = null)
276
    {
277
        $tableName = $this->getTable($table);
278
        $idName = 'id' . str_replace(Config::TABLE_PREFIX."_","",$tableName);
279
        $sql = "SELECT * FROM $tableName WHERE $idName = :rowId";
280
        $this->query($sql);
281
        $this->bind(':rowId', $rowId);
282
        $this->execute();
283
        $result = $this->stmt->fetch();
284
        return $this->returnArray($result);
285
    }
286
287
    /**
288
     * gets the row from the query SELECT * FROM table WHERE $columnName = $Value
289
     * @param string $columnName the column to search in. Does a regex check for security
290
     * @param string $value the value to search for
291
     * @param string $table the table to search, if blank then we get the table or view based on the model name
292
     * @return array the results or empty
293
     * @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results)
294
     * @throws Exception if the column name consists of other characters than lower case, numbers and underscore for security
295
     */
296
    protected function getRowByColumn(String $columnName, $value, $table = null): array
297
    {
298
        $tableName = $this->getTable($table);
299
        $columnNameOk = preg_match("/^[a-z0-9_]+$/i",
300
            $columnName); //testing if column name only has lower case, numbers and underscore
301
        if (!$columnNameOk) {
302
            throw new Exception("Syntax error : Column name \"$columnName\" is not legal");
303
        }
304
        $sql = "SELECT * FROM $tableName WHERE $columnName = :value";
305
        $this->query($sql);
306
        $this->bind(':value', $value);
307
        $this->execute();
308
        $result = $this->stmt->fetch();
309
        return $this->returnArray($result);
310
    }
311
312
    /**
313
     * count the number of rows in table
314
     * @param string $table
315
     * @return mixed
316
     * @throws Exception
317
     */
318
    protected function count(string $table = null)
319
    {
320
        $table = $this->getTable($table);
321
        $sql = "SELECT COUNT(*) FROM $table";
322
        $this->query($sql);
323
        $this->execute();
324
        return $this->stmt->fetchColumn();
325
    }
326
327
    /**
328
     * get list with offset and limit from table
329
     * @param int $offset
330
     * @param int $limit
331
     * @param string|null $table
332
     * @return array
333
     * @throws \ReflectionException
334
     */
335
    protected function list(int $offset = 0, int $limit = Constant::POSTS_PER_PAGE, string $table = null)
336
    {
337
        $table = $this->getTable($table);
338
        $sql = "
339
            SELECT * FROM $table
340
            LIMIT :limit OFFSET :offset
341
        ";
342
        $this->query($sql);
343
        $this->bind(":limit", $limit);
344
        $this->bind(":offset", $offset);
345
        $this->execute();
346
        return $this->fetchAll();
347
    }
348
349
    /**
350
     * is the slug unique, used when updating the slug
351
     * @param string $slug the slug to search for
352
     * @param string $table the table to search in
353
     * @param string $slugColumn the name of the slug column
354
     * @return bool
355
     * @throws Exception
356
     */
357
    protected function isSlugUnique(string $slug, string $slugColumn, string $table = null): bool
358
    {
359
        if (!$this->isAlphaNum($slugColumn)) {
360
            throw new Exception("Invalid Column name");
361
        }
362
363
        $table = $this->getTable($table);
364
365
        $sql = "SELECT * FROM $table WHERE $slugColumn = :slug";
366
        $this->query($sql);
367
        $this->bind(':slug', $slug);
368
        $this->execute();
369
        return !$this->stmt->rowCount() > 0;
370
    }
371
372
    /**
373
     * get the ID of the row from the slug
374
     * @param string $slug the slug to search
375
     * @param string $table the table to search in
376
     * @param string $slugColumn the slug column name
377
     * @param string $idColumn the id column name
378
     * @return int the id of the row
379
     * @throws Exception
380
     */
381
    protected function getIdFromSlug(string $slug, string $idColumn, string $slugColumn, string $table = null): int
382
    {
383
        if (!$this->isAllAlphaNum([$idColumn, $slugColumn])) {
384
            throw new Exception("Invalid Column name");
385
        }
386
387
        $table = $this->getTable($table);
388
389
        $sql = "SELECT $idColumn FROM $table WHERE $slugColumn = :slug";
390
        $this->query($sql);
391
        $this->bind(":slug", $slug);
392
        $this->execute();
393
        if (!$this->stmt->rowCount() > 0) {
394
            return 0;
395
        }
396
        return $this->stmt->fetchColumn();
397
398
    }
399
400
    /**
401
     * get the slug from an Id
402
     * @param int $searchId
403
     * @param string $idColumn
404
     * @param string $slugColumn
405
     * @param string $table
406
     * @return string
407
     * @throws \ReflectionException
408
     */
409
    protected function getSlugFromId(
410
        int $searchId,
411
        string $idColumn,
412
        string $slugColumn,
413
        string $table = null
414
    ): string {
415
416
        if (!$this->isAllAlphaNum([$idColumn, $slugColumn])) {
417
            throw new Exception("Invalid Column name");
418
        }
419
        $table = $this->getTable($table);
420
421
        $sql = "SELECT $slugColumn FROM $table WHERE $idColumn = :searchId";
422
        $this->query($sql);
423
        $this->bind(":searchId", $searchId);
424
        $this->execute();
425
        if (!$this->stmt->rowCount() > 0) {
426
            return 0;
427
        }
428
        return $this->stmt->fetchColumn();
429
    }
430
431
    /**
432
     * get's the result of SELECT * FROM table where table_slug=$slug
433
     * @param string $slug the slug to look up
434
     * @param string $slugColumn the name of the slug column
435
     * @param string $table the table to search, if blank then we get the table or view based on the model name
436
     * @return array result or empty array
437
     * @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results)
438
     */
439
    protected function getRowBySlug(String $slug, string $slugColumn, $table = null): array
440
    {
441
        if (!$this->isAlphaNum($slugColumn)) {
442
            throw new Exception("Invalid Column name");
443
        }
444
445
        $table = $this->getTable($table);
446
447
        $sql = "SELECT * FROM $table WHERE $slugColumn = :slug";
448
        $this->query($sql);
449
        $this->bind(':slug', $slug);
450
        $this->execute();
451
        $result = $this->stmt->fetch();
452
        return $this->returnArray($result);
453
    }
454
455
    /**
456
     * generates a token to use
457
     * @return string
458
     * @throws \Exception
459
     */
460
    protected function generateToken():string
461
    {
462
        return bin2hex(random_bytes(16));
463
    }
464
465
    /**
466
     * generate a hash from a token
467
     * @param string $token
468
     * @return string
469
     */
470
    protected function generateHash(string $token):string
471
    {
472
        return hash_hmac("sha256", $token, Constant::HASH_KEY);
473
    }
474
}