Passed
Push — master ( 6df7c9...74bc2a )
by y
02:15
created

DB::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 14
rs 9.8666
cc 2
nc 2
nop 4
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 PDO;
15
16
/**
17
 * Extends `PDO` and acts as a central access point for the schema.
18
 */
19
class DB extends PDO implements ArrayAccess {
20
21
    /**
22
     * @var string
23
     */
24
    private $driver;
25
26
    /**
27
     * @var Junction[]
28
     */
29
    protected $junctions = [];
30
31
    /**
32
     * Notified whenever a query is executed or a statement is prepared.
33
     * Takes only one argument: the SQL being executed or prepared.
34
     * This is a stub closure by default.
35
     *
36
     * @var Closure
37
     */
38
    protected $logger;
39
40
    /**
41
     * @var Record[]
42
     */
43
    protected $records = [];
44
45
    /**
46
     * Sets various attributes to streamline operations.
47
     *
48
     * Registers missing SQLite functions.
49
     *
50
     * @param string $dsn
51
     * @param string $username
52
     * @param string $password
53
     * @param array $options
54
     */
55
    public function __construct ($dsn, $username = null, $password = null, $options = null) {
56
        parent::__construct($dsn, $username, $password, $options);
57
        $this->driver = $this->getAttribute(self::ATTR_DRIVER_NAME);
58
        $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC);
59
        $this->setAttribute(self::ATTR_EMULATE_PREPARES, false);
60
        $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION);
61
        $this->setAttribute(self::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]);
62
        $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false);
63
        $this->logger = function() {
64
        };
65
        if ($this->driver === 'sqlite') {
66
            $this->sqliteCreateFunction('CEIL', 'ceil');
67
            $this->sqliteCreateFunction('FLOOR', 'floor');
68
            $this->sqliteCreateFunction('POW', 'pow');
69
        }
70
    }
71
72
    /**
73
     * Returns the driver.
74
     *
75
     * @return string
76
     */
77
    final public function __toString () {
78
        return $this->driver;
79
    }
80
81
    /**
82
     * Notifies the logger.
83
     *
84
     * @param string $sql
85
     * @return int
86
     */
87
    public function exec ($sql): int {
88
        $this->logger->__invoke($sql);
89
        return parent::exec($sql);
90
    }
91
92
    /**
93
     * @return string
94
     */
95
    final public function getDriver (): string {
96
        return $this->driver;
97
    }
98
99
    /**
100
     * Returns a {@link Junction} access object based on an annotated interface.
101
     *
102
     * @param string $interface
103
     * @return Junction
104
     */
105
    public function getJunction ($interface) {
106
        if (!isset($this->junctions[$interface])) {
107
            $this->junctions[$interface] = Junction::fromInterface($this, $interface);
108
        }
109
        return $this->junctions[$interface];
110
    }
111
112
    /**
113
     * @return Closure
114
     */
115
    public function getLogger () {
116
        return $this->logger;
117
    }
118
119
    /**
120
     * Returns a {@link Record} access object based on an annotated class.
121
     *
122
     * @param string|EntityInterface $class
123
     * @return Record
124
     */
125
    public function getRecord ($class) {
126
        if (is_object($class)) {
127
            $class = get_class($class);
128
        }
129
        if (!isset($this->records[$class])) {
130
            $this->records[$class] = Record::fromClass($this, $class);
131
        }
132
        return $this->records[$class];
133
    }
134
135
    /**
136
     * Generates an equality {@link Predicate} from mixed arguments.
137
     *
138
     * If `$b` is a closure, returns from `$b($a, DB $this)`
139
     *
140
     * If `$a` is an integer (enumerated item), returns `$b` as a {@link Predicate}
141
     *
142
     * If `$b` is an array, returns `$a IN (...quoted $b)`
143
     *
144
     * If `$b` is a {@link Select}, returns `$a IN ($b->toSql())`
145
     *
146
     * Otherwise predicates `$a = quoted $b`
147
     *
148
     * @param mixed $a
149
     * @param mixed $b
150
     * @return Predicate
151
     */
152
    public function match ($a, $b) {
153
        if ($b instanceof Closure) {
154
            return $b->__invoke($a, $this);
155
        }
156
        if (is_int($a)) {
157
            return new Predicate($b);
158
        }
159
        if (is_array($b)) {
160
            return new Predicate("{$a} IN ({$this->quoteList($b)})");
161
        }
162
        if ($b instanceof Select) {
163
            return new Predicate("{$a} IN ({$b->toSql()})");
164
        }
165
        return new Predicate("{$a} = {$this->quote($b)}");
166
    }
