Passed
Push — master ( c55c4a...28db53 )
by y
01:27
created

DB::matchArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 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 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 an equality `Predicate` from mixed arguments.
137
     *
138
     * If `$b` is a closure, this returns from `$b($a, DB $this)`
139
     *
140
     * If `$a` is an integer, this returns `$b` as a `Predicate`
141
     *
142
     * If `$b` is an array, this returns `$a IN (...$b)`
143
     *
144
     * If `$b` is a `Select`, this returns `$a IN ($b->toSql())`
145
     *
146
     * Otherwise predicates `$a = $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.
227
     *
228
     * @param string $sql
229
     * @param int $mode
230
     * @param mixed $arg3
231
     * @param array $ctorargs
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 resources are returned as unquoted integer string.
246
     * - Integers are returned as unquoted string.
247
     * - Everything else is returned as a quoted string.
248
     *
249
     * @param bool|number|string|object $value
250
     * @param int $type Ignored.
251
     * @return string
252
     */
253
    public function quote ($value, $type = null) {
254
        if ($value instanceof ExpressionInterface) {
255
            return $value;
256
        }
257
        switch (gettype($value)) {
258
            case 'integer' :
259
            case 'boolean' :
260
            case 'resource' :
261
                return (string)(int)$value;
262
            default:
263
                return parent::quote($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object; however, parameter $string of PDO::quote() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

263
                return parent::quote(/** @scrutinizer ignore-type */ $value);
Loading history...
264
        }
265
    }
266
267
    /**
268
     * Quotes an array of values. Keys are preserved.
269
     *
270
     * @param array $values
271
     * @return string[]
272
     */
273
    public function quoteArray (array $values): array {
274
        return array_map([$this, 'quote'], $values);
275
    }
276
277
    /**
278
     * Returns a quoted, comma-separated list.
279
     *
280
     * @param array $values
281
     * @return string
282
     */
283
    public function quoteList (array $values): string {
284
        return implode(',', $this->quoteArray($values));
285
    }
286
287
    /**
288
     * Forwards to the entity's {@link Record}
289
     *
290
     * @param EntityInterface $entity
291
     * @return int ID
292
     */
293
    public function save (EntityInterface $entity): int {
294
        return $this->getRecord($entity)->save($entity);
295
    }
296
297
    /**
298
     * @param string $interface
299
     * @param Junction $junction
300
     * @return $this
301
     */
302
    public function setJunction (string $interface, Junction $junction) {
303
        $this->junctions[$interface] = $junction;
304
        return $this;
305
    }
306
307
    /**
308
     * @param Closure $logger
309
     * @return $this
310
     */
311
    public function setLogger (Closure $logger) {
312
        $this->logger = $logger;
313
        return $this;
314
    }
315
316
    /**
317
     * @param string $class
318
     * @param Record $record
319
     * @return $this
320
     */
321
    public function setRecord (string $class, Record $record) {
322
        $this->records[$class] = $record;
323
        return $this;
324
    }
325
}