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

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