Passed
Push — master ( 6c4562...bd418e )
by y
01:33
created

DB::isMySQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Helix;
4
5
use ArrayAccess;
6
use Closure;
7
use Helix\DB\EntityInterface;
8
use Helix\DB\Junction;
9
use Helix\DB\Record;
10
use Helix\DB\Select;
11
use Helix\DB\SQL\ExpressionInterface;
12
use Helix\DB\SQL\Predicate;
13
use Helix\DB\Statement;
14
use Helix\DB\Table;
15
use LogicException;
16
use PDO;
17
18
/**
19
 * Extends `PDO` and acts as a central access point for the schema.
20
 */
21
class DB extends PDO implements ArrayAccess {
22
23
    /**
24
     * @var string
25
     */
26
    private $driver;
27
28
    /**
29
     * @var Junction[]
30
     */
31
    protected $junctions = [];
32
33
    /**
34
     * Notified whenever a query is executed or a statement is prepared.
35
     * Takes only one argument: the SQL being executed or prepared.
36
     * This is a stub closure by default.
37
     *
38
     * @var Closure
39
     */
40
    protected $logger;
41
42
    /**
43
     * @var Record[]
44
     */
45
    protected $records = [];
46
47
    /**
48
     * @var Table[]
49
     */
50
    protected $tables = [];
51
52
    /**
53
     * Sets various attributes to streamline operations.
54
     *
55
     * Registers missing SQLite functions.
56
     *
57
     * @param string $dsn
58
     * @param string $username
59
     * @param string $password
60
     * @param array $options
61
     */
62
    public function __construct ($dsn, $username = null, $password = null, $options = null) {
63
        parent::__construct($dsn, $username, $password, $options);
64
        $this->driver = $this->getAttribute(self::ATTR_DRIVER_NAME);
65
        $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC);
66
        $this->setAttribute(self::ATTR_EMULATE_PREPARES, false);
67
        $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION);
68
        $this->setAttribute(self::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]);
69
        $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false);
70
        $this->logger = function() {
71
        };
72
        if ($this->isSQLite()) {
73
            $this->sqliteCreateFunction('CEIL', 'ceil');
74
            $this->sqliteCreateFunction('FLOOR', 'floor');
75
            $this->sqliteCreateFunction('POW', 'pow');
76
        }
77
    }
78
79
    /**
80
     * Returns the driver.
81
     *
82
     * @return string
83
     */
84
    final public function __toString () {
85
        return $this->driver;
86
    }
87
88
    /**
89
     * Notifies the logger.
90
     *
91
     * @param string $sql
92
     * @return int
93
     */
94
    public function exec ($sql): int {
95
        $this->logger->__invoke($sql);
96
        return parent::exec($sql);
97
    }
98
99
    /**
100
     * Central point of framework object creation.
101
     *
102
     * Override this to override framework classes.
103
     *
104
     * @param string $class
105
     * @param mixed ...$args
106
     * @return mixed
107
     */
108
    public function factory (string $class, ...$args) {
109
        return new $class(...$args);
110
    }
111
112
    /**
113
     * @return string
114
     */
115
    final public function getDriver (): string {
116
        return $this->driver;
117
    }
118
119
    /**
120
     * Returns a {@link Junction} access object based on an annotated interface.
121
     *
122
     * @param string $interface
123
     * @return Junction
124
     */
125
    public function getJunction ($interface) {
126
        return $this->junctions[$interface]
127
            ?? $this->junctions[$interface] = Junction::fromInterface($this, $interface);
128
    }
129
130
    /**
131
     * @return Closure
132
     */
133
    public function getLogger () {
134
        return $this->logger;
135
    }
136
137
    /**
138
     * Returns a {@link Record} access object based on an annotated class.
139
     *
140
     * @param string|EntityInterface $class
141
     * @return Record
142
     */
143
    public function getRecord ($class) {
144
        if (is_object($class)) {
145
            $class = get_class($class);
146
        }
147
        return $this->records[$class]
148
            ?? $this->records[$class] = Record::fromClass($this, $class);
149
    }
150
151
    /**
152
     * @param string $name
153
     * @return null|Table
154
     */
155
    public function getTable (string $name) {
156
        if (!isset($this->tables[$name])) {
157
            if ($this->isSQLite()) {
158
                $info = $this->query("PRAGMA table_info({$this->quote($name)})")->fetchAll();
159
                $cols = array_column($info, 'name');
160
            }
161
            else {
162
                $cols = $this->query(
163
                    "SELECT column_name FROM information_schema.tables WHERE table_name = {$this->quote($name)}"
164
                )->fetchAll(self::FETCH_COLUMN);
165
            }
166
            if (!$cols){
0 ignored issues
show
Bug Best Practice introduced by
The expression $cols of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
167
                return null;
168
            }
169
            $this->tables[$name] = $this->factory(Table::class, $this, $name, $cols);
170
        }
171
        return $this->tables[$name];
172
    }
173
174
    /**
175
     * @return bool
176
     */
177
    final public function isMySQL (): bool {
178
        return $this->driver === 'mysql';
179
    }
180
181
    /**
182
     * @return bool
183
     */
184
    final public function isPostgreSQL (): bool {
185
        return $this->driver === 'pgsql';
186
    }
187
188
    /**
189
     * @return bool
190
     */
191
    final public function isSQLite (): bool {
192
        return $this->driver === 'sqlite';
193
    }
