Completed
Push — master ( 835dfc...c628c4 )
by Ivan
02:03
created

DB   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 65.06%

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 8
dl 0
loc 344
ccs 95
cts 146
cp 0.6506
rs 6.96
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 2
B getDriver() 0 50 6
A prepare() 0 4 1
A test() 0 4 1
B expand() 0 25 6
A query() 0 8 5
B get() 0 23 8
A one() 0 4 1
A all() 0 4 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 29 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 DriverInterface|string $driver a driver instance or a connection string
29
     */
30 15
    public function __construct($driver) {
31 15
        $this->driver = $driver instanceof DriverInterface ? $driver : static::getDriver($driver);
32 15
    }
33
    /**
34
     * Create a driver instance from a connection string
35
     * @param string $connectionString the connection string
36
     * @return DriverInterface
37
     */
38 15
    public static function getDriver(string $connectionString)
39
    {
40
        $connection = [
41 15
            'orig' => $connectionString,
42
            'type' => null,
43
            'user' => null,
44
            'pass' => null,
45
            'host' => null,
46
            'port' => null,
47
            'name' => null,
48
            'opts' => []
49
        ];
50
        $aliases = [
51 15
            'mysqli' => 'mysql',
52
            'pg' => 'postgre',
53
            'oci' => 'oracle',
54
            'firebird' => 'ibase'
55
        ];
56 15
        $connectionString = array_pad(explode('://', $connectionString, 2), 2, '');
57 15
        $connection['type'] = $connectionString[0];
58 15
        $connectionString = $connectionString[1];
59
60 15
        $host = substr($connectionString, 0, strrpos($connectionString, '/'));
61 15
        $path = substr($connectionString, strrpos($connectionString, '/') + 1);
62
63 15
        if (strpos($host, '@') !== false) {
64 9
            $auth = substr($host, 0, strrpos($host, '@'));
65 9
            $host = substr($host, strrpos($host, '@') + 1);
66 9
            list($connection['user'], $connection['pass']) = array_pad(explode(':', $auth, 2), 2, '');
67
        }
68 15
        list($connection['host'], $connection['port']) = array_pad(explode(':', $host, 2), 2, null);
69
70 15
        if ($pos = strrpos($path, '?')) {
71 15
            $opt = substr($path, $pos + 1);
72 15
            parse_str($opt, $connection['opts']);
73 15
            if ($connection['opts'] && count($connection['opts'])) {
74 15
                $connectionString = substr($connectionString, 0, $pos);
0 ignored issues
show
Unused Code introduced by
$connectionString is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
75
            } else {
76
                $connection['opts'] = [];
77
            }
78 15
            $connection['name'] = substr($path, 0, $pos);
79
        } else {
80 1
            $connection['name'] = $path;
81
        }
82 15
        $connection['type'] = isset($aliases[$connection['type']]) ?
83
            $aliases[$connection['type']] :
84 15
            $connection['type'];
85 15
        $tmp = '\\vakata\\database\\driver\\'.strtolower($connection['type']).'\\Driver';
86 15
        return new $tmp($connection);
87
    }
88
    /**
89
     * Prepare a statement.
90
     * Use only if you need a single query to be performed multiple times with different parameters.
91
     *
92
     * @param string $sql the query to prepare - use `?` for arguments
93
     * @return StatementInterface the prepared statement
94
     */
95 1
    public function prepare(string $sql) : StatementInterface
96
    {
97 1
        return $this->driver->prepare($sql);
98
    }
99
    /**
100
     * Test the connection
101
     *
102
     * @return bool
103
     */
104 1
    public function test() : bool
105
    {
106 1
        return $this->driver->test();
107
    }
108 27
    protected function expand(string $sql, $par = null) : array
