1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* SQL |
5
|
|
|
* |
6
|
|
|
* SQL database access via PDO. |
7
|
|
|
* |
8
|
|
|
* @package core |
9
|
|
|
* @author [email protected] |
10
|
|
|
* @copyright Caffeina srl - 2015 - http://caffeina.it |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
class SQL { |
15
|
|
|
use Module; |
16
|
|
|
protected static $connections = [], |
17
|
|
|
$current = 'default'; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Register a new datasource |
21
|
|
|
* @param string $name The assigned name for the datasource |
22
|
|
|
* @param string $dsn PDO DSN URL |
23
|
|
|
* @param string $username User credentials |
24
|
|
|
* @param string $password User credentials |
25
|
|
|
* @param array $options Options to pass to the PDO constructor |
26
|
|
|
* @return SQLConnection The datasource resource |
27
|
|
|
*/ |
28
|
|
|
public static function register($name, $dsn, $username=null, $password=null, $options=[]){ |
29
|
|
|
return self::$connections[$name] = new SQLConnection($dsn, $username, $password, $options); |
30
|
|
|
} |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Register the default datasource |
34
|
|
|
* @param string $dsn PDO DSN URL |
35
|
|
|
* @param string $username User credentials |
36
|
|
|
* @param string $password User credentials |
37
|
|
|
* @param array $options Options to pass to the PDO constructor |
38
|
|
|
* @return SQLConnection The datasource resource |
39
|
|
|
*/ |
40
|
|
|
public static function connect($dsn, $username=null, $password=null, $options=[]){ |
41
|
|
|
return self::register('default', $dsn, $username, $password, $options); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Bind the default datasource to another named connection |
46
|
|
|
* @param string $name The datasource name |
47
|
|
|
* @return bool `true` if correctly changed |
48
|
|
|
*/ |
49
|
|
|
public static function defaultTo($name){ |
50
|
|
View Code Duplication |
if (isset(self::$connections[$name])){ |
51
|
|
|
self::$current = $name; |
52
|
|
|
return true; |
53
|
|
|
} else return false; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Close one or all (if no parameter passed) registered datasource connections |
58
|
|
|
* @param string $name The datasource name, omit for close all of them |
59
|
|
|
* @return bool `true` if one or more datasource where closed |
60
|
|
|
*/ |
61
|
|
|
public static function close($name=null){ |
62
|
|
|
if ($name === null) { |
63
|
|
|
foreach (self::$connections as $conn) $conn->close(); |
64
|
|
|
return true; |
65
|
|
View Code Duplication |
} else if (isset(self::$connections[$name])){ |
66
|
|
|
self::$connections[$name]->close(); |
67
|
|
|
return true; |
68
|
|
|
} else return false; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Datasource connection accessor |
73
|
|
|
* @param strinf $name The datasource name |
74
|
|
|
* @return SQLConnect The datasource connection |
75
|
|
|
*/ |
76
|
|
|
public static function using($name){ |
77
|
|
|
if (empty(self::$connections[$name])) throw new \Exception("[SQL] Unknown connection named '$name'."); |
78
|
|
|
return self::$connections[$name]; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Proxy all direct static calls to the SQL module to the `default` datasource |
83
|
|
|
* @param string $method The method name |
84
|
|
|
* @param array $args The method arguments |
85
|
|
|
* @return mixed The method return value |
86
|
|
|
*/ |
87
|
|
|
public static function __callStatic($method, $args){ |
88
|
|
|
if (empty(self::$connections[self::$current])) throw new \Exception("[SQL] No default connection defined."); |
89
|
|
|
return call_user_func_array([self::$connections[self::$current],$method],$args); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
// Default connection to in-memory ephemeral database |
95
|
|
|
SQL::connect('sqlite::memory:'); |
96
|
|
|
|
97
|
|
|
|
98
|
|
|
class SQLConnection { |
99
|
|
|
|
100
|
|
|
protected $connection = [], |
101
|
|
|
$queries = [], |
102
|
|
|
$last_exec_success = true; |
103
|
|
|
|
104
|
|
|
public function __construct($dsn, $username=null, $password=null, $options=[]){ |
105
|
|
|
$this->connection = [ |
106
|
|
|
'dsn' => $dsn, |
107
|
|
|
'pdo' => null, |
108
|
|
|
'username' => $username, |
109
|
|
|
'password' => $password, |
110
|
|
|
'options' => array_merge([ |
111
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, |
112
|
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, |
113
|
|
|
PDO::ATTR_EMULATE_PREPARES => true, |
114
|
|
|
],$options), |
115
|
|
|
]; |
116
|
|
|
// "The auto-commit mode cannot be changed for this driver" SQLite workaround |
117
|
|
|
if (strpos($dsn,'sqlite:') === 0) { |
118
|
|
|
$this->connection['options'] = $options; |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
public function close(){ |
123
|
|
|
$this->connection['pdo'] = null; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
public function connection(){ |
127
|
|
|
if(empty($this->connection['pdo'])) { |
128
|
|
|
try { |
129
|
|
|
$this->connection['pdo'] = new PDO( |
130
|
|
|
$this->connection['dsn'], |
131
|
|
|
$this->connection['username'], |
132
|
|
|
$this->connection['password'], |
133
|
|
|
$this->connection['options'] |
134
|
|
|
|
135
|
|
|
); |
136
|
|
|
Event::trigger('core.sql.connect',$this); |
137
|
|
|
} catch(Exception $e) { |
138
|
|
|
$this->connection['pdo'] = null; |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
return $this->connection['pdo']; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
public function prepare($query){ |
145
|
|
|
if(!$this->connection()) return false; |
146
|
|
|
return isset($this->queries[$query]) ? $this->queries[$query] : ($this->queries[$query] = $this->connection()->prepare($query)); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function exec($query, $params=[]){ |
150
|
|
|
if(!$this->connection()) return false; |
151
|
|
|
|
152
|
|
|
if (false==is_array($params)) $params = (array)$params; |
153
|
|
|
$query = Filter::with('core.sql.query',$query); |
154
|
|
|
if($statement = $this->prepare($query)){ |
155
|
|
|
Event::trigger('core.sql.query',$query,$params,(bool)$statement); |
156
|
|
|
|
157
|
|
|
foreach ($params as $key => $val) { |
158
|
|
|
$type = PDO::PARAM_STR; |
159
|
|
|
if (is_bool($val)) { |
160
|
|
|
$type = PDO::PARAM_BOOL; |
161
|
|
|
} elseif (is_null($val)) { |
162
|
|
|
$type = PDO::PARAM_NULL; |
163
|
|
|
} elseif (is_int($val)) { |
164
|
|
|
$type = PDO::PARAM_INT; |
165
|
|
|
} |
166
|
|
|
// bindValue need a 1-based numeric parameter |
167
|
|
|
$statement->bindValue(is_numeric($key)?$key+1:':'.$key, $val, $type); |
168
|
|
|
} |
169
|
|
|
} else { |
170
|
|
|
Event::trigger('core.sql.error',$query,$params); |
171
|
|
|
return false; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
$this->last_exec_success = $statement && $statement->execute(); |
175
|
|
|
return $statement; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
public function value($query, $params=[], $column=0){ |
179
|
|
|
if(!$this->connection()) return false; |
180
|
|
|
|
181
|
|
|
$res = $this->exec($query,$params); |
182
|
|
|
return $res ? $res->fetchColumn($column) : null; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function column($query, $params=[], $column=0){ |
186
|
|
|
if(!$this->connection()) return false; |
187
|
|
|
|
188
|
|
|
$results = []; |
189
|
|
|
$res = $this->exec($query,$params); |
190
|
|
|
|
191
|
|
|
if (is_string($column)) |
192
|
|
|
while ($x = $res->fetch(PDO::FETCH_OBJ)) $results[] = $x->$column; |
193
|
|
|
else |
194
|
|
|
while ($x = $res->fetchColumn($column)) $results[] = $x; |
195
|
|
|
|
196
|
|
|
return $results; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
View Code Duplication |
public function each($query, $params=[], callable $looper = null){ |
|
|
|
|
200
|
|
|
if(!$this->connection()) return false; |
201
|
|
|
|
202
|
|
|
// ($query,$looper) shorthand |
203
|
|
|
if ($looper===null && is_callable($params)) {$looper = $params; $params = [];} |
204
|
|
|
if( $res = $this->exec($query,$params) ){ |
205
|
|
|
if(is_callable($looper)) |
206
|
|
|
while ($row = $res->fetchObject()) $looper($row); |
207
|
|
|
else |
208
|
|
|
return $res->fetchAll(PDO::FETCH_CLASS); |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
View Code Duplication |
public function single($query, $params=[], callable $handler = null){ |
|
|
|
|
213
|
|
|
if(!$this->connection()) return false; |
214
|
|
|
|
215
|
|
|
// ($query,$handler) shorthand |
216
|
|
|
if ($handler===null && is_callable($params)) {$handler = $params; $params = [];} |
217
|
|
|
if( $res = $this->exec($query,$params) ){ |
218
|
|
|
if (is_callable($handler)) |
219
|
|
|
$handler($res->fetchObject()); |
220
|
|
|
else |
221
|
|
|
return $res->fetchObject(); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
public function run($script){ |
226
|
|
|
if(!$this->connection()) return false; |
227
|
|
|
|
228
|
|
|
$sql_path = Options::get('database.sql.path',APP_DIR.'/sql'); |
229
|
|
|
$sql_sep = Options::get('database.sql.separator',';'); |
230
|
|
|
if (is_file($f = "$sql_path/$script.sql")){ |
231
|
|
|
$result = true; |
232
|
|
|
foreach(explode($sql_sep,file_get_contents($f)) as $statement) { |
233
|
|
|
$result = $this->exec($statement); |
234
|
|
|
} |
235
|
|
|
return $result; |
236
|
|
|
} else return false; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
public function all($query, $params=[], callable $looper = null){ |
240
|
|
|
if(!$this->connection()) return false; |
241
|
|
|
return $this->each($query,$params,$looper); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
public function delete($table, $pks=null, $pk='id', $inclusive=true){ |
245
|
|
|
if(!$this->connection()) return false; |
246
|
|
|
|
247
|
|
|
if (null===$pks) { |
248
|
|
|
return $this->exec("DELETE FROM `$table`"); |
249
|
|
|
} else { |
250
|
|
|
return $this->exec("DELETE FROM `$table` WHERE `$pk` ".($inclusive ? "" : "NOT " )."IN (" . implode( ',', array_fill_keys( (array)$pks, '?' ) ) . ")",(array)$pks); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
public function insert($table, $data){ |
255
|
|
|
if(!$this->connection()) return false; |
256
|
|
|
|
257
|
|
|
if (false==is_array($data)) $data = (array)$data; |
258
|
|
|
$k = array_keys($data); |
259
|
|
|
asort($k); |
260
|
|
|
$pk = $k; |
261
|
|
|
array_walk($pk,function(&$e){ $e = ':'.$e;}); |
262
|
|
|
$q = "INSERT INTO `$table` (`".implode('`,`',$k)."`) VALUES (".implode(',',$pk).")"; |
263
|
|
|
$this->exec($q,$data); |
264
|
|
|
return $this->last_exec_success ? $this->connection()->lastInsertId() : false; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
public function updateWhere($table, $data, $where, $pk='id'){ |
268
|
|
|
if(!$this->connection()) return false; |
269
|
|
|
|
270
|
|
|
if (false==is_array($data)) $data = (array)$data; |
271
|
|
|
if (empty($data)) return false; |
272
|
|
|
$k = array_keys($data); |
273
|
|
|
asort($k); |
274
|
|
|
|
275
|
|
|
// Remove primary key from SET |
276
|
|
|
array_walk($k,function(&$e) use ($pk) { |
277
|
|
|
$e = ($e==$pk) ? null : "`$e`=:$e"; |
278
|
|
|
}); |
279
|
|
|
|
280
|
|
|
$q = "UPDATE `$table` SET ".implode(', ',array_filter($k))." WHERE $where"; |
281
|
|
|
$this->exec($q,$data); |
282
|
|
|
return $this->last_exec_success; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
public function update($table, $data, $pk='id', $extra_where=''){ |
286
|
|
|
return $this->updateWhere($table, $data, "`$pk`=:$pk $extra_where", $pk); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
public function insertOrUpdate($table, $data=[], $pk='id', $extra_where=''){ |
290
|
|
|
if(!$this->connection()) return false; |
291
|
|
|
|
292
|
|
|
if (false==is_array($data)) $data = (array)$data; |
293
|
|
|
if (empty($data[$pk])) return $this->insert($table, $data); |
294
|
|
|
if( (string) $this->value("SELECT `$pk` FROM `$table` WHERE `$pk`=? LIMIT 1", [$data[$pk]]) === (string) $data[$pk] ){ |
295
|
|
|
return $this->update($table, $data, $pk, $extra_where); |
296
|
|
|
} else { |
297
|
|
|
return $this->insert($table, $data); |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.