Completed
Push — master ( 78dfa0...62e025 )
by
unknown
03:38
created

SQLConnection::reduce()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 7
c 1
b 0
f 1
nc 7
nop 4
dl 0
loc 10
rs 8.8571
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.com
11
 */
12
13
14
class SQL {
15
  use Module, Events;
16
17
  protected static $connections = [],
18
                   $current     = 'default';
19
20
  /**
21
   * Register a new datasource
22
   * @param  string $name     The assigned name for the datasource
23
   * @param  string $dsn      PDO DSN URL
24
   * @param  string $username User credentials
25
   * @param  string $password User credentials
26
   * @param  array  $options  Options to pass to the PDO constructor
27
   * @return SQLConnection    The datasource resource
28
   */
29
  public static function register($name, $dsn, $username=null, $password=null, $options=[]){
30
    return self::$connections[$name] = new SQLConnection($dsn, $username, $password, $options);
31
  }
32
33
  /**
34
   * Register the default datasource
35
   * @param  string $dsn      PDO DSN URL
36
   * @param  string $username User credentials
37
   * @param  string $password User credentials
38
   * @param  array  $options  Options to pass to the PDO constructor
39
   * @return SQLConnection    The datasource resource
40
   */
41
  public static function connect($dsn, $username=null, $password=null, $options=[]){
42
    return self::register('default', $dsn, $username, $password, $options);
43
  }
44
45
  /**
46
   * Bind the default datasource to another named connection
47
   * @param  string $name The datasource name
48
   * @return bool       `true` if correctly changed
49
   */
50
  public static function defaultTo($name){
51 View Code Duplication
    if (isset(self::$connections[$name])){
52
      self::$current = $name;
53
      return true;
54
    } else return false;
55
  }
56
57
  /**
58
   * Close one or all (if no parameter passed) registered datasource connections
59
   * @param  string $name The datasource name, omit for close all of them
60
   * @return bool       `true` if one or more datasource where closed
61
   */
62
  public static function close($name=null){
63
    if ($name === null) {
64
      foreach (self::$connections as $conn) $conn->close();
65
      return true;
66 View Code Duplication
    } else if (isset(self::$connections[$name])){
67
      self::$connections[$name]->close();
68
      return true;
69
    } else return false;
70
  }
71
72
  /**
73
   * Datasource connection accessor
74
   * @param  strinf $name The datasource name
75
   * @return SQLConnect   The datasource connection
76
   */
77
  public static function using($name){
78
    if (empty(self::$connections[$name])) throw new \Exception("[SQL] Unknown connection named '$name'.");
79
    return self::$connections[$name];
80
  }
81
82
  /**
83
   * Proxy all direct static calls to the SQL module to the `default` datasource
84
   * @param  string $method The method name
85
   * @param  array $args    The method arguments
86
   * @return mixed          The method return value
87
   */
88
  public static function __callStatic($method, $args){
89
    if (empty(self::$connections[self::$current])) throw new \Exception("[SQL] No default connection defined.");
90
    return call_user_func_array([self::$connections[self::$current],$method],$args);
91
  }
92
93
}
94
95
// Default connection to in-memory ephemeral database
96
SQL::connect('sqlite::memory:');
97
98
99
class SQLConnection {
100
101
  protected $connection        = [],
102
            $queries           = [],
103
            $last_exec_success = true;
104
105
  public function __construct($dsn, $username=null, $password=null, $options=[]){
106
    $this->connection = [
107
      'dsn'        => $dsn,
108
      'pdo'        => null,
109
      'username'   => $username,
110
      'password'   => $password,
111
      'options'    => array_merge([
112
        PDO::ATTR_ERRMODE                => PDO::ERRMODE_EXCEPTION,
113
        PDO::ATTR_DEFAULT_FETCH_MODE     => PDO::FETCH_ASSOC,
114
        PDO::ATTR_EMULATE_PREPARES       => true,
115
        PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,
116
      ], $options),
117
    ];
118
    // "The auto-commit mode cannot be changed for this driver" SQLite workaround
119
    if (strpos($dsn,'sqlite:') === 0) {
120
      $this->connection['options'] = $options;
121
    }
122
  }
123
124
  public function close(){
125
    $this->connection['pdo'] = null;
126
  }
127
128
  public function connection(){
129
    if(empty($this->connection['pdo'])) {
130
      try {
131
        $this->connection['pdo'] = new PDO(
132
            $this->connection['dsn'],
133
            $this->connection['username'],
134
            $this->connection['password'],
135
            $this->connection['options']
136
137
        );
138
        SQL::trigger('connect',$this);
139
        Event::trigger('core.sql.connect',$this);
140
      } catch(Exception $e) {
141
        $this->connection['pdo'] = null;
142
      }
143
    }
144
    return $this->connection['pdo'];
145
  }
146
147
  public function prepare($query, $pdo_params=[]){
148
    if(!$this->connection()) return false;
149
    return isset($this->queries[$query]) ? $this->queries[$query] : ($this->queries[$query] = $this->connection()->prepare($query, $pdo_params));
150
  }
151
152
  public function exec($query, $params=[], $pdo_params=[]){
153
    if(!$this->connection()) return false;
154
155
    if (false==is_array($params)) $params = (array)$params;
156
    $query = Filter::with('core.sql.query',$query);
157
    if($statement = $this->prepare($query, $pdo_params)){
158
      SQL::trigger('query',$query,$params,(bool)$statement);
159
      Event::trigger('core.sql.query',$query,$params,(bool)$statement);
160
161
      foreach ($params as $key => $val) {
162
        $type = PDO::PARAM_STR;
163
        if (is_bool($val)) {
164
          $type = PDO::PARAM_BOOL;
165
        } elseif (is_null($val)) {
166
          $type = PDO::PARAM_NULL;
167
        } elseif (is_int($val)) {
168
          $type = PDO::PARAM_INT;
169
        }
170
171
        // bindValue need a 1-based numeric parameter
172
        $statement->bindValue(is_numeric($key)?$key+1:':'.$key, $val, $type);
173
      }
174
    } else {
175
      static::trigger('error',$query,$params);
0 ignored issues
show
Bug introduced by
The method trigger() does not seem to exist on object<SQLConnection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
176
      Event::trigger('core.sql.error',$query,$params);
177
      return false;
178
    }
179
180
    $this->last_exec_success = $statement && $statement->execute();
181
    return $statement;
182
  }
183
184
  public function value($query, $params=[], $column=0){
185
    if(!$this->connection()) return false;
186
187
    $res = $this->exec($query,$params);
188
    return $res ? $res->fetchColumn($column) : null;
189
  }
190
191
  public function column($query, $params=[], $column=0){
192
    if(!$this->connection()) return false;
193
194
    $results = [];
195
    $res     = $this->exec($query,$params);
196
197
    if (is_string($column))
198
      while ($x = $res->fetch(PDO::FETCH_OBJ)) $results[] = $x->$column;
199
    else
200
      while ($x = $res->fetchColumn($column)) $results[] = $x;
201
202
    return $results;
203
  }
204
205
  public function reduce($query, $params=[], $looper = null, $initial = null){
206
    if(!$this->connection()) return false;
207
208
    // ($query,$looper,$initial) shorthand
209
    if (is_callable($params)) { $initial = $looper; $looper = $params; $params = []; }
210
    if(( $res = $this->exec($query,$params, [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true]) ) && is_callable($looper) ){
211
      while ($row = $res->fetchObject()) { $initial = $looper($row, $initial); }
212
      return $initial;
213
    } else return false;
214
  }
215
216
  public function each($query, $params=[], callable $looper = null){
217
    if(!$this->connection()) return false;
218
219
    // ($query,$looper) shorthand
220
    if ($looper===null && is_callable($params)) {$looper = $params; $params = [];}
221
    if( $res = $this->exec($query,$params, [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true]) ){
222
      if(is_callable($looper)) {
223
        while ($row = $res->fetchObject()) $looper($row);
224
        return true;
225
      } else return $res->fetchAll(PDO::FETCH_CLASS);
226
    } else return false;
227
  }
228
229
  public function single($query, $params=[], callable $handler = null){
230
    if(!$this->connection()) return false;
231
232
    // ($query,$handler) shorthand
233
    if ($handler===null && is_callable($params)) {$handler = $params; $params = [];}
234
    if( $res = $this->exec($query,$params, [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true]) ){
235
        if (is_callable($handler))
236
          return $handler($res->fetchObject());
237
        else
238
          return $res->fetchObject();
239
    }
240
  }
241
242
 public function run($script){
243
    if(!$this->connection()) return false;
244
245
    $sql_path = Options::get('database.sql.path',APP_DIR.'/sql');
246
    $sql_sep  = Options::get('database.sql.separator',';');
247
    if (is_file($f = "$sql_path/$script.sql")){
248
        $result = true;
249
        foreach(explode($sql_sep,file_get_contents($f)) as $statement) {
250
            $result = $this->exec($statement);
251
        }
252
        return $result;
253
    } else return false;
254
  }
255
256
  public function all($query, $params=[], callable $looper = null){
257
   if(!$this->connection()) return false;
258
   return $this->each($query,$params,$looper);
259
  }
260
261
  public function delete($table, $pks=null, $pk='id', $inclusive=true){
262
    if(!$this->connection()) return false;
263
264
    if (null===$pks) {
265
      return $this->exec("DELETE FROM `$table`");
266
    } else {
267
      return $this->exec("DELETE FROM `$table` WHERE `$pk` ".($inclusive ? "" : "NOT " )."IN (" . implode( ',', array_fill_keys( (array)$pks, '?' ) ) . ")",(array)$pks);
268
    }
269
  }
270
271
  public function insert($table, $data){
272
    if(!$this->connection()) return false;
273
274
    if (false==is_array($data)) $data = (array)$data;
275
    $k = array_keys($data);
276
    asort($k);
277
    $pk = $k;
278
    array_walk($pk,function(&$e){ $e = ':'.$e;});
279
    $q = "INSERT INTO `$table` (`".implode('`,`',$k)."`) VALUES (".implode(',',$pk).")";
280
    $this->exec($q,$data);
281
    return $this->last_exec_success ? $this->connection()->lastInsertId() : false;
282
  }
283
284
  public function updateWhere($table, $data, $where, $pk='id'){
285
    if(!$this->connection()) return false;
286
287
    if (false==is_array($data)) $data = (array)$data;
288
    if (empty($data)) return false;
289
    $k = array_keys($data);
290
    asort($k);
291
292
    // Remove primary key from SET
293
    array_walk($k,function(&$e) use ($pk) {
294
      $e = ($e==$pk) ? null : "`$e`=:$e";
295
    });
296
297
    $q = "UPDATE `$table` SET ".implode(', ',array_filter($k))." WHERE $where";
298
    $this->exec($q,$data);
299
    return $this->last_exec_success;
300
  }
301
302
  public function update($table, $data, $pk='id', $extra_where=''){
303
    return $this->updateWhere($table, $data, "`$pk`=:$pk $extra_where", $pk);
304
  }
305
306
  public function insertOrUpdate($table, $data=[], $pk='id', $extra_where=''){
307
    if(!$this->connection()) return false;
308
309
    if (false==is_array($data)) $data = (array)$data;
310
    if (empty($data[$pk])) return $this->insert($table, $data);
311
    if( (string) $this->value("SELECT `$pk` FROM `$table` WHERE `$pk`=? LIMIT 1", [$data[$pk]]) === (string) $data[$pk] ){
312
        return $this->update($table, $data, $pk, $extra_where);
313
    } else {
314
        return $this->insert($table, $data);
315
    }
316
  }
317
}
318
319