109
    {
110 27
        $new = '';
111 27
        $par = array_values($par);
112 27
        if (substr_count($sql, '?') === 2 && !is_array($par[0])) {
113 3
            $par = [ $par ];
114
        }
115 27
        $parts = explode('??', $sql);
116 27
        $index = 0;
117 27
        foreach ($parts as $part) {
118 27
            $tmp = explode('?', $part);
119 27
            $new .= $part;
120 27
            $index += count($tmp) - 1;
121 27
            if (isset($par[$index])) {
122 27
                if (!is_array($par[$index])) {
123
                    $par[$index] = [ $par[$index] ];
124
                }
125 27
                $params = $par[$index];
126 27
                array_splice($par, $index, 1, $params);
127 27
                $index += count($params);
128 27
                $new .= implode(',', array_fill(0, count($params), '?'));
129
            }
130
        }
131 27
        return [ $new, $par ];
132
    }
133
    /**
134
     * Run a query (prepare & execute).
135
     * @param string      $sql  SQL query
136
     * @param array|null  $par parameters (optional)
137
     * @return ResultInterface the result of the execution
138
     */
139 166
    public function query(string $sql, $par = null) : ResultInterface
140
    {
141 166
        $par = isset($par) ? (is_array($par) ? $par : [$par]) : [];
142 166
        if (strpos($sql, '??') && count($par)) {
143 27
            list($sql, $par) = $this->expand($sql, $par);
144
        }
145 166
        return $this->driver->prepare($sql)->execute($par);
146
    }
147
    /**
148
     * Run a SELECT query and get an array-like result.
149
     * When using `get` the data is kept in the database client and fetched as needed (not in PHP memory as with `all`)
150
     *
151
     * @param string   $sql      SQL query
152
     * @param array    $par      parameters
153
     * @param string   $key      column name to use as the array index
154
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
155
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
156
     *
157
     * @return Collection the result of the execution
158
     */
159 160
    public function get(string $sql, $par = null, string $key = null, bool $skip = false, bool $opti = true): Collection
160
    {
161 160
        $coll = Collection::from($this->query($sql, $par));
162 160
        if (($keys = $this->driver->option('mode')) && in_array($keys, ['strtoupper', 'strtolower'])) {
163 1
            $coll->map(function ($v) use ($keys) {
164 1
                $new = [];
165 1
                foreach ($v as $k => $vv) {
166 1
                    $new[call_user_func($keys, $k)] = $vv;
167
                }
168 1
                return $new;
169 1
            });
170
        }
171 160
        if ($key !== null) {
172
            $coll->mapKey(function ($v) use ($key) { return $v[$key]; });
173
        }
174 160
        if ($skip) {
175
            $coll->map(function ($v) use ($key) { unset($v[$key]); return $v; });
176
        }
177 160
        if ($opti) {
178
            $coll->map(function ($v) { return count($v) === 1 ? current($v) : $v; });
179
        }
180 160
        return $coll;
181
    }
182
    /**
183
     * Run a SELECT query and get a single row
184
     * @param string   $sql      SQL query
185
     * @param array    $par      parameters
186
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
187
     * @return mixed the result of the execution
188
     */
189 63
    public function one(string $sql, $par = null, bool $opti = true)
190
    {
191 63
        return $this->get($sql, $par, null, false, $opti)->value();
192
    }
193
    /**
194
     * Run a SELECT query and get an array
195
     * @param string   $sql      SQL query
196
     * @param array    $par      parameters
197
     * @param string   $key      column name to use as the array index
198
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
199
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
200
     * @return Collection the result of the execution
201
     */
202 6
    public function all(string $sql, $par = null, string $key = null, bool $skip = false, bool $opti = true) : array
203
    {
204 6
        return $this->get($sql, $par, $key, $skip, $opti)->toArray();
205
    }
206
    /**
207
     * Begin a transaction.
208
     * @return $this
209
     */
210 1
    public function begin() : DBInterface
211
    {
212 1
        if (!$this->driver->begin()) {
213
            throw new DBException('Could not begin');
214
        }
215 1
        return $this;
216
    }
217
    /**
218
     * Commit a transaction.
219
     * @return $this
220
     */
221 1
    public function commit() : DBInterface
222
    {
223 1
        if (!$this->driver->commit()) {
224
            throw new DBException('Could not commit');
225
        }
226 1
        return $this;
227
    }
228
    /**
229
     * Rollback a transaction.
230
     * @return $this
231
     */
232 1
    public function rollback() : DBInterface
233
    {
234 1
        if (!$this->driver->rollback()) {
235
            throw new DBException('Could not rollback');
236
        }
237 1
        return $this;
238
    }
