|
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
|
|
|
public static function register($name, $dsn, $username=null, $password=null, $options=[]){ |
|
20
|
|
|
return self::$connections[$name] = new SQLConnection($dsn, $username, $password, $options); |
|
21
|
|
|
} |
|
22
|
|
|
|
|
23
|
|
|
public static function connect($dsn, $username=null, $password=null, $options=[]){ |
|
24
|
|
|
return self::register('default', $dsn, $username, $password, $options); |
|
25
|
|
|
} |
|
26
|
|
|
|
|
27
|
|
|
public static function defaultTo($name){ |
|
28
|
|
View Code Duplication |
if (isset(self::$connections[$name])){ |
|
|
|
|
|
|
29
|
|
|
self::$current = $name; |
|
30
|
|
|
return true; |
|
31
|
|
|
} else return false; |
|
32
|
|
|
} |
|
33
|
|
|
|
|
34
|
|
|
public static function close($name=null){ |
|
35
|
|
|
if ($name === null) { |
|
36
|
|
|
foreach (self::$connections as $conn) $conn->close(); |
|
37
|
|
|
return true; |
|
38
|
|
View Code Duplication |
} else if (isset(self::$connections[$name])){ |
|
|
|
|
|
|
39
|
|
|
self::$connections->close(); |
|
|
|
|
|
|
40
|
|
|
return true; |
|
41
|
|
|
} else return false; |
|
42
|
|
|
} |
|
43
|
|
|
|
|
44
|
|
|
public static function using($name){ |
|
45
|
|
|
if (empty(self::$connections[$name])) throw new \Exception("[SQL] Unknown connection named '$name'."); |
|
46
|
|
|
return self::$connections[$name]; |
|
47
|
|
|
} |
|
48
|
|
|
|
|
49
|
|
|
public static function __callStatic($method, $args){ |
|
50
|
|
|
if (empty(self::$connections[self::$current])) throw new \Exception("[SQL] No default connection defined."); |
|
51
|
|
|
return call_user_func_array([self::$connections[self::$current],$method],$args); |
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
|
|
// Default connection to in-memory ephemeral database |
|
57
|
|
|
SQL::connect('sqlite::memory:'); |
|
58
|
|
|
|
|
59
|
|
|
|
|
60
|
|
|
class SQLConnection { |
|
61
|
|
|
|
|
62
|
|
|
protected $connection = [], |
|
63
|
|
|
$queries = [], |
|
64
|
|
|
$last_exec_success = true; |
|
65
|
|
|
|
|
66
|
|
|
public function __construct($dsn, $username=null, $password=null, $options=[]){ |
|
67
|
|
|
$this->connection = [ |
|
68
|
|
|
'dsn' => $dsn, |
|
69
|
|
|
'pdo' => null, |
|
70
|
|
|
'username' => $username, |
|
71
|
|
|
'password' => $password, |
|
72
|
|
|
'options' => array_merge([ |
|
73
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, |
|
74
|
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, |
|
75
|
|
|
PDO::ATTR_EMULATE_PREPARES => true, |
|
76
|
|
|
],$options), |
|
77
|
|
|
]; |
|
78
|
|
|
// "The auto-commit mode cannot be changed for this driver" SQLite workaround |
|
79
|
|
|
if (strpos($dsn,'sqlite:') === 0) { |
|
80
|
|
|
$this->connection['options'] = $options; |
|
81
|
|
|
} |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
public function close(){ |
|
85
|
|
|
$this->connection['pdo'] = null; |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
public function onConnect($callbkack){ |
|
|
|
|
|
|
89
|
|
|
$this->connection['pdo'] = null; |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
public function connection(){ |
|
93
|
|
|
if(empty($this->connection['pdo'])) { |
|
94
|
|
|
try { |
|
95
|
|
|
$this->connection['pdo'] = new PDO( |
|
96
|
|
|
$this->connection['dsn'], |
|
97
|
|
|
$this->connection['username'], |
|
98
|
|
|
$this->connection['password'], |
|
99
|
|
|
$this->connection['options'] |
|
100
|
|
|
|
|
101
|
|
|
); |
|
102
|
|
|
Event::trigger('core.sql.connect',$this); |
|
103
|
|
|
} catch(Exception $e) { |
|
104
|
|
|
$this->connection['pdo'] = null; |
|
105
|
|
|
} |
|
106
|
|
|
} |
|
107
|
|
|
return $this->connection['pdo']; |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
public function prepare($query){ |
|
111
|
|
|
if(!$this->connection()) return false; |
|
112
|
|
|
return isset($this->queries[$query]) ? $this->queries[$query] : ($this->queries[$query] = $this->connection()->prepare($query)); |
|
113
|
|
|
} |
|
114
|
|
|
|
|
115
|
|
|
public function exec($query, $params=[]){ |
|
116
|
|
|
if(!$this->connection()) return false; |
|
117
|
|
|
|
|
118
|
|
|
if (false==is_array($params)) $params = (array)$params; |
|
119
|
|
|
$query = Filter::with('core.sql.query',$query); |
|
120
|
|
|
if($statement = $this->prepare($query)){ |
|
121
|
|
|
Event::trigger('core.sql.query',$query,$params,(bool)$statement); |
|
122
|
|
|
|
|
123
|
|
|
foreach ($params as $key => $val) { |
|
124
|
|
|
$type = PDO::PARAM_STR; |
|
125
|
|
|
if (is_bool($val)) { |
|
126
|
|
|
$type = PDO::PARAM_BOOL; |
|
127
|
|
|
} elseif (is_null($val)) { |
|
128
|
|
|
$type = PDO::PARAM_NULL; |
|
129
|
|
|
} elseif (is_int($val)) { |
|
130
|
|
|
$type = PDO::PARAM_INT; |
|
131
|
|
|
} |
|
132
|
|
|
// bindValue need a 1-based numeric parameter |
|
133
|
|
|
$statement->bindValue(is_numeric($key)?$key+1:':'.$key, $val, $type); |
|
134
|
|
|
} |
|
135
|
|
|
} else { |
|
136
|
|
|
Event::trigger('core.sql.error',$query,$params); |
|
137
|
|
|
return false; |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
$this->last_exec_success = $statement && $statement->execute(); |
|
141
|
|
|
return $statement; |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
public function value($query, $params=[], $column=0){ |
|
145
|
|
|
if(!$this->connection()) return false; |
|
146
|
|
|
|
|
147
|
|
|
$res = $this->exec($query,$params); |
|
148
|
|
|
return $res ? $res->fetchColumn($column) : null; |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
View Code Duplication |
public function each($query, $params=[], callable $looper = null){ |
|
|
|
|
|
|
152
|
|
|
if(!$this->connection()) return false; |
|
153
|
|
|
|
|
154
|
|
|
// ($query,$looper) shorthand |
|
155
|
|
|
if ($looper===null && is_callable($params)) {$looper = $params; $params = [];} |
|
156
|
|
|
if( $res = $this->exec($query,$params) ){ |
|
157
|
|
|
if(is_callable($looper)) |
|
158
|
|
|
while ($row = $res->fetchObject()) $looper($row); |
|
159
|
|
|
else |
|
160
|
|
|
return $res->fetchAll(PDO::FETCH_CLASS); |
|
161
|
|
|
} |
|
162
|
|
|
} |
|
163
|
|
|
|
|
164
|
|
View Code Duplication |
public function single($query, $params=[], callable $handler = null){ |
|
|
|
|
|
|
165
|
|
|
if(!$this->connection()) return false; |
|
166
|
|
|
|
|
167
|
|
|
// ($query,$handler) shorthand |
|
168
|
|
|
if ($handler===null && is_callable($params)) {$handler = $params; $params = [];} |
|
169
|
|
|
if( $res = $this->exec($query,$params) ){ |
|
170
|
|
|
if (is_callable($handler)) |
|
171
|
|
|
$handler($res->fetchObject()); |
|
172
|
|
|
else |
|
173
|
|
|
return $res->fetchObject(); |
|
174
|
|
|
} |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
public function run($script){ |
|
178
|
|
|
if(!$this->connection()) return false; |
|
179
|
|
|
|
|
180
|
|
|
$sql_path = Options::get('database.sql.path',APP_DIR.'/sql'); |
|
181
|
|
|
$sql_sep = Options::get('database.sql.separator',';'); |
|
182
|
|
|
if (is_file($f = "$sql_path/$script.sql")){ |
|
183
|
|
|
$result = true; |
|
184
|
|
|
foreach(explode($sql_sep,file_get_contents($f)) as $statement) { |
|
185
|
|
|
$result = $this->exec($statement); |
|
186
|
|
|
} |
|
187
|
|
|
return $result; |
|
188
|
|
|
} else return false; |
|
189
|
|
|
} |
|
190
|
|
|
|
|
191
|
|
|
public function all($query, $params=[], callable $looper = null){ |
|
192
|
|
|
if(!$this->connection()) return false; |
|
193
|
|
|
return $this->each($query,$params,$looper); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
public function delete($table, $pks=null, $pk='id', $inclusive=true){ |
|
197
|
|
|
if(!$this->connection()) return false; |
|
198
|
|
|
|
|
199
|
|
|
if (null===$pks) { |
|
200
|
|
|
return $this->exec("DELETE FROM `$table`"); |
|
201
|
|
|
} else { |
|
202
|
|
|
return $this->exec("DELETE FROM `$table` WHERE `$pk` ".($inclusive ? "" : "NOT " )."IN (" . implode( ',', array_fill_keys( (array)$pks, '?' ) ) . ")",(array)$pks); |
|
203
|
|
|
} |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
public function insert($table, $data=[]){ |
|
207
|
|
|
if(!$this->connection()) return false; |
|
208
|
|
|
|
|
209
|
|
|
if (false==is_array($data)) $data = (array)$data; |
|
210
|
|
|
$k = array_keys($data); |
|
211
|
|
|
asort($k); |
|
212
|
|
|
$pk = $k; |
|
213
|
|
|
array_walk($pk,function(&$e){ $e = ':'.$e;}); |
|
214
|
|
|
$q = "INSERT INTO `$table` (`".implode('`,`',$k)."`) VALUES (".implode(',',$pk).")"; |
|
215
|
|
|
$this->exec($q,$data); |
|
216
|
|
|
return $this->last_exec_success ? $this->connection()->lastInsertId() : false; |
|
217
|
|
|
} |
|
218
|
|
|
|
|
219
|
|
|
public function updateWhere($table, $data=[], $where){ |
|
220
|
|
|
if(!$this->connection()) return false; |
|
221
|
|
|
|
|
222
|
|
|
if (false==is_array($data)) $data = (array)$data; |
|
223
|
|
|
if (empty($data)) return false; |
|
224
|
|
|
$k = array_keys($data); |
|
225
|
|
|
asort($k); |
|
226
|
|
|
array_walk($k,function(&$e){ $e = "`$e`=:$e";}); |
|
227
|
|
|
$q = "UPDATE `$table` SET ".implode(', ',$k)." WHERE $where"; |
|
228
|
|
|
$this->exec($q,$data); |
|
229
|
|
|
return $this->last_exec_success; |
|
230
|
|
|
} |
|
231
|
|
|
|
|
232
|
|
|
public function update($table, $data=[], $pk='id', $extra_where=''){ |
|
233
|
|
|
return $this->updateWhere($table, $data, "`$pk`=:$pk $extra_where"); |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
public function insertOrUpdate($table, $data=[], $pk='id', $extra_where=''){ |
|
237
|
|
|
if(!$this->connection()) return false; |
|
238
|
|
|
|
|
239
|
|
|
if (false==is_array($data)) $data = (array)$data; |
|
240
|
|
|
if (empty($data[$pk])) return $this->insert($table, $data); |
|
241
|
|
|
if( (string) $this->value("SELECT `$pk` FROM `$table` WHERE `$pk`=? LIMIT 1", [$data[$pk]]) === (string) $data[$pk] ){ |
|
242
|
|
|
return $this->update($table, $data, $pk, $extra_where); |
|
243
|
|
|
} else { |
|
244
|
|
|
return $this->insert($table, $data); |
|
245
|
|
|
} |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
|
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.