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