239
    /**
240
     * Get the current driver name (`"mysql"`, `"postgre"`, etc).
241
     * @return string the current driver name
242
     */
243 16
    public function driverName() : string
244
    {
245 16
        return array_reverse(explode('\\', get_class($this->driver)))[1];
246
    }
247
    /**
248
     * Get an option from the driver
249
     *
250
     * @param string $key     the option name
251
     * @param mixed  $default the default value to return if the option key is not defined
252
     * @return mixed the option value
253
     */
254 76
    public function driverOption(string $key, $default = null)
255
    {
256 76
        return $this->driver->option($key, $default);
257
    }
258
259 156
    public function definition(string $table, bool $detectRelations = true) : Table
260
    {
261 156
        return isset($this->tables[$table]) ?
262
            $this->tables[$table] :
263 156
            $this->driver->table($table, $detectRelations);
264
    }
265
    /**
266
     * Parse all tables from the database.
267
     * @return $this
268
     */
269
    public function parseSchema()
270
    {
271
        $this->tables = $this->driver->tables();
272
        return $this;
273
    }
274
    /**
275
     * Get the full schema as an array that you can serialize and store
276
     * @return array
277
     */
278
    public function getSchema($asPlainArray = true)
279
    {
280 4
        return !$asPlainArray ? $this->tables : array_map(function ($table) {
281
            return [
282
                'name' => $table->getName(),
283
                'pkey' => $table->getPrimaryKey(),
284
                'comment' => $table->getComment(),
285
                'columns' => array_map(function ($column) {
286
                    return [
287
                        'name' => $column->getName(),
288
                        'type' => $column->getType(),
289
                        'length' => $column->getLength(),
290
                        'comment' => $column->getComment(),
291
                        'values' => $column->getValues(),
292
                        'default' => $column->getDefault(),
293
                        'nullable' => $column->isNullable()
294
                    ];
295
                }, $table->getFullColumns()),
296
                'relations' => array_map(function ($rel) {
297
                    $relation = clone $rel;
298
                    $relation->table = $relation->table->getName();
299
                    if ($relation->pivot) {
300
                        $relation->pivot = $relation->pivot->getName();
301
                    }
302
                    return (array)$relation;
303
                }, $table->getRelations())
304
            ];
305 4
        }, $this->tables);
306
    }
307
    /**
308
     * Load the schema data from a schema definition array (obtained from getSchema)
309
     * @param  array        $data the schema definition
310
     * @return $this
311
     */
312
    public function setSchema(array $data)
313
    {
314
        foreach ($data as $tableData) {
315
            $this->tables[$tableData['name']] = (new Table($tableData['name']))
316
                        ->setPrimaryKey($tableData['pkey'])
317
                        ->setComment($tableData['comment'])
318
                        ->addColumns($tableData['columns']);
319
        }
320
        foreach ($data as $tableData) {
321
            $table = $this->definition($tableData['name']);
322
            foreach ($tableData['relations'] as $relationName => $relationData) {
323
                $relationData['table'] = $this->definition($relationData['table']);
324
                if ($relationData['pivot']) {
325
                    $relationData['pivot'] = $this->definition($relationData['pivot']);
326
                }
327
                $table->addRelation(new TableRelation(
328
                    $relationData['name'],
329
                    $relationData['table'],
330
                    $relationData['keymap'],
331
                    $relationData['many'],
332
                    $relationData['pivot'] ?? null,
333
                    $relationData['pivot_keymap'],
334
                    $relationData['sql'],
335
                    $relationData['par']
336
                ));
337
            }
338
        }
339
        return $this;
340
    }
341
342
    /**
343
     * Initialize a table query
344
     * @param string $table the table to query
345
     * @return TableQuery
346
     */
347 156
    public function table($table, bool $mapped = false)
348
    {
349 156
        return $mapped ?
350 80
            new TableQueryMapped($this, $this->definition($table)) :
351 156
            new TableQuery($this, $this->definition($table));
352
    }
353 148
    public function __call($method, $args)
354
    {
355 148
        return $this->table($method, $args[0] ?? false);
356
    }
357
}