Passed
Branch master (a9567f)
by y
01:25
created

DB::quoteMixed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
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\EntityInterface;
9
use Helix\DB\ExpressionInterface;
10
use Helix\DB\Junction;
11
use Helix\DB\Record;
12
use Helix\DB\SQL;
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 Junction[]
23
     */
24
    protected $junctions = [];
25
26
    /**
27
     * Notified whenever a query is executed or a statement is prepared.
28
     * Takes only one argument: the SQL being executed or prepared.
29
     * This is a stub closure by default.
30
     *
31
     * @var Closure
32
     */
33
    protected $logger;
34
35
    /**
36
     * @var Record[]
37
     */
38
    protected $records = [];
39
40
    /**
41
     * Sets various attributes to streamline operations.
42
     *
43
     * @param string $dsn
44
     * @param string $username
45
     * @param string $passwd
46
     * @param array $options
47
     */
48
    public function __construct ($dsn, $username = null, $passwd = null, $options = null) {
49
        /** @scrutinizer ignore-call */
50
        parent::__construct(...func_get_args());
51
        $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC);
52
        $this->setAttribute(self::ATTR_EMULATE_PREPARES, false);
53
        $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION);
54
        $this->setAttribute(self::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]);
55
        $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false);
56
        $this->logger = function() { };
57
    }
58
59
    /**
60
     * Notifies the logger.
61
     *
62
     * @param string $sql
63
     * @return int
64
     */
65
    public function exec ($sql): int {
66
        $this->logger->__invoke($sql);
67
        return parent::exec($sql);
68
    }
69
70
    /**
71
     * `ATTR_DRIVER_NAME`
72
     *
73
     * @return string
74
     */
75
    final public function getDriver (): string {
76
        return $this->getAttribute(self::ATTR_DRIVER_NAME);
77
    }
78
79
    /**
80
     * Returns a `Junction` access object based on an annotated interface.
81
     *
82
     * @param string $interface
83
     * @return Junction
84
     */
85
    public function getJunction ($interface): Junction {
86
        if (!isset($this->junctions[$interface])) {
87
            $this->junctions[$interface] = new Junction($this, $interface);
88
        }
89
        return $this->junctions[$interface];
90
    }
91
92
    /**
93
     * @return Closure
94
     */
95
    public function getLogger (): Closure {
96
        return $this->logger;
97
    }
98
99
    /**
100
     * Returns a `Record` access object based on an annotated class.
101
     *
102
     * @param string|EntityInterface $class
103
     * @return Record
104
     */
105
    public function getRecord ($class): Record {
106
        if (is_object($class)) {
107
            $class = get_class($class);
108
        }
109
        if (!isset($this->records[$class])) {
110
            $this->records[$class] = new Record($this, $class);
111
        }
112
        return $this->records[$class];
113
    }
114
115
    /**
116
     * Generates string conditions from mixed arguments.
117
     *
118
     * When `$b` is a closure, it's invoked with `(DB $this, string $a)`.
119
     * The closure must return a string.
120
     *
121
     * For all other types of `$b`, {@link SQL::isEqual()} is used and `$b` is quoted.
122
     *
123
     * @see quote()
124
     * @see SQL::isEqual()
125
     *
126
     * @param int|string $a
127
     * @param mixed $b
128
     * @return string
129
     */
130
    public function match ($a, $b = null) {
131
        if (is_int($a) and is_string($b)) {
132
            return $b;
133
        }
134
        if ($b instanceof Closure) {
135
            return (string)$b->__invoke($this, $a);
136
        }
137
        return SQL::isEqual($a, $this->quote($b));
138
    }
139
140
    /**
141
     * Generates an array of string conditions from an input array.
142
     *
143
     * - Integer keys with string values are returned as-is
144
     * - String keys are used for `$a`, and values are used for `$b`
145
     *
146
     * Keys are not preserved, an enumerated array of strings is returned.
147
     *
148
     * @see match()
149
     *
150
     * @param array $match
151
     * @return string[]
152
     */
153
    public function matchArray (array $match) {
154
        return array_map([$this, 'match'], array_keys($match), $match);
155
    }
