Completed
Push — master ( 85d2a2...f0e67d )
by Ivan
04:12
created

DB   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 62.11%

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 8
dl 0
loc 385
ccs 100
cts 161
cp 0.6211
rs 2.88
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
F __construct() 0 59 21
A driver() 0 4 1
A prepare() 0 4 1
A test() 0 4 1
B expand() 0 25 6
A query() 0 8 5
A raw() 0 4 1
B get() 0 36 8
A one() 0 4 1
A all() 0 4 1
A unbuffered() 0 9 1
A begin() 0 7 2
A commit() 0 7 2
A rollback() 0 7 2
A driverName() 0 4 1
A driverOption() 0 4 1
A definition() 0 6 2
A parseSchema() 0 5 1
A getSchema() 0 30 3
A setSchema() 0 29 5
A table() 0 6 2
A __call() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like DB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DB, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace vakata\database;
4
5
use \vakata\collection\Collection;
6
use \vakata\database\schema\Table;
7
use \vakata\database\schema\TableQuery;
8
use \vakata\database\schema\TableQueryMapped;
9
use \vakata\database\schema\TableRelation;
10
11
/**
12
 * A database abstraction with support for various drivers (mySQL, postgre, oracle, msSQL, sphinx, and even PDO).
13
 */
14
class DB implements DBInterface
15
{
16
    /**
17
     * @var DriverInterface
18
     */
19
    protected $driver;
20
    /**
21
     * @var Table[]
22
     */
23
    protected $tables = [];
24
25
    /**
26
     * Create an instance.
27
     *
28
     * @param string $connectionString a driver instance or a connection string
29
     */
30 15
    public function __construct(string $connectionString)
31
    {
32
        $connection = [
33 15
            'orig' => $connectionString,
34
            'type' => null,
35
            'user' => null,
36
            'pass' => null,
37
            'host' => null,
38
            'port' => null,
39
            'name' => null,
40
            'opts' => []
41
        ];
42
        $aliases = [
43 15
            'my'        => 'mysql',
44
            'mysqli'    => 'mysql',
45
            'pg'        => 'postgre',
46
            'oci'       => 'oracle',
47
            'firebird'  => 'ibase'
48
        ];
49 15
        $temp = parse_url($connectionString);
50 15
        if ($temp === false || (isset($temp['query']) && strpos($temp['query'], 'regexparser=1') !== false)) {
51 6
            if (!preg_match(
52 6
                '(^
53
                    (?<scheme>.*?)://
54
                    (?:(?<user>.*?)(?:\:(?<pass>.*))?@)?
55
                    (?<host>[a-zа-я.\-_0-9=();:]+?) # added =();: for oracle and pdo configs
56
                    (?:\:(?<port>\d+))?
57
                    (?<path>/.+?)? # path is optional for oracle and pdo configs
58
                    (?:\?(?<query>.*))?
59
                $)xui',
60 6
                $connectionString,
61 6
                $temp
62
            )) {
63
                $temp = explode('://', $connectionString, 2);
64
                if (!preg_match('(^[a-z0-9_]+$)i', $temp[0])) {
65
                    throw new DBException('Could not parse connection string');
66
                }
67
                $temp = [
68
                    'scheme' => $temp[0]
69
                ];
70
            }
71
        }
72 15
        $connection['type'] = isset($temp['scheme']) && strlen($temp['scheme']) ? $temp['scheme'] : null;
73 15
        $connection['user'] = isset($temp['user']) && strlen($temp['user']) ? $temp['user'] : null;
74 15
        $connection['pass'] = isset($temp['pass']) && strlen($temp['pass']) ? $temp['pass'] : null;
75 15
        $connection['host'] = isset($temp['host']) && strlen($temp['host']) ? $temp['host'] : null;
76 15
        $connection['name'] = isset($temp['path']) && strlen($temp['path']) ? trim($temp['path'], '/') : null;
77 15
        $connection['port'] = isset($temp['port']) && (int)$temp['port'] ? (int)$temp['port'] : null;
78 15
        if (isset($temp['query']) && strlen($temp['query'])) {
79 15
            parse_str($temp['query'], $connection['opts']);
80
        }
81
        // create the driver
82 15
        $connection['type'] = $aliases[$connection['type']] ?? $connection['type'];
83 15
        $tmp = '\\vakata\\database\\driver\\'.strtolower($connection['type']).'\\Driver';
84 15
        if (!class_exists($tmp)) {
85
            throw new DBException('Unknown DB backend');
86
        }
87 15
        $this->driver = new $tmp($connection);
88 15
    }
89
90
    public function driver(): DriverInterface
91
    {
92
        return $this->driver;
93
    }
94
95
    /**
96
     * Prepare a statement.
97
     * Use only if you need a single query to be performed multiple times with different parameters.
98
     *
99
     * @param string $sql the query to prepare - use `?` for arguments
100
     * @return StatementInterface the prepared statement
101
     */
102 1
    public function prepare(string $sql) : StatementInterface
