Completed
Push — master ( e4d56c...8e58c0 )
by Ivan
02:47
created

DB::getDriver()   F

Complexity

Conditions 20
Paths > 20000

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 20.2634

Importance

Changes 0
Metric Value
dl 0
loc 53
ccs 21
cts 23
cp 0.913
rs 0
c 0
b 0
f 0
cc 20
nc 32769
nop 1
crap 20.2634

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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