167
168
    /**
169
     * @param string $class Class or interface name.
170
     * @return bool
171
     */
172
    public function offsetExists ($class): bool {
173
        return (bool)$this->offsetGet($class);
174
    }
175
176
    /**
177
     * @param string $class Class or interface name.
178
     * @return null|Record|Junction
179
     */
180
    public function offsetGet ($class) {
181
        if (class_exists($class)) {
182
            return $this->getRecord($class);
183
        }
184
        if (interface_exists($class)) {
185
            return $this->getJunction($class);
186
        }
187
        return null;
188
    }
189
190
    /**
191
     * @param string $class Class or interface name.
192
     * @param Record|Junction $access
193
     */
194
    public function offsetSet ($class, $access) {
195
        if ($access instanceof Record) {
196
            $this->setRecord($class, $access);
197
        }
198
        else {
199
            $this->setJunction($class, $access);
200
        }
201
    }
202
203
    /**
204
     * @param string $class Class or interface name.
205
     */
206
    public function offsetUnset ($class) {
207
        unset($this->records[$class]);
208
        unset($this->junctions[$class]);
209
    }
210
211
    /**
212
     * Notifies the logger.
213
     *
214
     * @param string $sql
215
     * @param array $options
216
     * @return Statement
217
     */
218
    public function prepare ($sql, $options = []) {
219
        $this->logger->__invoke($sql);
220
        /** @var Statement $statement */
221
        $statement = parent::prepare($sql, $options);
222
        return $statement;
223
    }
224
225
    /**
226
     * Notifies the logger and executes.
227
     *
228
     * @param string $sql
229
     * @param int $mode
230
     * @param mixed $arg3 Optional.
231
     * @param array $ctorargs Optional.
232
     * @return Statement
233
     */
234
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) {
235
        $this->logger->__invoke($sql);
236
        /** @var Statement $statement */
237
        $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

237
        $statement = parent::query(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
238
        return $statement;
239
    }
240
241
    /**
242
     * Quotes a value, with special considerations.
243
     *
244
     * - {@link ExpressionInterface} instances are returned as-is.
245
     * - Booleans and integers are returned as unquoted integer-string.
246
     * - Everything else is returned as a quoted string.
247
     *
248
     * @param bool|number|string|object $value
249
     * @param int $type Ignored.
250
     * @return string|ExpressionInterface
251
     */
252
    public function quote ($value, $type = self::PARAM_STR) {
253
        if ($value instanceof ExpressionInterface) {
254
            return $value;
255
        }
256
        switch (gettype($value)) {
257
            case 'integer' :
258
            case 'boolean' :
259
            case 'resource' :
260
                return (string)(int)$value;
261
            default:
262
                return parent::quote((string)$value);
263
        }
264
    }
265
266
    /**
267
     * Quotes an array of values. Keys are preserved.
268
     *
269
     * @param array $values
270
     * @return string[]
271
     */
272
    public function quoteArray (array $values) {
273
        return array_map([$this, 'quote'], $values);
274
    }
275
276
    /**
277
     * Returns a quoted, comma-separated list.
278
     *
279
     * @param array $values
280
     * @return string
281
     */
282
    public function quoteList (array $values): string {
283
        return implode(',', $this->quoteArray($values));
284
    }
285
286
    /**
287
     * Forwards to the entity's {@link Record}
288
     *
289
     * @param EntityInterface $entity
290
     * @return int ID
291
     */
292
    public function save (EntityInterface $entity): int {
293
        return $this->getRecord($entity)->save($entity);
294
    }
295
296
    /**
297
     * @param string $interface
298
     * @param Junction $junction
299
     * @return $this
300
     */
301
    public function setJunction (string $interface, Junction $junction) {
302
        $this->junctions[$interface] = $junction;
303
        return $this;
304
    }
305
306
    /**
307
     * @param Closure $logger
308
     * @return $this
309
     */
310
    public function setLogger (Closure $logger) {
311
        $this->logger = $logger;
312
        return $this;
313
    }
314
315
    /**
316
     * @param string $class
317
     * @param Record $record
318
     * @return $this
319
     */
320
    public function setRecord (string $class, Record $record) {
321
        $this->records[$class] = $record;
322
        return $this;
323
    }
324
}