1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Core; |
4
|
|
|
|
5
|
|
|
use PDO; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Class Model here we have all the generic calls to be inherited by the App\Models |
9
|
|
|
* using PDO connections |
10
|
|
|
* @package Core |
11
|
|
|
* |
12
|
|
|
* PHP version 7 |
13
|
|
|
*/ |
14
|
|
|
abstract class Model |
15
|
|
|
{ |
16
|
|
|
/** |
17
|
|
|
* @var null|PDO the database handeler |
18
|
|
|
*/ |
19
|
|
|
protected $dbh; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var \PDOStatement the prepared sql statement |
23
|
|
|
*/ |
24
|
|
|
protected $stmt; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var Container the dependancy injector |
28
|
|
|
*/ |
29
|
|
|
private $container; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Model constructor. prepares the database connection |
33
|
|
|
* @param Container $container |
34
|
|
|
*/ |
35
|
|
|
public function __construct(Container $container) |
36
|
|
|
{ |
37
|
|
|
$this->container = $container; |
38
|
|
|
$this->dbh = $this->container->setPdo(); |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/* |
42
|
|
|
* generic PDO query constructor |
43
|
|
|
* --------------------------------------------- |
44
|
|
|
*/ |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* creating and storing the query |
48
|
|
|
* @param $sql string creating the sql query |
49
|
|
|
*/ |
50
|
|
|
protected function query($sql): void |
51
|
|
|
{ |
52
|
|
|
$this->stmt = $this->dbh->prepare($sql); |
|
|
|
|
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* binding the parameters to the query. Need the stmt to be declared before via query() |
57
|
|
|
* @param $param |
58
|
|
|
* @param $value |
59
|
|
|
* @param $type |
60
|
|
|
* @throws \Exception error if no sql query to bind to |
61
|
|
|
*/ |
62
|
|
|
protected function bind($param, $value, $type = null): void |
63
|
|
|
{ |
64
|
|
|
if ($this->stmt == null) { |
65
|
|
|
throw new \Exception("No query to bind to"); |
66
|
|
|
} |
67
|
|
|
if (is_null($type)) { //need a bind value, so just check it in code. that way we can just call bind(param,value) |
68
|
|
|
switch (true) { |
69
|
|
|
case is_int($value): |
70
|
|
|
$type = PDO::PARAM_INT; |
71
|
|
|
break; |
72
|
|
|
case is_bool($value): |
73
|
|
|
$type = PDO::PARAM_BOOL; |
74
|
|
|
break; |
75
|
|
|
case is_null($value): |
76
|
|
|
$type = PDO::PARAM_NULL; |
77
|
|
|
break; |
78
|
|
|
default: |
79
|
|
|
$type = PDO::PARAM_STR; |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
$this->stmt->bindValue($param, $value, $type); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Execute our constructed SQL statement |
87
|
|
|
* @return bool |
88
|
|
|
* @throws \Exception if the statement is empty |
89
|
|
|
*/ |
90
|
|
|
protected function execute() |
91
|
|
|
{ |
92
|
|
|
if ($this->stmt == null) { |
93
|
|
|
throw new \Exception("No statement to execute"); |
94
|
|
|
} |
95
|
|
|
return $this->stmt->execute(); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/* |
99
|
|
|
* END generic PDO query constructor |
100
|
|
|
* --------------------------------------------- |
101
|
|
|
*/ |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* correlation between the model name and the table name |
105
|
|
|
* 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 !!) |
106
|
|
|
* Also search if the table exists, if not do a check in the views (must be v_$table) |
107
|
|
|
* @param string|null $table the name of the table to get, if none the get the table of the models name |
108
|
|
|
* @return string the table name (with an s) |
109
|
|
|
* @throws \ReflectionException the model doesn't exist, should never happen |
110
|
|
|
* @throws \Exception table or view doesn't exist |
111
|
|
|
* @return string table or view name |
112
|
|
|
*/ |
113
|
|
|
private function getTable(string $table = null): string |
114
|
|
|
{ |
115
|
|
|
//If no table is passed, get the calling model name |
116
|
|
|
if ($table === null) { |
117
|
|
|
$reflect = new \ReflectionClass(get_class($this)); |
118
|
|
|
$table = $reflect->getShortName(); //this is to only get the model name, otherwise we get the full namespace |
119
|
|
|
$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 |
120
|
|
|
$table = strtolower($table); //the database names are in lowercase |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
if(Config::TABLE_PREFIX != '') |
|
|
|
|
124
|
|
|
{ |
125
|
|
|
$table = Config::TABLE_PREFIX.'_'.$table; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
//see if table exists |
129
|
|
|
$sql = "SHOW TABLES LIKE :table"; |
130
|
|
|
$stmt = $this->dbh->prepare($sql); |
131
|
|
|
$stmt->bindValue(':table', $table, PDO::PARAM_STR); |
132
|
|
|
$stmt->execute(); |
133
|
|
|
$exists = $stmt->rowCount() > 0; //will return 1 if table exists or 0 if non existant |
134
|
|
|
|
135
|
|
|
if ($exists) { |
136
|
|
|
//the table exists |
137
|
|
|
return $table; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
//if we are here, then table doesn't exist, check for view |
141
|
|
|
$view = 'v_' . $table; |
142
|
|
|
$stmt->bindValue(':table', $view, PDO::PARAM_STR); |
143
|
|
|
$stmt->execute(); |
144
|
|
|
$exists = $stmt->rowCount() > 0; //will return 1 if table exists or 0 if non existant |
145
|
|
|
|
146
|
|
|
if ($exists) { |
147
|
|
|
//the view exists |
148
|
|
|
return $view; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
//neither table or view exists |
152
|
|
|
//throw an error |
153
|
|
|
throw new \Exception("Table or view $table doesn't exist"); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* checks if the result from a PDO query has any data. |
158
|
|
|
* If yes then we return the array |
159
|
|
|
* If debugging is enabled we throw an exception on no results |
160
|
|
|
* or we just return an empty array |
161
|
|
|
* @param mixed $result the PDO result of a query |
162
|
|
|
* @return array the result or empty |
163
|
|
|
* @throws \Exception if debugging is on and no result |
164
|
|
|
*/ |
165
|
|
|
private function returnArray($result): array |
166
|
|
|
{ |
167
|
|
|
if ($result) { |
168
|
|
|
return $result; |
169
|
|
|
} |
170
|
|
|
if (Config::DEV_ENVIRONMENT) { |
171
|
|
|
throw new \Exception("No results in database"); |
172
|
|
|
} |
173
|
|
|
return []; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* gets the entire table or view and returns the array |
178
|
|
|
* @param string $table the table to search in, if empty then get the table based on model name |
179
|
|
|
* @return array the results from database |
180
|
|
|
* @throws \ReflectionException |
181
|
|
|
*/ |
182
|
|
|
protected function getResultSet($table = ''): array |
183
|
|
|
{ |
184
|
|
|
$tableName = $this->getTable($table); |
185
|
|
|
$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. |
186
|
|
|
$this->query($sql); |
187
|
|
|
$this->execute(); |
188
|
|
|
$result = $this->stmt->fetchAll(); //returns an array or false if no results |
189
|
|
|
return $this->returnArray($result); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* gets the entire table or view and returns the array with a limit to the number of rows |
194
|
|
|
* @param string $table the table to search in, if empty then get the table based on model name |
195
|
|
|
* @param string $limit the limit of rows to return |
196
|
|
|
* @return array the results from database |
197
|
|
|
* @throws \ReflectionException |
198
|
|
|
*/ |
199
|
|
|
protected function getResultSetLimited($limit, $table = ''): array |
200
|
|
|
{ |
201
|
|
|
$tableName = $this->getTable($table); |
202
|
|
|
$sql = "SELECT * FROM $tableName LIMIT :limit"; |
203
|
|
|
$this->query($sql); |
204
|
|
|
$this->bind(':limit', $limit); |
205
|
|
|
$this->execute(); |
206
|
|
|
$result = $this->stmt->fetchAll(); //returns an array or false if no results |
207
|
|
|
return $this->returnArray($result); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* get's the result of SELECT * FROM table where idtable=$id |
212
|
|
|
* @param int $rowId searched id |
213
|
|
|
* @param string $table the table to search, if blank then we get the table or view based on the model name |
214
|
|
|
* @return array result or empty array |
215
|
|
|
* @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results) |
216
|
|
|
*/ |
217
|
|
|
protected function getRowById($rowId, $table = ''): array |
218
|
|
|
{ |
219
|
|
|
$tableName = $this->getTable($table); |
220
|
|
|
$idName = 'id' . $tableName; |
221
|
|
|
$sql = "SELECT * FROM $tableName WHERE $idName = :rowId"; |
222
|
|
|
$this->query($sql); |
223
|
|
|
$this->bind(':rowId', $rowId); |
224
|
|
|
$this->execute(); |
225
|
|
|
$result = $this->stmt->fetch(); |
226
|
|
|
return $this->returnArray($result); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* gets the row from the query SELECT * FROM table WHERE $columnName = $Value |
231
|
|
|
* @param string $columnName the column to search in. Does a regex check for security |
232
|
|
|
* @param string $value the value to search for |
233
|
|
|
* @param string $table the table to search, if blank then we get the table or view based on the model name |
234
|
|
|
* @return array the results or empty |
235
|
|
|
* @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results) |
236
|
|
|
* @throws \Exception if the column name consists of other characters than lower case, numbers and underscore for security |
237
|
|
|
*/ |
238
|
|
|
protected function getRowByColumn($columnName, $value, $table = ''): array |
239
|
|
|
{ |
240
|
|
|
$tableName = $this->getTable($table); |
241
|
|
|
$columnNameOk = preg_match("/^[a-z0-9_]+$/i", $columnName); //testing if column name only has lower case, numbers and underscore |
242
|
|
|
if (!$columnNameOk) { |
243
|
|
|
throw new \Exception("Syntax error : Column name \"$columnName\" is not legal"); |
244
|
|
|
} |
245
|
|
|
$sql = "SELECT * FROM $tableName WHERE $columnName = :value"; |
246
|
|
|
$this->query($sql); |
247
|
|
|
$this->bind(':value', $value); |
248
|
|
|
$this->execute(); |
249
|
|
|
$result = $this->stmt->fetch(); |
250
|
|
|
return $this->returnArray($result); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* get's the result of SELECT * FROM table where table_slug=$slug |
255
|
|
|
* @param string $slug the slug to look up |
256
|
|
|
* @param string $table the table to search, if blank then we get the table or view based on the model name |
257
|
|
|
* @return array result or empty array |
258
|
|
|
* @throws \ReflectionException (probably not, but will throw an exception if debugging is on and no results) |
259
|
|
|
*/ |
260
|
|
|
protected function getRowBySlug(string $slug, $table = ''): array |
261
|
|
|
{ |
262
|
|
|
$tableName = $this->getTable($table); |
263
|
|
|
$slugName = $tableName.'_slug'; |
264
|
|
|
$sql = "SELECT * FROM $tableName WHERE $slugName = :slug"; |
265
|
|
|
$this->query($sql); |
266
|
|
|
$this->bind(':slug', $slug); |
267
|
|
|
$this->execute(); |
268
|
|
|
$result = $this->stmt->fetch(); |
269
|
|
|
return $this->returnArray($result); |
270
|
|
|
} |
271
|
|
|
} |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.