Completed
Push — master ( b71ad6...2211ef )
by Ivan
08:51
created

DB::parseSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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