Completed
Push — master ( 6e1dd7...0ff8e8 )
by Ivan
02:34
created

Driver::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace vakata\database\driver\mysql;
4
5
use \vakata\database\DBException;
6
use \vakata\database\DriverInterface;
7
use \vakata\database\DriverAbstract;
8
use \vakata\database\StatementInterface;
9
use \vakata\database\schema\Table;
10
use \vakata\database\schema\TableRelation;
11
use \vakata\collection\Collection;
12
13
class Driver extends DriverAbstract implements DriverInterface
14
{
15
    protected $lnk = null;
16
17
    public function __construct(array $connection)
18
    {
19
        $this->connection = $connection;
20
        if (!isset($this->connection['port'])) {
21
            $this->connection['port'] = 3306;
22
        }
23
        if (!isset($this->connection['opts'])) {
24
            $this->connection['opts'] = [];
25
        }
26
        if (!isset($this->connection['opts']['charset'])) {
27
            $this->connection['opts']['charset'] = 'UTF8';
28
        }
29
    }
30
    public function __destruct()
31
    {
32
        $this->disconnect();
33
    }
34
    protected function connect()
35
    {
36
        if ($this->lnk === null) {
37
            $this->lnk = new \mysqli(
38
                (isset($this->connection['opts']['persist']) && $this->connection['opts']['persist'] ? 'p:' : '') .
39
                    $this->connection['host'],
40
                $this->connection['user'],
41
                $this->connection['pass'],
42
                $this->connection['name'],
43
                isset($this->connection['opts']['socket']) ? null : $this->connection['port'],
44
                $this->connection['opts']['socket'] ?? null
45
            );
46
            if ($this->lnk->connect_errno) {
47
                throw new DBException('Connect error: '.$this->lnk->connect_errno);
48
            }
49
            if (!$this->lnk->set_charset($this->connection['opts']['charset'])) {
50
                throw new DBException('Charset error: '.$this->lnk->connect_errno);
51
            }
52
            if (isset($this->connection['opts']['timezone'])) {
53
                $this->lnk->query("SET time_zone = '".addslashes($this->connection['opts']['timezone'])."'");
54
            }
55
        }
56
    }
57
    public function test() : bool
58
    {
59
        if ($this->lnk) {
60
            return true;
61
        }
62
        try {
63
            @$this->connect();
64
            return true;
65
        } catch (\Exception $e) {
66
            $this->lnk = null;
67
            return false;
68
        }
69
    }
70
    protected function disconnect()
71
    {
72
        if ($this->lnk !== null && $this->lnk !== false) {
73
            $this->lnk->close();
74
        }
75
    }
76
    public function prepare(string $sql) : StatementInterface
77
    {
78
        $this->connect();
79
        $temp = $this->lnk->prepare($sql);
80
        if (!$temp) {
81
            throw new DBException('Could not prepare : '.$this->lnk->error.' <'.$sql.'>');
82
        }
83
        return new Statement($temp);
84
    }
85
86
    public function begin() : bool
87
    {
88
        $this->connect();
89
        return $this->lnk->begin_transaction();
90
    }
91
    public function commit() : bool
92
    {
93
        $this->connect();
94
        return $this->lnk->commit();
95
    }
96
    public function rollback() : bool
97
    {
98
        $this->connect();
99
        return $this->lnk->rollback();
100
    }
101
102
    public function table(string $table, bool $detectRelations = true) : Table
103
    {
104
        static $tables = [];
105
        if (isset($tables[$table])) {
106
            return $tables[$table];
107
        }
108
109
        static $comments = null;
110
        if (!isset($comments)) {
111
            $comments = Collection::from(
112
                $this->query(
113
                    "SELECT TABLE_NAME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?",
114
                    [ $this->connection['name'] ]
115
                )
116
            )
117
            ->mapKey(function ($v) {
118
                return $v['TABLE_NAME'];
119
            })
120
            ->pluck('TABLE_COMMENT')
121
            ->toArray();
122
        }
123
124
        static $relationsT = null;
125
        static $relationsR = null;
126
        if (!isset($relationsT) || !isset($relationsR)) {
127
            $relationsT = [];
128
            $relationsR = [];
129
            $col = Collection::from(
130
                $this->query(
131
                    "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
132
                     FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
133
                     WHERE
134
                        TABLE_SCHEMA = ? AND TABLE_NAME IS NOT NULL AND
135
                        REFERENCED_TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL",
136
                    [ $this->connection['name'], $this->connection['name'] ]
137
                )
138
            )->toArray();
139
            foreach ($col as $row) {
140
                $relationsT[$row['TABLE_NAME']][] = $row;
141
                $relationsR[$row['REFERENCED_TABLE_NAME']][] = $row;
142
            }
143
        }
144
145
        
146
        $columns = Collection::from($this->query("SHOW FULL COLUMNS FROM {$table}"));
147
        if (!count($columns)) {
148
            throw new DBException('Table not found by name');
149
        }
150
        $tables[$table] = $definition = (new Table($table))
151
            ->addColumns(
152
                $columns
153
                    ->clone()
154
                    ->mapKey(function ($v) { return $v['Field']; })
155
                    ->map(function ($v) {
156
                        $v['length'] = null;
157
                        if (!isset($v['Type'])) {
158
                            return $v;
159
                        }
160
                        $type = strtolower($v['Type']);
161
                        switch ($type) {
162
                            case 'tinytext':
163
                                $v['length'] = 255;
164
                                break;
165
                            case 'text':
166
                                $v['length'] = 65535;
167
                                break;
168
                            case 'mediumtext':
169
                                $v['length'] = 16777215;
170
                                break;
171
                            case 'longtext':
172
                                // treat this as no limit
173
                                break;
174
                            default:
175
                                if (strpos($type, 'char') !== false && strpos($type, '(') !== false) {
176
                                    // extract length from varchar
177
                                    $v['length'] = (int)explode(')', explode('(', $type)[1])[0];
178
                                    $v['length'] = $v['length'] > 0 ? $v['length'] : null;
179
                                }
180
                                break;
181
                        }
182
                        return $v;
183
                    })
184
                    ->toArray()
185
            )
186
            ->setPrimaryKey(
187
                $columns
188
                    ->clone()
189
                    ->filter(function ($v) { return $v['Key'] === 'PRI'; })
190
                    ->pluck('Field')
191
                    ->toArray()
192
            )
193
            ->setComment($comments[$table] ?? '');
194
195
        if ($detectRelations) {
196
            // relations where the current table is referenced
197
            // assuming current table is on the "one" end having "many" records in the referencing table
198
            // resulting in a "hasMany" or "manyToMany" relationship (if a pivot table is detected)
199
            $relations = [];
200
            foreach ($relationsR[$table] ?? [] as $relation) {
201
                $relations[$relation['CONSTRAINT_NAME']]['table'] = $relation['TABLE_NAME'];
202
                $relations[$relation['CONSTRAINT_NAME']]['keymap'][$relation['REFERENCED_COLUMN_NAME']] = $relation['COLUMN_NAME'];
203
            }
204
            foreach ($relations as $data) {
205
                $rtable = $this->table($data['table'], true);
206
                $columns = [];
207
                foreach ($rtable->getColumns() as $column) {
208
                    if (!in_array($column, $data['keymap'])) {
209
                        $columns[] = $column;
210
                    }
211
                }
212
                $foreign = [];
213
                $usedcol = [];
214
                if (count($columns)) {
215
                    foreach (Collection::from($relationsT[$data['table']] ?? [])
216
                        ->filter(function ($v) use ($columns) {
217
                            return in_array($v['COLUMN_NAME'], $columns);
218
                        })
219
                        ->map(function ($v) {
220
                            $new = [];
221
                            foreach ($v as $kk => $vv) {
222
                                $new[strtoupper($kk)] = $vv;
223
                            }
224
                            return $new;
225
                        }) as $relation
226
                    ) {
227
                        $foreign[$relation['CONSTRAINT_NAME']]['table'] = $relation['REFERENCED_TABLE_NAME'];
228
                        $foreign[$relation['CONSTRAINT_NAME']]['keymap'][$relation['COLUMN_NAME']] = $relation['REFERENCED_COLUMN_NAME'];
229
                        $usedcol[] = $relation['COLUMN_NAME'];
230
                    }
231
                }
232
                if (count($foreign) === 1 && !count(array_diff($columns, $usedcol))) {
233
                    $foreign = current($foreign);
234
                    $relname = $foreign['table'];
235
                    $cntr = 1;
236
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
237
                        $relname = $foreign['table'] . '_' . (++ $cntr);
238
                    }
239
                    $definition->addRelation(
240
                        new TableRelation(
241
                            $relname,
242
                            $this->table($foreign['table'], true),
243
                            $data['keymap'],
244
                            true,
245
                            $rtable,
246
                            $foreign['keymap']
247
                        )
248
                    );
249
                } else {
250
                    $relname = $data['table'];
251
                    $cntr = 1;
252
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
253
                        $relname = $data['table'] . '_' . (++ $cntr);
254
                    }
255
                    $definition->addRelation(
256
                        new TableRelation(
257
                            $relname,
258
                            $this->table($data['table'], true),
259
                            $data['keymap'],
260
                            true
261
                        )
262
                    );
263
                }
264
            }
265
            // relations where the current table references another table
266
            // assuming current table is linked to "one" record in the referenced table
267
            // resulting in a "belongsTo" relationship
268
            $relations = [];
269
            foreach (Collection::from($relationsT[$table] ?? [])
270
                ->map(function ($v) {
271
                    $new = [];
272
                    foreach ($v as $kk => $vv) {
273
                        $new[strtoupper($kk)] = $vv;
274
                    }
275
                    return $new;
276
                }) as $relation
277
            ) {
278
                $relations[$relation['CONSTRAINT_NAME']]['table'] = $relation['REFERENCED_TABLE_NAME'];
279
                $relations[$relation['CONSTRAINT_NAME']]['keymap'][$relation['COLUMN_NAME']] = $relation['REFERENCED_COLUMN_NAME'];
280
            }
281
            foreach ($relations as $name => $data) {
282
                $relname = $data['table'];
283
                $cntr = 1;
284
                while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
285
                    $relname = $data['table'] . '_' . (++ $cntr);
286
                }
287
                $definition->addRelation(
288
                    new TableRelation(
289
                        $relname,
290
                        $this->table($data['table'], true),
291
                        $data['keymap'],
292
                        false
293
                    )
294
                );
295
            }
296
        }
297
        return $definition->toLowerCase();
298
    }
299
    public function tables() : array
300
    {
301
        return Collection::from($this
302
            ->query(
303
                "SELECT table_name FROM information_schema.tables where table_schema = ?",
304
                [$this->connection['name']]
305
            ))
306
            ->map(function ($v) {
307
                $new = [];
308
                foreach ($v as $kk => $vv) {
309
                    $new[strtoupper($kk)] = $vv;
310
                }
311
                return $new;
312
            })
313
            ->pluck('TABLE_NAME')
314
            ->map(function ($v) {
315
                return $this->table($v);
316
            })
317
            ->toArray();
318
    }
319
}