Completed
Push — master ( 5924b0...d78793 )
by Ivan
02:02
created

Driver::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
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
                $this->connection['port']
44
            );
45
            if ($this->lnk->connect_errno) {
46
                throw new DBException('Connect error: '.$this->lnk->connect_errno);
47
            }
48
            if (!$this->lnk->set_charset($this->connection['opts']['charset'])) {
49
                throw new DBException('Charset error: '.$this->lnk->connect_errno);
50
            }
51
            if (isset($this->connection['opts']['timezone'])) {
52
                $this->lnk->query("SET time_zone = '".addslashes($this->connection['opts']['timezone'])."'");
53
            }
54
        }
55
    }
56
    public function test() : bool
57
    {
58
        if ($this->lnk) {
59
            return true;
60
        }
61
        try {
62
            $this->connect();
63
            return true;
64
        } catch (\Exception $e) {
65
            return false;
66
        }
67
    }
68
    protected function disconnect()
69
    {
70
        if ($this->lnk !== null) {
71
            $this->lnk->close();
72
        }
73
    }
74
    public function prepare(string $sql) : StatementInterface
75
    {
76
        $this->connect();
77
        $temp = $this->lnk->prepare($sql);
78
        if (!$temp) {
79
            throw new DBException('Could not prepare : '.$this->lnk->error.' <'.$sql.'>');
80
        }
81
        return new Statement($temp);
82
    }
83
84
    public function begin() : bool
85
    {
86
        $this->connect();
87
        return $this->lnk->begin_transaction();
88
    }
89
    public function commit() : bool
90
    {
91
        $this->connect();
92
        return $this->lnk->commit();
93
    }
94
    public function rollback() : bool
95
    {
96
        $this->connect();
97
        return $this->lnk->rollback();
98
    }
99
100
    public function table(string $table, bool $detectRelations = true) : Table
101
    {
102
        static $tables = [];
103
        if (isset($tables[$table])) {
104
            return $tables[$table];
105
        }
106
        
107
        $columns = Collection::from($this->query("SHOW FULL COLUMNS FROM {$table}"));
108
        if (!count($columns)) {
109
            throw new DBException('Table not found by name');
110
        }
111
        $tables[$table] = $definition = (new Table($table))
112
            ->addColumns(
113
                $columns
114
                    ->clone()
115
                    ->mapKey(function ($v) { return $v['Field']; })
116
                    ->toArray()
117
            )
118
            ->setPrimaryKey(
119
                $columns
120
                    ->clone()
121
                    ->filter(function ($v) { return $v['Key'] === 'PRI'; })
122
                    ->pluck('Field')
123
                    ->toArray()
124
            )
125
            ->setComment(
126
                (string)Collection::from($this
127
                    ->query(
128
                        "SELECT table_comment FROM information_schema.tables WHERE table_schema = ? AND table_name = ?",
129
                        [ $this->connection['name'], $table ]
130
                    ))
131
                    ->pluck('table_comment')
132
                    ->value()
133
            );
134
135
        if ($detectRelations) {
136
            // relations where the current table is referenced
137
            // assuming current table is on the "one" end having "many" records in the referencing table
138
            // resulting in a "hasMany" or "manyToMany" relationship (if a pivot table is detected)
139
            $relations = [];
140
            foreach (
141
                $this
142
                    ->query(
143
                        "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_COLUMN_NAME
144
                         FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
145
                         WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME = ?",
146
                        [ $this->connection['name'], $this->connection['name'], $table ]
147
                    ) as $relation
148
            ) {
149
                $relations[$relation['CONSTRAINT_NAME']]['table'] = $relation['TABLE_NAME'];
150
                $relations[$relation['CONSTRAINT_NAME']]['keymap'][$relation['REFERENCED_COLUMN_NAME']] = $relation['COLUMN_NAME'];
151
            }
152
            foreach ($relations as $data) {
153
                $rtable = $this->table($data['table'], true);
154
                $columns = [];
155
                foreach ($rtable->getColumns() as $column) {
156
                    if (!in_array($column, $data['keymap'])) {
157
                        $columns[] = $column;
158
                    }
159
                }
160
                $foreign = [];
161
                $usedcol = [];
162
                if (count($columns)) {
163
                    foreach (Collection::from($this
164
                        ->query(
165
                            "SELECT
166
                                 TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME,
167
                                 REFERENCED_COLUMN_NAME, REFERENCED_TABLE_NAME
168
                             FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
169
                             WHERE
170
                                 TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME IN (??) AND
171
                                 REFERENCED_TABLE_NAME IS NOT NULL",
172
                            [ $this->connection['name'], $data['table'], $columns ]
173
                        ))
174
                        ->map(function ($v) {
175
                            $new = [];
176
                            foreach ($v as $kk => $vv) {
177
                                $new[strtoupper($kk)] = $vv;
178
                            }
179
                            return $new;
180
                        }) as $relation
181
                    ) {
182
                        $foreign[$relation['CONSTRAINT_NAME']]['table'] = $relation['REFERENCED_TABLE_NAME'];
183
                        $foreign[$relation['CONSTRAINT_NAME']]['keymap'][$relation['COLUMN_NAME']] = $relation['REFERENCED_COLUMN_NAME'];
184
                        $usedcol[] = $relation['COLUMN_NAME'];
185
                    }
186
                }
187
                if (count($foreign) === 1 && !count(array_diff($columns, $usedcol))) {
188
                    $foreign = current($foreign);
189
                    $relname = $foreign['table'];
190
                    $cntr = 1;
191
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
192
                        $relname = $foreign['table'] . '_' . (++ $cntr);
193
                    }
194
                    $definition->addRelation(
195
                        new TableRelation(
196
                            $relname,
197
                            $this->table($foreign['table'], true),
198
                            $data['keymap'],
199
                            true,
200
                            $rtable,
201
                            $foreign['keymap']
202
                        )
203
                    );
204
                } else {
205
                    $relname = $data['table'];
206
                    $cntr = 1;
207
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
208
                        $relname = $data['table'] . '_' . (++ $cntr);
209
                    }
210
                    $definition->addRelation(
211
                        new TableRelation(
212
                            $relname,
213
                            $this->table($data['table'], true),
214
                            $data['keymap'],
215
                            true
216
                        )
217
                    );
218
                }
219
            }
