Passed
Push — master ( 331e8c...9d3685 )
by y
01:19
created

DB::getLogger()   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 0
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\Junction;
11
use Helix\DB\Record;
12
use Helix\DB\Select;
13
use Helix\DB\SQL;
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 Junction[]
24
     */
25
    protected $junctions = [];
26
27
    /**
28
     * Notified whenever a query is executed or a statement is prepared.
29
     * Takes only one argument: the SQL being executed or prepared.
30
     * This is a stub closure by default.
31
     *
32
     * @var Closure
33
     */
34
    protected $logger;
35
36
    /**
37
     * @var Record[]
38
     */
39
    protected $records = [];
40
41
    /**
42
     * Sets various attributes to streamline operations.
43
     *
44
     * @param string $dsn
45
     * @param string $username
46
     * @param string $passwd
47
     * @param array $options
48
     */
49
    public function __construct ($dsn, $username = null, $passwd = null, $options = null) {
50
        parent::__construct(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $dsn of PDO::__construct() 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

50
        parent::__construct(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
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 conditions from mixed arguments.
117
     *
118
     * When `$a` is an array:
119
     *
120
     * - Integer keys with string values are used as-is.
121
     * - String keys are used for `$a`, and values are used for `$b`
122
     *
123
     * In both cases, an enumerated array of strings is returned.
124
     *
125
     * When `$b` is a closure, it's invoked with `$a` and the `DB` instance.
126
     * The closure must return a string.
127
     *
128
     * Otherwise {@link SQL::isEqual()} is used, and `$b` is quoted.
129
     *
130
     * @see quote()
131
     * @see SQL::isEqual()
132
     *
133
     * @param string|array $a
134
     * @param string|array|Select|Closure $b
135
     * @return string|string[] Same type as `$a`, keys are not preserved.
136
     */
137
    public function match ($a, $b = null) {
138
        if (is_array($a)) {
139
            return array_map([$this, __FUNCTION__], array_keys($a), $a);
140
        }
141
        if (is_int($a) and is_string($b)) {
0 ignored issues
show
introduced by
The condition is_int($a) is always false.
Loading history...
142
            return $b;
143
        }
144
        if ($b instanceof Closure) {
145
            return $b->__invoke($a, $this);
146
        }
147
        return SQL::isEqual($a, $this->quote($b));
148
    }
149
150
    /**
151
     * @param string $access Class or interface name.
152
     * @return bool
153
     */
154
    public function offsetExists ($access): bool {
155
        return (bool)$this->offsetGet($access);
156
    }
157
158
    /**
159
     * @param string $access Class or interface name.
160
     * @return Record|Junction|null
161
     */
162
    public function offsetGet ($access) {
163
        if (class_exists($access)) {
164
            return $this->getRecord($access);
165
        }
166
        if (interface_exists($access)) {
167
            return $this->getJunction($access);
168
        }
169
        return null;
170
    }
171
172
    /**
173
     * Throws.
174
     *
175
     * @param void $access
176
     * @param void $value
177
     * @throws Exception
178
     */
179
    final public function offsetSet ($access, $value): void {
180
        throw new Exception('The schema is immutable.');
181
    }
182
183
    /**
184
     * Throws.
185
     *
186
     * @param void $access
187
     * @throws Exception
188
     */
189
    final public function offsetUnset ($access): void {
190
        $this->offsetSet($access, null);
191
    }
192
193
    /**
194
     * Notifies the logger.
195
     *
196
     * @param string $sql
197
     * @param array $options
198
     * @return Statement
199
     */
200
    public function prepare ($sql, $options = null): Statement {
201
        $this->logger->__invoke($sql);
202
        /** @var Statement $statement */
203
        $statement = parent::prepare(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $statement of PDO::prepare() 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

203
        $statement = parent::prepare(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
204
        return $statement;
205
    }
206
207
    /**
208
     * Notifies the logger.
209
     *
210
     * @param string $sql
211
     * @param int $mode
212
     * @param null $arg3
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $arg3 is correct as it would always require null to be passed?
Loading history...
213
     * @param array $ctorargs
214
     * @return Statement
215
     */
216
    public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []): Statement {
217
        $this->logger->__invoke($sql);
218
        /** @var Statement $statement */
219
        $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

219
        $statement = parent::query(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
220
        return $statement;
221
    }
222
223
    /**
224
     * Quotes a value or an array of values recursively.
225
     * Array keys are preserved.
226
     *
227
     * - Booleans and resources are cast to integer.
228
     * - Integers are returned as-is.
229
     * - Everything else is quoted as a string.
230
     *
231
     * There are two cases where values are not quoted in order to allow
232
     * safe pass-through to the SQL helper functions:
233
     *
234
     * - {@link Column} is returned as its string representation (the qualified name).
235
     * - {@link Select} is returned back so it may be used as an anonymous subquery.
236
     *
237
     * @param mixed $value
238
     * @param int $type Ignored.
239
     * @return mixed
240
     */
241
    public function quote ($value, $type = null) {
242
        if ($value instanceof Column) {
243
            return (string)$value;
244
        }
245
        if ($value instanceof Select) {
246
            return $value;
247
        }
248
        switch (gettype($value)) {
249
            case 'array' :
250
                return array_map([$this, __FUNCTION__], $value);
251
            case 'integer' :
252
            case 'boolean' :
253
            case 'resource' :
254
                return (int)$value;
255
            default:
256
                return parent::quote($value);
257
        }
258
    }
259
260
    /**
261
     * Forwards to the entity's {@link Record}
262
     *
263
     * @param EntityInterface $entity
264
     * @return int ID
265
     */
266
    public function save (EntityInterface $entity): int {
267
        return $this->getRecord($entity)->save($entity);
268
    }
269
270
    /**
271
     * @param Closure $logger
272
     * @return $this
273
     */
274
    public function setLogger (Closure $logger) {
275
        $this->logger = $logger;
276
        return $this;
277
    }
278
}