103
    {
104 1
        return $this->driver->prepare($sql);
105
    }
106
    /**
107
     * Test the connection
108
     *
109
     * @return bool
110
     */
111 1
    public function test() : bool
112
    {
113 1
        return $this->driver->test();
114
    }
115 27
    protected function expand(string $sql, $par = null) : array
116
    {
117 27
        $new = '';
118 27
        $par = array_values($par);
119 27
        if (substr_count($sql, '?') === 2 && !is_array($par[0])) {
120 3
            $par = [ $par ];
121
        }
122 27
        $parts = explode('??', $sql);
123 27
        $index = 0;
124 27
        foreach ($parts as $part) {
125 27
            $tmp = explode('?', $part);
126 27
            $new .= $part;
127 27
            $index += count($tmp) - 1;
128 27
            if (isset($par[$index])) {
129 27
                if (!is_array($par[$index])) {
130
                    $par[$index] = [ $par[$index] ];
131
                }
132 27
                $params = $par[$index];
133 27
                array_splice($par, $index, 1, $params);
134 27
                $index += count($params);
135 27
                $new .= implode(',', array_fill(0, count($params), '?'));
136
            }
137
        }
138 27
        return [ $new, $par ];
139
    }
140
    /**
141
     * Run a query (prepare & execute).
142
     * @param string   $sql   SQL query
143
     * @param mixed    $par   parameters (optional)
144
     * @param bool     $buff  should the results be buffered (defaults to true)
145
     * @return ResultInterface the result of the execution
146
     */
147 166
    public function query(string $sql, $par = null, bool $buff = true) : ResultInterface
148
    {
149 166
        $par = isset($par) ? (is_array($par) ? $par : [$par]) : [];
150 166
        if (strpos($sql, '??') && count($par)) {
151 27
            list($sql, $par) = $this->expand($sql, $par);
152
        }
153 166
        return $this->driver->prepare($sql)->execute($par, $buff);
154
    }
155
    /**
156
     * Run a query.
157
     * @param string   $sql   SQL query
158
     * @return mixed the result of the execution
159
     */
160
    public function raw(string $sql)
161
    {
162
        return $this->driver->raw($sql);
163
    }
164
    /**
165
     * Run a SELECT query and get an array-like result.
166
     * When using `get` the data is kept in the database client and fetched as needed (not in PHP memory as with `all`)
167
     *
168
     * @param string   $sql      SQL query
169
     * @param array    $par      parameters
170
     * @param string   $key      column name to use as the array index
171
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
172
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
173
     * @param bool     $buff     should the results be buffered (defaults to `false`)
174
     *
175
     * @return Collection the result of the execution
176
     */
177 160
    public function get(
178
        string $sql,
179
        $par = null,
180
        string $key = null,
181
        bool $skip = false,
182
        bool $opti = true,
183
        bool $buff = true
184
    ): Collection {
185 160
        $coll = Collection::from($this->query($sql, $par, $buff));
186 160
        if (($keys = $this->driver->option('mode')) && in_array($keys, ['strtoupper', 'strtolower'])) {
187 1
            $coll->map(function ($v) use ($keys) {
188 1
                $new = [];
189 1
                foreach ($v as $k => $vv) {
190 1
                    $new[call_user_func($keys, $k)] = $vv;
191
                }
192 1
                return $new;
193 1
            });
194
        }
195 160
        if ($key !== null) {
196 2
            $coll->mapKey(function ($v) use ($key) {
197 2
                return $v[$key];
198 2
            });
199
        }
200 160
        if ($skip) {
201 2
            $coll->map(function ($v) use ($key) {
202 2
                unset($v[$key]);
203 2
                return $v;
204 2
            });
205
        }
206 160
        if ($opti) {
207 67
            $coll->map(function ($v) {
208 67
                return count($v) === 1 ? current($v) : $v;
209 67
            });
210
        }
211 160
        return $coll;
212
    }
213
    /**
214
     * Run a SELECT query and get a single row
215
     * @param string   $sql      SQL query
216
     * @param array    $par      parameters
217
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
218
     * @return mixed the result of the execution
219
     */
220 63
    public function one(string $sql, $par = null, bool $opti = true)
221
    {
222 63
        return $this->get($sql, $par, null, false, $opti, true)->value();
223
    }
224
    /**
225
     * Run a SELECT query and get an array
226
     * @param string   $sql      SQL query
227
     * @param array    $par      parameters
228
     * @param string   $key      column name to use as the array index
229
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
230
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
231
     * @return array the result of the execution
232
     */
233 6
    public function all(string $sql, $par = null, string $key = null, bool $skip = false, bool $opti = true) : array
234
    {
235 6
        return $this->get($sql, $par, $key, $skip, $opti, true)->toArray();
236
    }
237
    public function unbuffered(
238
        string $sql,
239
        $par = null,
240
        string $key = null,
241
        bool $skip = false,
242
        bool $opti = true
243
    ) : Collection {
244
        return $this->get($sql, $par, $key, $skip, $opti, false);
245
    }
