Passed
Push — master ( 6a4462...5500dc )
by y
01:44
created

DB::quoteList()   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
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Helix;
4
5
use ArrayAccess;
6
use Closure;
7
use Helix\DB\Column;
8
use Helix\DB\EntityInterface;
9
use Helix\DB\Junction;
10
use Helix\DB\Record;
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 $passwd
53
     * @param array $options
54
     */
55
    public function __construct ($dsn, $username = null, $passwd = null, $options = null) {
56
        parent::__construct($dsn, $username, $passwd, $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
        if ($this->driver === 'sqlite') {
65
            $this->sqliteCreateFunction('CEIL', 'ceil');
0 ignored issues
show
Bug introduced by
The method sqliteCreateFunction() does not exist on Helix\DB. ( Ignorable by Annotation )

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

65
            $this->/** @scrutinizer ignore-call */ 
66
                   sqliteCreateFunction('CEIL', 'ceil');

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...
66
            $this->sqliteCreateFunction('FLOOR', 'floor');
67
            $this->sqliteCreateFunction('POW', 'pow');
68
        }
69
    }
70
71
    /**
72
     * Returns the driver.
73
     *
74
     * @return string
75
     */
76
    final public function __toString () {
77
        return $this->driver;
78
    }
79
80
    /**
81
     * Notifies the logger.
82
     *
83
     * @param string $sql
84
     * @return int
85
     */
86
    public function exec ($sql): int {
87
        $this->logger->__invoke($sql);
88
        return parent::exec($sql);
89
    }
90
91
    /**
92
     * @return string
93
     */
94
    final public function getDriver (): string {
95
        return $this->driver;
96
    }
97
98
    /**
99
     * Returns a `Junction` access object based on an annotated interface.
100
     *
101
     * @param string $interface
102
     * @return Junction
103
     */
104
    public function getJunction ($interface) {
105
        if (!isset($this->junctions[$interface])) {
106
            $this->junctions[$interface] = Junction::fromInterface($this, $interface);
107
        }
108
        return $this->junctions[$interface];
109
    }
110
111
    /**
112
     * @return Closure
113
     */
114
    public function getLogger () {
115
        return $this->logger;
116
    }
117
118
    /**
119
     * Returns a `Record` access object based on an annotated class.
120
     *
121
     * @param string|EntityInterface $class
122
     * @return Record
123
     */
124
    public function getRecord ($class) {
125
        $name = $class;
126
        if (is_object($name)) {
127
            $name = get_class($name);
128
        }
129
        if (!isset($this->records[$name])) {
130
            $this->records[$name] = Record::fromClass($this, $class);
131
        }
132
        return $this->records[$name];
133
    }
134
135
    /**
136
     * Generates predicate strings from mixed arguments.
137
     *
138
     * If `$b` is a closure, this returns `(string)($b(Column $a, DB $this))`
139
     *
140
     * Otherwise if `$a` is an integer, this returns `(string)$b`
141
     *
142
     * Otherwise this returns an equality comparison string.
143
     * `$b` is quoted if it's not an `ExpressionInterface`.
144
     *
145
     * @see Predicate::compare()
146
     *
147
     * @param mixed $a
148
     * @param mixed $b
149
     * @return string
150
     */
151
    public function match ($a, $b) {
152
        if ($b instanceof Closure) {
153
            if (!$a instanceof Column) {
154
                $a = new Column($this, $a);
155
            }
156
            return (string)$b->__invoke($a, $this);
157
        }
158
        if (is_int($a)) {
159
            return (string)$b;
160
        }
161
        return (string)Predicate::compare($a, $this->quoteMixed($b));
162
    }
163
164
    /**
165
     * Generates an array of string conditions from an input array.
166
     *
167
     * - Integer keys with string values are returned as-is
168
     * - String keys are used for `$a`, and values are used for `$b`
169
     *
170
     * Keys are not preserved, an enumerated array of strings is returned.
171
     *
172
     * @see match()
173
     *
174
     * @param array $match
175
     * @return string[]
176
     */
177
    public function matchArray (array $match) {
178
        return array_map([$this, 'match'], array_keys($match), $match);
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.
240
     *
241
     * @param string $sql
242
     * @param int $mode
243
     * @param mixed $arg3
244
     * @param array $ctorargs
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 resources are returned as unquoted integer string.
259
     * - Integers are returned as unquoted string.
260
     * - Everything else is returned as a quoted string.
261
     *
262
     * @param mixed $value
263
     * @param int $type Ignored.
264
     * @return string
265
     */
266
    public function quote ($value, $type = null) {
267
        if ($value instanceof ExpressionInterface) {
268
            return $value;
269
        }
270
        switch (gettype($value)) {
271
            case 'integer' :
272
            case 'boolean' :
273
            case 'resource' :
274
                return (string)(int)$value;
275
            default:
276
                return parent::quote($value);
277
        }
278
    }
279
280
    /**
281
     * Quotes an array of values. Keys are preserved.
282
     *
283
     * @param array $values
284
     * @return string[]
285
     */
286
    public function quoteArray (array $values): array {
287
        return array_map([$this, 'quote'], $values);
288
    }
289
290
    /**
291
     * Returns a quoted, comma-separated list.
292
     *
293
     * @param array $values
294
     * @return string
295
     */
296
    public function quoteList (array $values): string {
297
        return implode(',', $this->quoteArray($values));
298
    }
299
300
    /**
301
     * Accepts an array or string and returns it quoted.
302
     *
303
     * @param mixed $value
304
     * @return string|string[]
305
     */
306
    public function quoteMixed ($value) {
307
        if (is_array($value)) {
308
            return $this->quoteArray($value);
309
        }
310
        return $this->quote($value);
311
    }
312
313
    /**
314
     * Forwards to the entity's {@link Record}
315
     *
316
     * @param EntityInterface $entity
317
     * @return int ID
318
     */
319
    public function save (EntityInterface $entity): int {
320
        return $this->getRecord($entity)->save($entity);
321
    }
322
323
    /**
324
     * @param string $interface
325
     * @param Junction $junction
326
     * @return $this
327
     */
328
    public function setJunction (string $interface, Junction $junction) {
329
        $this->junctions[$interface] = $junction;
330
        return $this;
331
    }
332
333
    /**
334
     * @param Closure $logger
335
     * @return $this
336
     */
337
    public function setLogger (Closure $logger) {
338
        $this->logger = $logger;
339
        return $this;
340
    }
341
342
    /**
343
     * @param string $class
344
     * @param Record $record
345
     * @return $this
346
     */
347
    public function setRecord (string $class, Record $record) {
348
        $this->records[$class] = $record;
349
        return $this;
350
    }
351
}