220
            // relations where the current table references another table
221
            // assuming current table is linked to "one" record in the referenced table
222
            // resulting in a "belongsTo" relationship
223
            $relations = [];
224
            foreach (Collection::from($this
225
                ->query(
226
                    "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
227
                     FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
228
                     WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL",
229
                    [ $this->connection['name'], $table ]
230
                ))
231
                ->map(function ($v) {
232
                    $new = [];
233
                    foreach ($v as $kk => $vv) {
234
                        $new[strtoupper($kk)] = $vv;
235
                    }
236
                    return $new;
237
                }) as $relation
238
            ) {
239
                $relations[$relation['CONSTRAINT_NAME']]['table'] = $relation['REFERENCED_TABLE_NAME'];
240
                $relations[$relation['CONSTRAINT_NAME']]['keymap'][$relation['COLUMN_NAME']] = $relation['REFERENCED_COLUMN_NAME'];
241
            }
242
            foreach ($relations as $name => $data) {
243
                $relname = $data['table'];
244
                $cntr = 1;
245
                while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
246
                    $relname = $data['table'] . '_' . (++ $cntr);
247
                }
248
                $definition->addRelation(
249
                    new TableRelation(
250
                        $relname,
251
                        $this->table($data['table'], true),
252
                        $data['keymap'],
253
                        false
254
                    )
255
                );
256
            }
257
        }
258
        return $definition->toLowerCase();
259
    }
260
    public function tables() : array
261
    {
262
        return Collection::from($this
263
            ->query(
264
                "SELECT table_name FROM information_schema.tables where table_schema = ?",
265
                [$this->connection['name']]
266
            ))
267
            ->map(function ($v) {
268
                $new = [];
269
                foreach ($v as $kk => $vv) {
270
                    $new[strtoupper($kk)] = $vv;
271
                }
272
                return $new;
273
            })
274
            ->pluck('TABLE_NAME')
275
            ->map(function ($v) {
276
                return $this->table($v);
277
            })
278
            ->toArray();
279
    }
280
}