246
    /**
247
     * Begin a transaction.
248
     * @return $this
249
     */
250 1
    public function begin() : DBInterface
251
    {
252 1
        if (!$this->driver->begin()) {
253
            throw new DBException('Could not begin');
254
        }
255 1
        return $this;
256
    }
257
    /**
258
     * Commit a transaction.
259
     * @return $this
260
     */
261 1
    public function commit() : DBInterface
262
    {
263 1
        if (!$this->driver->commit()) {
264
            throw new DBException('Could not commit');
265
        }
266 1
        return $this;
267
    }
268
    /**
269
     * Rollback a transaction.
270
     * @return $this
271
     */
272 1
    public function rollback() : DBInterface
273
    {
274 1
        if (!$this->driver->rollback()) {
275
            throw new DBException('Could not rollback');
276
        }
277 1
        return $this;
278
    }
279
    /**
280
     * Get the current driver name (`"mysql"`, `"postgre"`, etc).
281
     * @return string the current driver name
282
     */
283 16
    public function driverName() : string
284
    {
285 16
        return array_reverse(explode('\\', get_class($this->driver)))[1];
286
    }
287
    /**
288
     * Get an option from the driver
289
     *
290
     * @param string $key     the option name
291
     * @param mixed  $default the default value to return if the option key is not defined
292
     * @return mixed the option value
293
     */
294 76
    public function driverOption(string $key, $default = null)
295
    {
296 76
        return $this->driver->option($key, $default);
297
    }
298
299 156
    public function definition(string $table, bool $detectRelations = true) : Table
300
    {
301 156
        return isset($this->tables[$table]) ?
302
            $this->tables[$table] :
303 156
            $this->driver->table($table, $detectRelations);
304
    }
305
    /**
306
     * Parse all tables from the database.
307
     * @return $this
308
     */
309
    public function parseSchema()
310
    {
311
        $this->tables = $this->driver->tables();
312
        return $this;
313
    }
314
    /**
315
     * Get the full schema as an array that you can serialize and store
316
     * @return array
317
     */
318
    public function getSchema($asPlainArray = true)
319
    {
320 4
        return !$asPlainArray ? $this->tables : array_map(function ($table) {
321
            return [
322
                'name' => $table->getName(),
323
                'pkey' => $table->getPrimaryKey(),
324
                'comment' => $table->getComment(),
325
                'columns' => array_map(function ($column) {
326
                    return [
327
                        'name' => $column->getName(),
328
                        'type' => $column->getType(),
329
                        'length' => $column->getLength(),
330
                        'comment' => $column->getComment(),
331
                        'values' => $column->getValues(),
332
                        'default' => $column->getDefault(),
333
                        'nullable' => $column->isNullable()
334
                    ];
335
                }, $table->getFullColumns()),
336
                'relations' => array_map(function ($rel) {
337
                    $relation = clone $rel;
338
                    $relation = (array)$relation;
339
                    $relation['table'] = $rel->table->getName();
340
                    if ($rel->pivot) {
341
                        $relation['pivot'] = $rel->pivot->getName();
342
                    }
343
                    return $relation;
344
                }, $table->getRelations())
345
            ];
346 4
        }, $this->tables);
347
    }
348
    /**
349
     * Load the schema data from a schema definition array (obtained from getSchema)
350
     * @param  array        $data the schema definition
351
     * @return $this
352
     */
353
    public function setSchema(array $data)
354
    {
355
        foreach ($data as $tableData) {
356
            $this->tables[$tableData['name']] = (new Table($tableData['name']))
357
                        ->setPrimaryKey($tableData['pkey'])
358
                        ->setComment($tableData['comment'])
359
                        ->addColumns($tableData['columns']);
360
        }
361
        foreach ($data as $tableData) {
362
            $table = $this->definition($tableData['name']);
363
            foreach ($tableData['relations'] as $relationName => $relationData) {
364
                $relationData['table'] = $this->definition($relationData['table']);
365
                if ($relationData['pivot']) {
366
                    $relationData['pivot'] = $this->definition($relationData['pivot']);
367
                }
368
                $table->addRelation(new TableRelation(
369
                    $relationData['name'],
370
                    $relationData['table'],
371
                    $relationData['keymap'],
372
                    $relationData['many'],
373
                    $relationData['pivot'] ?? null,
374
                    $relationData['pivot_keymap'],
375
                    $relationData['sql'],
376
                    $relationData['par']
377
                ));
378
            }
379
        }
380
        return $this;
381
    }
382
383
    /**
384
     * Initialize a table query
385
     * @param string $table the table to query
386
     * @return TableQuery
387
     */
388 156
    public function table(string $table, bool $mapped = false)
389
    {
390 156
        return $mapped ?
391 80
            new TableQueryMapped($this, $this->definition($table)) :
392 156
            new TableQuery($this, $this->definition($table));
393
    }
394 148
    public function __call($method, $args)
395
    {
396 148
        return $this->table($method, $args[0] ?? false);
397
    }
398
}
399