156
157
    /**
158
     * Generates a string condition or an array of string conditions.
159
     *
160
     * @see match()
161
     *
162
     * @param mixed $match
163
     * @return string|string[]
164
     */
165
    public function matchMixed ($match) {
166
        if (is_array($match)) {
167
            return $this->matchArray($match);
168
        }
169
        return $this->match($match);
170
    }
171
172
    /**
173
     * @param string $access Class or interface name.
174
     * @return bool
175
     */
176
    public function offsetExists ($access): bool {
177
        return (bool)$this->offsetGet($access);
178
    }
179
180
    /**
181
     * @param string $access Class or interface name.
182
     * @return Record|Junction|null
183
     */
184
    public function offsetGet ($access) {
185
        if (class_exists($access)) {
186
            return $this->getRecord($access);
187
        }
188
        if (interface_exists($access)) {
189
            return $this->getJunction($access);
190
        }
191
        return null;
192
    }
193
194
    /**
195
     * Throws.
196
     *
197
     * @param void $access
198
     * @param void $value
199
     * @throws Exception
200
     */
201
    final public function offsetSet ($access, $value): void {
202
        throw new Exception('The schema is immutable.');
203
    }
204
205
    /**
206
     * Throws.
207
     *
208
     * @param void $access
209
     * @throws Exception
210
     */
211
    final public function offsetUnset ($access): void {
212
        $this->offsetSet($access, null);
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 = null): Statement {
223
        $this->logger->__invoke($sql);
224
        /** @var Statement $statement */
225
        /** @scrutinizer ignore-call */
226
        $statement = parent::prepare(...func_get_args());
227
        return $statement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $statement returns the type boolean which is incompatible with the type-hinted return Helix\DB\Statement.
Loading history...
228
    }
229
230
    /**
231
     * Notifies the logger.
232
     *
233
     * @param string $sql
234
     * @param int $mode
235
     * @param mixed $arg3
236
     * @param array $ctorargs
237
     * @return Statement
238
     */
239
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []): Statement {
240
        $this->logger->__invoke($sql);
241
        /** @var Statement $statement */
242
        /** @scrutinizer ignore-call */
243
        $statement = parent::query(...func_get_args());
244
        return $statement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $statement returns the type boolean which is incompatible with the type-hinted return Helix\DB\Statement.
Loading history...
245
    }
246
247
    /**
248
     * Quotes a value, with special considerations.
249
     *
250
     * - {@link ExpressionInterface} instances are returned back as-is.
251
     * - Booleans and resources are returned as and unquoted integer string.
252
     * - Integers are returned as an unquoted string.
253
     * - Everything else is quoted as a string.
254
     *
255
     * @param mixed $value
256
     * @param int $type Ignored.
257
     * @return string
258
     */
259
    public function quote ($value, $type = null) {
260
        if ($value instanceof ExpressionInterface) {
261
            return $value;
262
        }
263
        switch (gettype($value)) {
264
            case 'integer' :
265
            case 'boolean' :
266
            case 'resource' :
267
                return (string)(int)$value;
268
            default:
269
                return parent::quote($value);
270
        }
271
    }
272
273
    /**
274
     * Quotes an array of values. Keys are preserved.
275
     *
276
     * @param array $values
277
     * @return array
278
     */
279
    public function quoteArray (array $values): array {
280
        return array_map([$this, 'quote'], $values);
281
    }
282
283
    /**
284
     * Accepts an array or string and returns it quoted.
285
     *
286
     * @param mixed $value
287
     * @return array|string
288
     */
289
    public function quoteMixed ($value) {
290
        if (is_array($value)) {
291
            return $this->quoteArray($value);
292
        }
293
        return $this->quote($value);
294
    }
295
296
    /**
297
     * Forwards to the entity's {@link Record}
298
     *
299
     * @param EntityInterface $entity
300
     * @return int ID
301
     */
302
    public function save (EntityInterface $entity): int {
303
        return $this->getRecord($entity)->save($entity);
304
    }
305
306
    /**
307
     * @param Closure $logger
308
     * @return $this
309
     */
310
    public function setLogger (Closure $logger) {
311
        $this->logger = $logger;
312
        return $this;
313
    }
314
}