Completed
Push — master ( 0ff8e8...37635b )
by Ivan
03:55
created

DB::driverOption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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