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

Driver   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 43
c 0
b 0
f 0
lcom 1
cbo 6
dl 0
loc 257
rs 8.3157

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 4
A __destruct() 0 4 1
C connect() 0 22 7
A disconnect() 0 6 2
A prepare() 0 9 2
A begin() 0 5 1
A commit() 0 5 1
A rollback() 0 5 1
D table() 0 160 22
A tables() 0 20 2

How to fix   Complexity   

Complex Class

Complex classes like Driver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Driver, and based on these observations, apply Extract Interface, too.

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 $connection;
16
    protected $lnk = null;
17
18
    public function __construct(array $connection)
19
    {
20
        $this->connection = $connection;
21
        if (!isset($this->connection['port'])) {
22
            $this->connection['port'] = 3306;
23
        }
24
        if (!isset($this->connection['opts'])) {
25
            $this->connection['opts'] = [];
26
        }
27
        if (!isset($this->connection['opts']['charset'])) {
28
            $this->connection['opts']['charset'] = 'UTF8';
29
        }
30
    }
31
    public function __destruct()
32
    {
33
        $this->disconnect();
34
    }
35
    protected function connect()
36
    {
37
        if ($this->lnk === null) {
38
            $this->lnk = new \mysqli(
39
                (isset($this->connection['opts']['persist']) && $this->connection['opts']['persist'] ? 'p:' : '') .
40
                    $this->connection['host'],
41
                $this->connection['user'],
42
                $this->connection['pass'],
43
                $this->connection['name'],
44
                $this->connection['port']
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'])."'");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
54
            }
55
        }
56
    }
57
    protected function disconnect()
58
    {
59
        if ($this->lnk !== null) {
60
            @$this->lnk->close();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

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