1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
| <?php
/**
* title: PDO wrapper
* description: Hybrid db() interface for extended SQL parameterization and result folding
* api: php
* type: database
* version: 0.9.9
* depends: php:pdo
* license: Public Domain
* author: Mario Salzer
* doc: http://fossil.include-once.org/hybrid7/wiki/db
*
*
* QUERY
*
* Provides simple database queries with enumerated / named parameters. It's
* flexible in accepting plain PDO scalar arguments or arrays. Array args get
* merged, or transcribed when special placeholders are present:
*
* $r = db("SELECT * FROM tbl WHERE a>=? AND b IN (??)", $a, array($b, $c));
*
* Extended placeholder syntax and common uses:
*
* βββββββ¬βββββββββββββββ¬ββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
* βPlcH β Expands to β Context β Purpose β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β ?? β ?, ?, ? β IN β Expansion of indexed arrays. β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :: β :a, :b β VALUES β Expand associative arrays into named β
* β β β β value list. β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :? β βaβ, βbβ β Names β Interpolates key names (does not paβ β
* β β β β rameterize values). β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :& β x=:x β WHERE β Becomes ANDβjoined name=:value list. β
* β β AND y=:y β β β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :| β x=:x β WHERE β Becomes ORβjoined name=:value list. β
* β β OR y=:y β β β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :, β x=:x, y=:y β UPDATE β Becomes commaβjoined name=:value list. β
* βββββββΌβββββββββββββββΌββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
* β :* β SQL + params β Expr β Expression placeholder, where the asβ β
* β β β β sociated argument should contain an β
* β β β β array ["AND foo IN (??)", $params]. β
* β β β β Which only interpolates if $params β
* β β β β contains any value. Can be nested. β
* βββββββ΄βββββββββββββββ΄ββββββββββ΄βββββββββββββββββββββββββββββββββββββββββ
*
* Configurable {TOKENS} from db()->tokens[] are also substituted..
*
*
* RESULT
*
* The returned result can be accessed as single data row, when fetching just
* one:
* $result->column
* $result["column"]
*
* Or just traversed row-wise as usual by iteration
*
* foreach (db("...") as $row)
*
* Alternatively by object-wrapping (unlike plain PDO->fetchObject() this
* hydrates the object using its normal constructor) the result set with:
*
* foreach ($result->into("ArrayObject") as $row)
*
* Yet all PDO ->fetch() methods are still available for use on the result obj.
*
*
* CONNECT
*
* The db() interface binds the global "$db" variable. It ought to be
* initialized with:
*
* db(new PDO(...));
*
* It's wrapped PDO handle can also be retrieved with just $pdo = db(); then.
*
*
* RECORD WRAPPER
*
* There's also a simple table data gateway wrapper implemented here. It
* accepts db() queries for single entries, and allows ->save()ing back, or
* to ->delete() records.
* You should only use it in conjuction with sql2php and its simpler wrappers.
*
*/
/**
* Hybrid instantiation / query function.
* Couples `$db` in the shared/global scope.
*
*/
function db($sql=NULL, $params=NULL) {
#-- shared PDO handle
$db = & $GLOBALS["db"]; // Alternatively could be just static to hide it behind db()
#-- open database
if (is_object($sql)) {
// use passed param
$db = new db_wrap($sql);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, is_int(stripos($db->getAttribute(PDO::ATTR_DRIVER_NAME), "mysql")));
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
$db->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
// save settings
$db->tokens = array("PREFIX"=>""); // or reference global $config
$db->in_clause = $db->getAttribute(PDO::ATTR_DRIVER_NAME) == "sqlite"
and $db->getAttribute(PDO::ATTR_CLIENT_VERSION) < 3.6;
}
#-- return PDO handle
elseif (empty($sql)) {
return $db;
}
#-- just dispatch to the wrapper
else {
$args = array_slice(func_get_args(), 1);
return $db($sql, $args);
}
}
/**
* Binds PDO handle, allows original calls and extended placeholder use.
*
*/
class db_wrap {
/**
* Keep PDO handle.
*
*/
public $pdo = NULL;
function __construct($pdo) {
$this->pdo = $pdo;
}
/**
* Chain to plain PDO if any other method invoked.
*
*/
function __call($func, $args) {
return call_user_func_array(array($this->pdo, $func), $args);
}
/**
* Prepares and executes query after extended placeholders and parameter unpacking.
*
*/
function __invoke($sql, $args=array()) {
// $sql may contain associative SQL parts and parameters
if (is_array($sql)) {
list($sql, $args) = $this->join($sql);
}
// reject plain strings in SQL
if (strpos($sql, "'")) {
trigger_error("SQL query contained raw data. DO NOT WANT", E_USER_WARNING);
return NULL;
}
// flatten array arguments and extended placeholders
list($sql, $args) = $this->fold($sql, $args);
// placeholders
if (!empty($this->tokens) && strpos($sql, "{")) {
$sql = preg_replace_callback("/\{(\w+)(.*?)\}/", array($this, "token"), $sql);
}
// older SQLite workaround
if (!empty($this->in_clause) && strpos($sql, " IN (")) { // only for ?,?,?,? enum params
$sql = preg_replace_callback("/(\S+)\s+IN\s+\(([?,]+)\)/", array($this, "in_clause"), $sql);
}
// just debug output
if (!empty($this->test)) {
print json_encode($args)." => " . trim($sql) . "\n"; return;
}
// run
$s = $this->prepare($sql)
and
$r = $s->execute($args);
// wrap
return $s && $r ? new db_result($s) : $s;
}
/**
* Expands the extended placeholders and flattens arrays from parameter list.
*
*/
function fold($sql, $args) {
// output parameter list
$flat_params = array();
#-- flattening sub-arrays (works for ? enumarated and :named params)
foreach ($args as $i=>$a) {
// subarray that corresponds to special syntax placeholder?
if (is_array($a)
and preg_match("/ \?\? | : [?:* &,|] /x", $sql, $capture, PREG_OFFSET_CAPTURE))
{
list($token, $pos) = current($capture);
// placeholder substitution, possibly changing $a params
$replace = $this->{self::$expand[$token]}($a);
// update SQL string
$sql = substr($sql, 0, $pos) . $replace . substr($sql, $pos + strlen($token));
}
// unfold into plain parameter list
if (is_array($a)) {
$flat_params = array_merge($flat_params, $a);
}
else {
$flat_params[] = $a;
}
}
return array($sql, $flat_params);
}
/**
* Syntax expansion callbacks
*
*/
static $expand = array(
"??" => "expand_list",
":?" => "expand_keys",
"::" => "expand_named",
":," => "expand_assoc_comma",
":&" => "expand_assoc_and",
":|" => "expand_assoc_or",
":*" => "expand_expr",
);
// ?? array placeholders
function expand_list($a) {
return implode(",", array_fill(0, count($a), "?"));
}
// :? name placeholders, transforms list into enumerated params
function expand_keys(&$a) {
$enum = array_keys($a) === range(0, count($a) - 1);
$r = implode(",", $this->db_identifier($enum ? $a : array_keys($a), "`"));
$a = array();
return $r;
}
// :: becomes :named,:value,:list
function expand_named($a) {
return ":" . implode(",:", $this->db_identifier(array_keys($a)) );
}
// for :, expand COMMA-separated key=:key,bar=:bar associative array
function expand_assoc_comma($a, $fill = ", ", $replace=array()) {
foreach ($this->db_identifier(array_keys($a)) as $key) {
$replace[] = "`$key` = :$key";
}
return implode($fill, $replace);
}
// for :& AND-chained assoc foo=:foo AND bar=:bar
function expand_assoc_and($a) {
return $this->expand_assoc_comma($a, " AND ");
}
// for :| OR-chained assoc foo=:foo OR bar=:bar
function expand_assoc_or($a) {
return $this->expand_assoc_comma($a, " OR ");
}
/**
* While :* holds an optional expression and subvalue list. Which only gets
* interpolated if the params list is non-empty.
* which may be provided as alternative pairs ["AND :&", $and, "OR :|", $or].
*
* While each value list should be a list itself, it's common to just pass
* one array param for a single ::/?? extended placeholder. (Which then will
* be auto-wrapped.)
*
*/
function expand_expr(&$a) {
foreach (array_chunk($a, 2) as $pair) if (list($sql, $args) = $pair) {
// substitute subexpression as if it were a regular SQL string
if (is_array($args) && count($args)) {
// rewrap simple value lists into param-args list
$args = array_sum(array_map("is_array", $args)) ? $args : array($args);
list ($replace, $a) = $this->fold($sql, $args);
return $replace;
}
}
$a = array(); // else replace with nothing and omit current data for flattened $params2
}
/**
* For readability input SQL may come as associative clause => params list.
* ["SELECT ?" => $num,
* "FROM :?" => [$tbl],
* "WHERE :&" => $match
* ]
* Which is separated here into keys as $sql string and $args from values.
*
*/
function join($sql_args, $sql="", $args=array()) {
foreach ($sql_args as $key=>$val) {
// Key itself is not an SQL part
if (is_int($key)) {
// Value then can be an SQL string, or a param
if (is_string($val)) {
$sql .= $val;
}
else {
$args[] = $val;
}
}
// Plain SQL => Value
else {
$sql .= $key . "\n ";
$args[] = $val;
}
}
return array($sql, $args);
}
// This is a restrictive filter function for column/table name identifiers.
// Can only be foregone if it's ensured that none of the passed named db() $arg keys originated from http/user input.
function db_identifier($as, $wrap="") {
return preg_replace(array("/[^\w\d_.]/", "/^|$/"), array("_", $wrap), $as);
}
// Regex callbacks
function token($m) {
list($m, $tok, $ext) = $m;
return isset($this->token[$tok]) ? $this->token[$tok].$ext : $this->token["$tok$ext"];
}
function in_clause($m) {
list($m, $key, $vals) = $m;
$num = substr_count($vals, "?");
return "($key=" . implode("OR $key=", array_fill(0, $num, "? ")) . ")";
}
}
/**
* Allows traversing result sets as arrays or hydrated objects,
* or fetches only first result row on ->column_name accesses.
*
*/
class db_result extends ArrayObject implements IteratorAggregate {
protected $results = NULL;
function __construct($results) {
parent::__construct(array(), 2);
$this->results = $results;
}
// used as PDO statement
function __call($func, $args) {
return call_user_func_array(array($this->results, $func), $args);
}
// Single column access
function offsetGet($name) {
// get first result, transfuse into $this
if (is_object($r = $this->results)) {
unset($this->results);
if ($row = $r->fetch()) {
$this->exchangeArray($row);
}
// no row returned, silently return
else {
return NULL;
}
}
// suffice __get
return parent::offsetGet($name);
}
// Just let PDOStatement handle the Traversable
function getIterator() {
return isset($this->results)
? $this->results
: new ArrayIterator($this);
}
// Or hydrate specific result objects ourselves
function into() {
$into = func_get_args() ?: array("ArrayObject", 2);
return new db_result_iter($this->results, $into);
}
}
/**
* More wrapping for hydrated iteration.
*
*/
class db_result_iter implements Iterator {
// Again keep PDOStatement and class specifier
protected $results = NULL;
protected $into = array();
function __construct($results, $into) {
$this->results = $results;
$this->into = $into;
}
// Iterator just fetches and converts on traversal
protected $row = NULL;
public function current()
{
list($class, $a2, $a3, $a4, $a5) = array_merge($this->into, [NULL, NULL, NULL, NULL]);
return new $class($this->row, $a2);
}
function valid() {
return !empty($this->row = $this->results->fetch());
}
// unused for normal `foreach` operation
function next() { return NULL; }
function rewind() { return NULL; }
function key() { return NULL; }
}
/**
* Table data gateway. Don't use directly.
*
* Keeps ->_meta->table name and ->_meta->fields,
* uses extendable tables with [ext] field serialization.
* Doesn't cope with table joins. (yet?)
*
* Allows to ->set() and ->save() record back.
*/
class db_record /*resembles db_result*/ extends ArrayObject {
// this is not purposelessly private, but to not pollute (array) typecasts with decorative data
private $_meta;
// initialize from db() result or array
function __construct($results, $table, $fields, $keys) {
// meta
$this->_meta = new stdClass();
$this->_meta->table = $table;
$this->_meta->fields = array_unique(array_merge(array_keys($fields), array_keys($results)));
$this->_meta->keys = $keys;
// db query result
if (is_array($results)) {
$this->_meta->new = 1; // instantiate from defaults or given row values
}
else {
//if (is_string($results)) { // queries are handled in wrapper
// $results = db($results);
//}
$results = $results->fetch(); // just get first result row
$this->_meta->new = 0;
}
// unfold .ext
if ($this->_meta->ext = isset($results["ext"])) {
$results = array_merge($results, unserialize($results["ext"]));
}
// copy data
// and turn object==array
parent::__construct((array)$results, 2); //ArrayObject::ARRAY_AS_PROPS
// fluent (hybrid constructor wrapper)
return $this;
}
// set field
function set($key, $val) {
$this->{$key} = $val;
return $this; // fluent
}
// store table back to DB
function save($row=NULL) {
// source
if (empty($row)) {
$row = $this->getArrayCopy();
}
else {
$row = array_merge($this->getArrayCopy(), is_array($row) ? $row : $row->getArrayCopy());
}
// fold .ext
if ($this->_meta->ext) {
$ext = array();
foreach ($row as $key=>$val) {
if (!in_array($key, $this->_meta->fields)) {
$ext[$key] = $val;
unset($row[$key]);
}
}
$row["ext"] = serialize($ext);
}
// store
if ($this->_meta->new) {
db("INSERT INTO {$this->_meta->table} (:?) VALUES (??)", $row, $row);
$this->_meta->new = 0;
}
// update
else {
$keys = $this->keys($row, 1);
db("UPDATE {$this->_meta->table} SET :, WHERE :&", $row, $keys);
}
return $this; // fluent
}
// split $keys from $row/$this
function keys(&$row, $unset=0) {
$keys = array();
foreach ($this->_meta->keys as $key) {
$keys[$key] = $row[$key];
if ($unset) unset($row[$key]);
}
return $keys;
}
// oh noooes
function delete() {
db("DELETE FROM {$this->_meta->table} WHERE :&", $this->keys($this));
return $this; // well
}
}
?>
|