194
195
    /**
196
     * Generates an equality {@link Predicate} from mixed arguments.
197
     *
198
     * If `$b` is a closure, returns from `$b($a, DB $this)`
199
     *
200
     * If `$a` is an integer (enumerated item), returns `$b` as a {@link Predicate}
201
     *
202
     * If `$b` is an array, returns `$a IN (...quoted $b)`
203
     *
204
     * If `$b` is a {@link Select}, returns `$a IN ($b->toSql())`
205
     *
206
     * Otherwise predicates `$a = quoted $b`
207
     *
208
     * @param mixed $a
209
     * @param mixed $b
210
     * @return Predicate
211
     */
212
    public function match ($a, $b) {
213
        if ($b instanceof Closure) {
214
            return $b->__invoke($a, $this);
215
        }
216
        if (is_int($a)) {
217
            return $this->factory(Predicate::class, $b);
218
        }
219
        if (is_array($b)) {
220
            return $this->factory(Predicate::class, "{$a} IN ({$this->quoteList($b)})");
221
        }
222
        if ($b instanceof Select) {
223
            return $this->factory(Predicate::class, "{$a} IN ({$b->toSql()})");
224
        }
225
        return $this->factory(Predicate::class, "{$a} = {$this->quote($b)}");
226
    }
227
228
    /**
229
     * @param string $class Class or interface name.
230
     * @return bool
231
     */
232
    public function offsetExists ($class): bool {
233
        return (bool)$this->offsetGet($class);
234
    }
235
236
    /**
237
     * @param string $class Class or interface name.
238
     * @return null|Table|Record|Junction
239
     */
240
    public function offsetGet ($class) {
241
        if (is_a($class, EntityInterface::class, true)) {
242
            return $this->getRecord($class);
243
        }
244
        elseif (interface_exists($class)) {
245
            return $this->getJunction($class);
246
        }
247
        else {
248
            return $this->getTable($class);
249
        }
250
    }
251
252
    /**
253
     * @param mixed $class Class or interface name.
254
     * @param Table|Record|Junction $access
255
     */
256
    public function offsetSet ($class, $access) {
257
        if ($access instanceof Record) {
258
            $this->setRecord($class, $access);
259
        }
260
        elseif ($access instanceof Junction) {
261
            $this->setJunction($class, $access);
262
        }
263
        else {
264
            throw new LogicException('Raw table access is immutable.');
265
        }
266
    }
267
268
    /**
269
     * @param string $class Class or interface name.
270
     */
271
    public function offsetUnset ($class) {
272
        unset($this->records[$class]);
273
        unset($this->junctions[$class]);
274
    }
275
276
    /**
277
     * Notifies the logger.
278
     *
279
     * @param string $sql
280
     * @param array $options
281
     * @return Statement
282
     */
283
    public function prepare ($sql, $options = []) {
284
        $this->logger->__invoke($sql);
285
        /** @var Statement $statement */
286
        $statement = parent::prepare($sql, $options);
287
        return $statement;
288
    }
289
290
    /**
291
     * Notifies the logger and executes.
292
     *
293
     * @param string $sql
294
     * @param int $mode
295
     * @param mixed $arg3 Optional.
296
     * @param array $ctorargs Optional.
297
     * @return Statement
298
     */
299
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) {
300
        $this->logger->__invoke($sql);
301
        /** @var Statement $statement */
302
        $statement = parent::query(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $statement of PDO::query() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

302
        $statement = parent::query(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
303
        return $statement;
304
    }
305
306
    /**
307
     * Quotes a value, with special considerations.
308
     *
309
     * - {@link ExpressionInterface} instances are returned as-is.
310
     * - Booleans and integers are returned as unquoted integer-string.
311
     * - Everything else is returned as a quoted string.
312
     *
313
     * @param bool|number|string|object $value
314
     * @param int $type Ignored.
315
     * @return string|ExpressionInterface
316
     */
317
    public function quote ($value, $type = self::PARAM_STR) {
318
        if ($value instanceof ExpressionInterface) {
319
            return $value;
320
        }
321
        switch (gettype($value)) {
322
            case 'integer' :
323
            case 'boolean' :
324
            case 'resource' :
325
                return (string)(int)$value;
326
            default:
327
                return parent::quote((string)$value);
328
        }
329
    }
330
331
    /**
332
     * Quotes an array of values. Keys are preserved.
333
     *
334
     * @param array $values
335
     * @return string[]
336
     */
337
    public function quoteArray (array $values) {
338
        return array_map([$this, 'quote'], $values);
339
    }
340
341
    /**
342
     * Returns a quoted, comma-separated list.
343
     *
344
     * @param array $values
345
     * @return string
346
     */
347
    public function quoteList (array $values): string {
348
        return implode(',', $this->quoteArray($values));
349
    }
350
351
    /**
352
     * Forwards to the entity's {@link Record}
353
     *
354
     * @param EntityInterface $entity
355
     * @return int ID
356
     */
357
    public function save (EntityInterface $entity): int {
358
        return $this->getRecord($entity)->save($entity);
359
    }
360
361
    /**
362
     * @param string $interface
363
     * @param Junction $junction
364
     * @return $this
365
     */
366
    public function setJunction (string $interface, Junction $junction) {
367
        $this->junctions[$interface] = $junction;
368
        return $this;
369
    }
370
371
    /**
372
     * @param Closure $logger
373
     * @return $this
374
     */
375
    public function setLogger (Closure $logger) {
376
        $this->logger = $logger;
377
        return $this;
378
    }
379
380
    /**
381
     * @param string $class
382
     * @param Record $record
383
     * @return $this
384
     */
385
    public function setRecord (string $class, Record $record) {
386
        $this->records[$class] = $record;
387
        return $this;
388
    }
389
}