Passed
Push — master ( ff844e...3fca7d )
by y
01:24
created

DB::setRecord()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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 InvalidArgumentException;
15
use PDO;
16
17
/**
18
 * Extends `PDO` and acts as a central access point for the schema.
19
 */
20
class DB extends PDO implements ArrayAccess {
21
22
    /**
23
     * @var string
24
     */
25
    private $driver;
26
27
    /**
28
     * @var Junction[]
29
     */
30
    protected $junctions = [];
31
32
    /**
33
     * Notified whenever a query is executed or a statement is prepared.
34
     * Takes only one argument: the SQL being executed or prepared.
35
     * This is a stub closure by default.
36
     *
37
     * @var Closure
38
     */
39
    protected $logger;
40
41
    /**
42
     * @var Record[]
43
     */
44
    protected $records = [];
45
46
    /**
47
     * Sets various attributes to streamline operations.
48
     *
49
     * Registers missing SQLite functions.
50
     *
51
     * @param string $dsn
52
     * @param string $username
53
     * @param string $passwd
54
     * @param array $options
55
     */
56
    public function __construct ($dsn, $username = null, $passwd = null, $options = null) {
57
        parent::__construct($dsn, $username, $passwd, $options);
58
        $this->driver = $this->getAttribute(self::ATTR_DRIVER_NAME);
59
        $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC);
60
        $this->setAttribute(self::ATTR_EMULATE_PREPARES, false);
61
        $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION);
62
        $this->setAttribute(self::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]);
63
        $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false);
64
        $this->logger = function() { };
65
        if ($this->driver === 'sqlite') {
66
            $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

66
            $this->/** @scrutinizer ignore-call */ 
67
                   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...
67
            $this->sqliteCreateFunction('FLOOR', 'floor');
68
            $this->sqliteCreateFunction('POW', 'pow');
69
        }
70
    }
71
72
    /**
73
     * Notifies the logger.
74
     *
75
     * @param string $sql
76
     * @return int
77
     */
78
    public function exec ($sql): int {
79
        $this->logger->__invoke($sql);
80
        return parent::exec($sql);
81
    }
82
83
    /**
84
     * @return string
85
     */
86
    final public function getDriver (): string {
87
        return $this->driver;
88
    }
89
90
    /**
91
     * Returns a `Junction` access object based on an annotated interface.
92
     *
93
     * @param string $interface
94
     * @return Junction
95
     */
96
    public function getJunction ($interface) {
97
        if (!isset($this->junctions[$interface])) {
98
            $this->junctions[$interface] = Junction::fromInterface($this, $interface);
99
        }
100
        return $this->junctions[$interface];
101
    }
102
103
    /**
104
     * @return Closure
105
     */
106
    public function getLogger () {
107
        return $this->logger;
108
    }
109
110
    /**
111
     * Returns a `Record` access object based on an annotated class.
112
     *
113
     * @param string|EntityInterface $class
114
     * @return Record
115
     */
116
    public function getRecord ($class) {
117
        $name = $class;
118
        if (is_object($name)) {
119
            $name = get_class($name);
120
        }
121
        if (!isset($this->records[$name])) {
122
            $this->records[$name] = Record::fromClass($this, $class);
123
        }
124
        return $this->records[$name];
125
    }
126
127
    /**
128
     * Generates string conditions from mixed arguments.
129
     *
130
     * When `$b` is a closure, this returns from that.
131
     * It's invoked with `(Column $a, DB $this)`
132
     *
133
     * If `$a` is an integer, `$b` is returned as a string.
134
     *
135
     * For all other types of `$b`, it's quoted and checked for equivalence with `$a`
136
     *
137
     * @param mixed $a
138
     * @param mixed $b
139
     * @return string
140
     */
141
    public function match ($a, $b) {
142
        if ($b instanceof Closure) {
143
            if (!$a instanceof Column) {
144
                $a = new Column($this, $a);
145
            }
146
            return (string)$b->__invoke($a, $this);
147
        }
148
        if (is_int($a)) {
149
            return (string)$b;
150
        }
151
        return (string)Predicate::compare($a, $this->quoteMixed($b));
152
    }
153
154
    /**
155
     * Generates an array of string conditions from an input array.
156
     *
157
     * - Integer keys with string values are returned as-is
158
     * - String keys are used for `$a`, and values are used for `$b`
159
     *
160
     * Keys are not preserved, an enumerated array of strings is returned.
161
     *
162
     * @see match()
163
     *
164
     * @param array $match
165
     * @return string[]
166
     */
167
    public function matchArray (array $match) {
168
        return array_map([$this, 'match'], array_keys($match), $match);
169
    }
170
171
    /**
172
     * @param string $class Class or interface name.
173
     * @return bool
174
     */
175
    public function offsetExists ($class): bool {
176
        return (bool)$this->offsetGet($class);
177
    }
178
179
    /**
180
     * @param string $class Class or interface name.
181
     * @return null|Record|Junction
182
     */
183
    public function offsetGet ($class) {
184
        if (class_exists($class)) {
185
            return $this->getRecord($class);
186
        }
187
        if (interface_exists($class)) {
188
            return $this->getJunction($class);
189
        }
190
        return null;
191
    }
192
193
    /**
194
     * @param string $class Class or interface name.
195
     * @param Record|Junction $access
196
     */
197
    public function offsetSet ($class, $access) {
198
        if ($access instanceof Record) {
199
            $this->setRecord($class, $access);
200
        }
201
        elseif ($access instanceof Junction) {
0 ignored issues
show
introduced by
$access is always a sub-type of Helix\DB\Junction.
Loading history...
202
            $this->setJunction($class, $access);
203
        }
204
        throw new InvalidArgumentException('Expected a Record or Junction.');
205
    }
206
207
    /**
208
     * @param string $class Class or interface name.
209
     */
210
    public function offsetUnset ($class) {
211
        unset($this->records[$class]);
212
        unset($this->junctions[$class]);
213
    }
214
215
    /**
216
     * Notifies the logger.
217
     *
218
     * @param string $sql
219
     * @param array $options
220
     * @return Statement
221
     */
222
    public function prepare ($sql, $options = []) {
223
        $this->logger->__invoke($sql);
224
        /** @var Statement $statement */
225
        $statement = parent::prepare($sql, $options);
226
        return $statement;
227
    }
228
229
    /**
230
     * Notifies the logger.
231
     *
232
     * @param string $sql
233
     * @param int $mode
234
     * @param mixed $arg3
235
     * @param array $ctorargs
236
     * @return Statement
237
     */
238
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) {
239
        $this->logger->__invoke($sql);
240
        /** @var Statement $statement */
241
        $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

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