Completed
Push — master ( a6d599...c90248 )
by Ivan
04:03
created

DB   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 64.23%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 51
c 1
b 0
f 1
lcom 1
cbo 7
dl 0
loc 321
ccs 88
cts 137
cp 0.6423
rs 8.3206

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 2
B getDriver() 0 45 6
A prepare() 0 4 1
B expand() 0 25 6
B query() 0 8 5
D get() 0 26 9
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 driver() 0 4 1
A definition() 0 6 2
A parseSchema() 0 5 1
B setSchema() 0 29 5
A table() 0 4 1
A __call() 0 4 1
B getSchema() 0 28 3

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\TableRelation;
9
10
/**
11
 * A database abstraction with support for various drivers (mySQL, postgre, oracle, msSQL, sphinx, and even PDO).
12
 */
13
class DB implements DBInterface
14
{
15
    /**
16
     * @var DriverInterface
17
     */
18
    protected $driver;
19
    /**
20
     * @var Table[]
21
     */
22
    protected $tables = [];
23
24
    /**
25
     * Create an instance.
26
     *
27
     * @param DriverInterface|string $driver a driver instance or a connection string
28
     */
29 3
    public function __construct($driver) {
30 3
        $this->driver = $driver instanceof DriverInterface ? $driver : static::getDriver($driver);
31 3
    }
32
    /**
33
     * Create a driver instance from a connection string
34
     * @param string $connectionString the connection string
35
     * @return DriverInterface
36
     */
37 3
    public static function getDriver(string $connectionString)
38
    {
39
        $connection = [
40 3
            'orig' => $connectionString,
41
            'type' => null,
42
            'user' => null,
43
            'pass' => null,
44
            'host' => null,
45
            'port' => null,
46
            'name' => null,
47
            'opts' => []
48
        ];
49
        $aliases = [
50 3
            'mysqli' => 'mysql',
51
            'pg' => 'postgre',
52
            'oci' => 'oracle',
53
            'firebird' => 'ibase'
54
        ];
55 3
        $connectionString = array_pad(explode('://', $connectionString, 2), 2, '');
56 3
        $connection['type'] = $connectionString[0];
57 3
        $connectionString = $connectionString[1];
58 3
        if (strpos($connectionString, '@') !== false) {
59 3
            $connectionString = array_pad(explode('@', $connectionString, 2), 2, '');
60 3
            list($connection['user'], $connection['pass']) = array_pad(explode(':', $connectionString[0], 2), 2, '');
61 3
            $connectionString = $connectionString[1];
62
        }
63 3
        $connectionString = array_pad(explode('/', $connectionString, 2), 2, '');
64 3
        list($connection['host'], $connection['port']) = array_pad(explode(':', $connectionString[0], 2), 2, null);
65 3
        $connectionString = $connectionString[1];
66 3
        if ($pos = strrpos($connectionString, '?')) {
67 3
            $opt = substr($connectionString, $pos + 1);
68 3
            parse_str($opt, $connection['opts']);
69 3
            if ($connection['opts'] && count($connection['opts'])) {
70 3
                $connectionString = substr($connectionString, 0, $pos);
71
            } else {
72
                $connection['opts'] = [];
73
            }
74
        }
75 3
        $connection['name'] = $connectionString;
76 3
        $connection['type'] = isset($aliases[$connection['type']]) ?
77
            $aliases[$connection['type']] :
78 3
            $connection['type'];
79 3
        $tmp = '\\vakata\\database\\driver\\'.strtolower($connection['type']).'\\Driver';
80 3
        return new $tmp($connection);
81
    }
82
    /**
83
     * Prepare a statement.
84
     * Use only if you need a single query to be performed multiple times with different parameters.
85
     *
86
     * @param string $sql the query to prepare - use `?` for arguments
87
     * @return StatementInterface the prepared statement
88
     */
89 1
    public function prepare(string $sql) : StatementInterface
90
    {
91 1
        return $this->driver->prepare($sql);
92
    }
93 8
    protected function expand(string $sql, $par = null) : array
94
    {
95 8
        $new = '';
96 8
        $par = array_values($par);
97 8
        if (substr_count($sql, '?') === 2 && !is_array($par[0])) {
98 3
            $par = [ $par ];
99
        }
100 8
        $parts = explode('??', $sql);
101 8
        $index = 0;
102 8
        foreach ($parts as $part) {
103 8
            $tmp = explode('?', $part);
104 8
            $new .= $part;
105 8
            $index += count($tmp) - 1;
106 8
            if (isset($par[$index])) {
107 8
                if (!is_array($par[$index])) {
108
                    $par[$index] = [ $par[$index] ];
109
                }
110 8
                $params = $par[$index];
111 8
                array_splice($par, $index, 1, $params);
112 8
                $index += count($params);
113 8
                $new .= implode(',', array_fill(0, count($params), '?'));
114
            }
115
        }
116 8
        return [ $new, $par ];
117
    }
118
    /**
119
     * Run a query (prepare & execute).
120
     * @param string $sql  SQL query
121
     * @param array  $data parameters (optional)
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
122
     * @return ResultInterface the result of the execution
123
     */
124 26
    public function query(string $sql, $par = null) : ResultInterface
125
    {
126 26
        $par = isset($par) ? (is_array($par) ? $par : [$par]) : [];
127 26
        if (strpos($sql, '??') && count($par)) {
128 8
            list($sql, $par) = $this->expand($sql, $par);
129
        }
130 26
        return $this->driver->prepare($sql)->execute($par);
131
    }
132
    /**
133
     * Run a SELECT query and get an array-like result.
134
     * When using `get` the data is kept in the database client and fetched as needed (not in PHP memory as with `all`)
135
     *
136
     * @param string   $sql      SQL query
137
     * @param array    $par      parameters
138
     * @param string   $key      column name to use as the array index
139
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
140
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
141
     *
142
     * @return Collection the result of the execution
143
     */
144 20
    public function get(string $sql, $par = null, string $key = null, bool $skip = false, bool $opti = true) : Collection
145
    {
146 20
        $coll = Collection::from($this->query($sql, $par));
147 20
        if (($keys = $this->driver->option('mode')) && in_array($keys, ['strtoupper', 'strtolower'])) {
148
            $coll->map(function ($v) use ($keys) {
149 1
                $new = [];
150 1
                foreach ($v as $k => $vv) {
151 1
                    $new[call_user_func($keys, $k)] = $vv;
152
                }
153 1
                return $new;
154 1
            });
155
        }
156 20
        if ($key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
157
            $coll->mapKey(function ($v) use ($key) { return $v[$key]; });
158
        }
159 20
        if ($skip) {
160
            $coll->map(function ($v) use ($key) { unset($v[$key]); return $v; });
161
        }
162 20
        if ($opti) {
163
            $coll->map(function ($v) { return count($v) === 1 ? current($v) : $v; });
164
        }
165 20
        if ($keys) {
166
            $coll->map(function ($v) use ($key) { unset($v[$key]); return $v; });
167
        }
168 20
        return $coll;
169
    }
170
    /**
171
     * Run a SELECT query and get a single row
172
     * @param string   $sql      SQL query
173
     * @param array    $par      parameters
174
     * @param callable $keys     an optional mutator to pass each row's keys through (the column names)
0 ignored issues
show
Bug introduced by
There is no parameter named $keys. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
175
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
176
     * @return Collection the result of the execution
177
     */
178 7
    public function one(string $sql, $par = null, bool $opti = true)
179
    {
180 7
        return $this->get($sql, $par, null, false, $opti)->value();
181
    }
182
    /**
183
     * Run a SELECT query and get an array
184
     * @param string   $sql      SQL query
185
     * @param array    $par      parameters
186
     * @param string   $key      column name to use as the array index
187
     * @param bool     $skip     do not include the column used as index in the value (defaults to `false`)
188
     * @param callable $keys     an optional mutator to pass each row's keys through (the column names)
0 ignored issues
show
Bug introduced by
There is no parameter named $keys. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
189
     * @param bool     $opti     if a single column is returned - do not use an array wrapper (defaults to `true`)
190
     * @return Collection the result of the execution
191
     */
192 2
    public function all(string $sql, $par = null, string $key = null, bool $skip = false, bool $opti = true) : array
193
    {
194 2
        return $this->get($sql, $par, $key, $skip, $opti)->toArray();
195
    }
196
    /**
197
     * Begin a transaction.
198
     * @return $this
199
     */
200 1
    public function begin() : DBInterface
201
    {
202 1
        if (!$this->driver->begin()) {
203
            throw new DBException('Could not begin');
204
        }
205 1
        return $this;
206
    }
207
    /**
208
     * Commit a transaction.
209
     * @return $this
210
     */
211 1
    public function commit() : DBInterface
212
    {
213 1
        if (!$this->driver->commit()) {
214
            throw new DBException('Could not commit');
215
        }
216 1
        return $this;
217
    }
218
    /**
219
     * Rollback a transaction.
220
     * @return $this
221
     */
222 1
    public function rollback() : DBInterface
223
    {
224 1
        if (!$this->driver->rollback()) {
225
            throw new DBException('Could not rollback');
226
        }
227 1
        return $this;
228
    }
229
    /**
230
     * Get the current driver name (`"mysql"`, `"postgre"`, etc).
231
     * @return string the current driver name
232
     */
233 2
    public function driver() : string
234
    {
235 2
        return array_reverse(explode('\\', get_class($this->driver)))[1];
236
    }
237
238 12
    public function definition(string $table, bool $detectRelations = true) : Table
239
    {
240 12
        return isset($this->tables[$table]) ?
241
            $this->tables[$table] :
242 12
            $this->driver->table($table, $detectRelations);
243
    }
244
    /**
245
     * Parse all tables from the database.
246
     * @return $this
247
     */
248
    public function parseSchema()
249
    {
250
        $this->tables = $this->driver->tables();
251
        return $this;
252
    }
253
    /**
254
     * Get the full schema as an array that you can serialize and store
255
     * @return array
256
     */
257 1
    public function getSchema($asPlainArray = true)
258
    {
259
        return !$asPlainArray ? $this->tables : array_map(function ($table) {
260
            return [
261
                'name' => $table->getName(),
262
                'pkey' => $table->getPrimaryKey(),
263
                'comment' => $table->getComment(),
264
                'columns' => array_map(function ($column) {
265
                    return [
266
                        'name' => $column->getName(),
267
                        'type' => $column->getType(),
268
                        'comment' => $column->getComment(),
269
                        'values' => $column->getValues(),
270
                        'default' => $column->getDefault(),
271
                        'nullable' => $column->isNullable()
272
                    ];
273
                }, $table->getFullColumns()),
274
                'relations' => array_map(function ($rel) {
275
                    $relation = clone $rel;
276
                    $relation->table = $relation->table->getName();
277
                    if ($relation->pivot) {
278
                        $relation->pivot = $relation->pivot->getName();
279
                    }
280
                    return (array)$relation;
281
                }, $table->getRelations())
282
            ];
283 1
        }, $this->tables);
284
    }
285
    /**
286
     * Load the schema data from a schema definition array (obtained from getSchema)
287
     * @param  array        $data the schema definition
288
     * @return $this
289
     */
290
    public function setSchema(array $data)
291
    {
292
        foreach ($data as $tableData) {
293
            $this->tables[$tableData['name']] = (new Table($tableData['name']))
294
                        ->setPrimaryKey($tableData['pkey'])
295
                        ->setComment($tableData['comment'])
296
                        ->addColumns($tableData['columns']);
297
        }
298
        foreach ($data as $tableData) {
299
            $table = $this->definition($tableData['name']);
300
            foreach ($tableData['relations'] as $relationName => $relationData) {
301
                $relationData['table'] = $this->definition($relationData['table']);
302
                if ($relationData['pivot']) {
303
                    $relationData['pivot'] = $this->definition($relationData['pivot']);
304
                }
305
                $table->addRelation(new TableRelation(
306
                    $relationData['name'],
1 ignored issue
show
Documentation introduced by
$relationData['name'] is of type object<vakata\database\schema\Table>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
307
                    $relationData['table'],
308
                    $relationData['keymap'],
1 ignored issue
show
Documentation introduced by
$relationData['keymap'] is of type object<vakata\database\schema\Table>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
309
                    $relationData['many'],
1 ignored issue
show
Documentation introduced by
$relationData['many'] is of type object<vakata\database\schema\Table>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
310
                    $relationData['pivot'] ?? null,
311
                    $relationData['pivot_keymap'],
1 ignored issue
show
Documentation introduced by
$relationData['pivot_keymap'] is of type object<vakata\database\schema\Table>, but the function expects a null|array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
312
                    $relationData['sql'],
1 ignored issue
show
Documentation introduced by
$relationData['sql'] is of type object<vakata\database\schema\Table>, but the function expects a null|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
313
                    $relationData['par']
1 ignored issue
show
Documentation introduced by
$relationData['par'] is of type object<vakata\database\schema\Table>, but the function expects a null|array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
314
                ));
315
            }
316
        }
317
        return $this;
318
    }
319
320
    /**
321
     * Initialize a table query
322
     * @param string $table the table to query
323
     * @return TableQuery
324
     */
325 12
    public function table($table)
326
    {
327 12
        return new TableQuery($this, $this->definition($table));
328
    }
329 12
    public function __call($method, $args)
330
    {
331 12
        return $this->table($method);
332
    }
333
}