Passed
Push — master ( 74bc2a...6c4562 )
by y
02:11
created

DB::factory()   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 2
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
     * Central point of framework object creation.
94
     *
95
     * Override this to override framework classes.
96
     *
97
     * @param string $class
98
     * @param mixed ...$args
99
     * @return mixed
100
     */
101
    public function factory (string $class, ...$args) {
102
        return new $class(...$args);
103
    }
104
105
    /**
106
     * @return string
107
     */
108
    final public function getDriver (): string {
109
        return $this->driver;
110
    }
111
112
    /**
113
     * Returns a {@link Junction} access object based on an annotated interface.
114
     *
115
     * @param string $interface
116
     * @return Junction
117
     */
118
    public function getJunction ($interface) {
119
        if (!isset($this->junctions[$interface])) {
120
            $this->junctions[$interface] = Junction::fromInterface($this, $interface);
121
        }
122
        return $this->junctions[$interface];
123
    }
124
125
    /**
126
     * @return Closure
127
     */
128
    public function getLogger () {
129
        return $this->logger;
130
    }
131
132
    /**
133
     * Returns a {@link Record} access object based on an annotated class.
134
     *
135
     * @param string|EntityInterface $class
136
     * @return Record
137
     */
138
    public function getRecord ($class) {
139
        if (is_object($class)) {
140
            $class = get_class($class);
141
        }
142
        if (!isset($this->records[$class])) {
143
            $this->records[$class] = Record::fromClass($this, $class);
144
        }
145
        return $this->records[$class];
146
    }
147
148
    /**
149
     * Generates an equality {@link Predicate} from mixed arguments.
150
     *
151
     * If `$b` is a closure, returns from `$b($a, DB $this)`
152
     *
153
     * If `$a` is an integer (enumerated item), returns `$b` as a {@link Predicate}
154
     *
155
     * If `$b` is an array, returns `$a IN (...quoted $b)`
156
     *
157
     * If `$b` is a {@link Select}, returns `$a IN ($b->toSql())`
158
     *
159
     * Otherwise predicates `$a = quoted $b`
160
     *
161
     * @param mixed $a
162
     * @param mixed $b
163
     * @return Predicate
164
     */
165
    public function match ($a, $b) {
166
        if ($b instanceof Closure) {
167
            return $b->__invoke($a, $this);
168
        }
169
        if (is_int($a)) {
170
            return $this->factory(Predicate::class, $b);
171
        }
172
        if (is_array($b)) {
173
            return $this->factory(Predicate::class, "{$a} IN ({$this->quoteList($b)})");
174
        }
175
        if ($b instanceof Select) {
176
            return $this->factory(Predicate::class, "{$a} IN ({$b->toSql()})");
177
        }
178
        return $this->factory(Predicate::class, "{$a} = {$this->quote($b)}");
179
    }
180
181
    /**
182
     * @param string $class Class or interface name.
183
     * @return bool
184
     */
185
    public function offsetExists ($class): bool {
186
        return (bool)$this->offsetGet($class);
187
    }
188
189
    /**
190
     * @param string $class Class or interface name.
191
     * @return null|Record|Junction
192
     */
193
    public function offsetGet ($class) {
194
        if (class_exists($class)) {
195
            return $this->getRecord($class);
196
        }
197
        if (interface_exists($class)) {
198
            return $this->getJunction($class);
199
        }
200
        return null;
201
    }
202
203
    /**
204
     * @param string $class Class or interface name.
205
     * @param Record|Junction $access
206
     */
207
    public function offsetSet ($class, $access) {
208
        if ($access instanceof Record) {
209
            $this->setRecord($class, $access);
210
        }
211
        else {
212
            $this->setJunction($class, $access);
213
        }
214
    }
215
216
    /**
217
     * @param string $class Class or interface name.
218
     */
219
    public function offsetUnset ($class) {
220
        unset($this->records[$class]);
221
        unset($this->junctions[$class]);
222
    }
223
224
    /**
225
     * Notifies the logger.
226
     *
227
     * @param string $sql
228
     * @param array $options
229
     * @return Statement
230
     */
231
    public function prepare ($sql, $options = []) {
232
        $this->logger->__invoke($sql);
233
        /** @var Statement $statement */
234
        $statement = parent::prepare($sql, $options);
235
        return $statement;
236
    }
237
238
    /**
239
     * Notifies the logger and executes.
240
     *
241
     * @param string $sql
242
     * @param int $mode
243
     * @param mixed $arg3 Optional.
244
     * @param array $ctorargs Optional.
245
     * @return Statement
246
     */
247
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) {
248
        $this->logger->__invoke($sql);
249
        /** @var Statement $statement */
250
        $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

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