Passed
Push — master ( 7e0467...a278bb )
by Marcio
02:41
created

MySQLDump::write()   B

Complexity

Conditions 8
Paths 13

Size

Total Lines 45
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 30
dl 0
loc 45
rs 8.1954
c 1
b 0
f 0
cc 8
nc 13
nop 1
1
<?php
2
3
namespace Ballybran\Database;
4
5
use mysqli;
6
7
/**
8
 * MySQL database dump.
9
 *
10
 * @version    1.0
11
 */
12
class MySQLDump
13
{
14
    const MAX_SQL_SIZE = 1e6;
15
16
    const NONE = 0;
17
    const DROP = 1;
18
    const CREATE = 2;
19
    const DATA = 4;
20
    const TRIGGERS = 8;
21
    const ALL = 15; // DROP | CREATE | DATA | TRIGGERS
22
23
    private $delTable;
24
    /** @var array */
25
    public $tables = [
26
        '*' => self::ALL,
27
    ];
28
29
    /** @var mysqli */
30
    private $connection;
31
32
33
    /**
34
     * Connects to database.
35
     * @param  mysqli connection
0 ignored issues
show
Bug introduced by
The type Ballybran\Database\connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
     */
37
    public function __construct(mysqli $connection, $charset = 'utf8')
38
    {
39
        $this->connection = $connection;
40
41
        if ($connection->connect_errno) {
42
            throw new \Exception($connection->connect_error);
43
44
        } elseif (!$connection->set_charset($charset)) { // was added in MySQL 5.0.7 and PHP 5.0.5, fixed in PHP 5.1.5)
45
            throw new \Exception($connection->error);
46
        }
47
    }
48
49
50
    /**
51
     * Saves dump to the file.
52
     * @param  string filename
0 ignored issues
show
Bug introduced by
The type Ballybran\Database\filename was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
53
     * @return void
54
     */
55
    public function save($file)
56
    {
57
        $handle = strcasecmp(substr($file, -3), '.gz') ? fopen($file, 'wb') : gzopen($file, 'wb');
58
        if (!$handle) {
0 ignored issues
show
introduced by
$handle is of type false|resource, thus it always evaluated to false.
Loading history...
59
            throw new \Exception("ERROR: Cannot write file '$file'.");
60
        }
61
        $this->write($handle);
62
    }
63
64
65
    /**
66
     * Writes dump to logical file.
67
     * @param  resource
68
     * @return void
69
     */
70
    public function write($handle = null)
71
    {
72
        if ($handle === null) {
73
            $handle = fopen('php://output', 'wb');
74
        } elseif (!is_resource($handle) || get_resource_type($handle) !== 'stream') {
75
            throw new \Exception('Argument must be stream resource.');
76
        }
77
78
        $tables = $views = [];
79
80
        $res = $this->connection->query('SHOW FULL TABLES');
81
        while ($row = $res->fetch_row()) {
82
            if ($row[1] === 'VIEW') {
83
                $views[] = $row[0];
84
            } else {
85
                $tables[] = $row[0];
86
            }
87
        }
88
        $res->close();
89
90
        $tables = array_merge($tables, $views); // views must be last
91
92
        $this->connection->query('LOCK TABLES `' . implode('` READ, `', $tables) . '` READ');
93
94
        $db = $this->connection->query('SELECT DATABASE()')->fetch_row();
95
        fwrite($handle, '-- Created at ' . date('j.n.Y G:i') . " using David Grudl MySQL Dump Utility\n"
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

95
        fwrite(/** @scrutinizer ignore-type */ $handle, '-- Created at ' . date('j.n.Y G:i') . " using David Grudl MySQL Dump Utility\n"
Loading history...
96
            . (isset($_SERVER['HTTP_HOST']) ? "-- Host: $_SERVER[HTTP_HOST]\n" : '')
97
            . '-- MySQL Server: ' . $this->connection->server_info . "\n"
98
            . '-- Database: ' . $db[0] . "\n"
99
            . "\n"
100
            . "SET NAMES utf8;\n"
101
            . "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n"
102
            . "SET FOREIGN_KEY_CHECKS=0;\n"
103
            . "SET UNIQUE_CHECKS=0;\n"
104
            . "SET AUTOCOMMIT=0;\n"
105
        );
106
107
        foreach ($tables as $table) {
108
            $this->dumpTable($handle, $table);
109
        }
110
111
        fwrite($handle, "COMMIT;\n");
112
        fwrite($handle, "-- THE END\n");
113
114
        $this->connection->query('UNLOCK TABLES');
115
    }
116
117
118
    /**
119
     * Dumps table to logical file.
120
     * @param  resource
121
     * @return void
122
     */
123
    public function dumpTable($handle, $table)
124
    {
125
        $this->delTable = $this->delimite($table);
126
        $res = $this->connection->query("SHOW CREATE TABLE $this->delTable");
127
        $row = $res->fetch_assoc();
128
        $res->close();
129
130
        fwrite($handle, "-- --------------------------------------------------------\n\n");
131
132
        $mode = isset($this->tables[$table]) ? $this->tables[$table] : $this->tables['*'];
133
        $view = isset($row['Create View']);
134
135
        if ($mode & self::DROP) {
136
            fwrite($handle, 'DROP ' . ($view ? 'VIEW' : 'TABLE') . " IF EXISTS $this->delTable;\n\n");
137
        }
138
139
        if ($mode & self::CREATE) {
140
            fwrite($handle, $row[$view ? 'Create View' : 'Create Table'] . ";\n\n");
141
        }
142
143
        if (!$view && ($mode & self::DATA)) {
144
            fwrite($handle, 'ALTER ' . ($view ? 'VIEW' : 'TABLE') . ' ' . $this->delTable . " DISABLE KEYS;\n\n");
0 ignored issues
show
introduced by
The condition $view is always false.
Loading history...
145
            $numeric = [];
146
            $res = $this->connection->query("SHOW COLUMNS FROM $this->delTable");
147
            $cols = [];
148
            while ($row = $res->fetch_assoc()) {
149
                $col = $row['Field'];
150
                $cols[] = $this->delimite($col);
151
                $numeric[$col] = (bool)preg_match('#^[^(]*(BYTE|COUNTER|SERIAL|INT|LONG$|CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)#i', $row['Type']);
152
            }
153
            $cols = '(' . implode(', ', $cols) . ')';
154
            $res->close();
155
156
            $this->mysqliUseResult($mode, $table, $numeric, $cols, $view, $handle);
157
        }
158
    }
159
160
        private function mysqliUseResult($mode, $table, $numeric, $cols, $view, $handle){
161
            $size = 0;
162
            $res = $this->connection->query("SELECT * FROM $this->delTable", MYSQLI_USE_RESULT);
163
            while ($row = $res->fetch_assoc()) {
164
                $s = '(';
165
                foreach ($row as $key => $value) {
166
                    if ($value === null) {
167
                        $s .= "NULL,\t";
168
                    } elseif ($numeric[$key]) {
169
                        $s .= $value . ",\t";
170
                    } else {
171
                        $s .= "'" . $this->connection->real_escape_string($value) . "',\t";
172
                    }
173
                }
174
175
                if ($size == 0) {
176
                    $s = "INSERT INTO $this->delTable $cols VALUES\n$s";
177
                } else {
178
                    $s = ",\n$s";
179
                }
180
181
                $len = strlen($s) - 1;
182
                $s[$len - 1] = ')';
183
                fwrite($handle, $s, $len);
184
185
                $size += $len;
186
                if ($size > self::MAX_SQL_SIZE) {
187
                    fwrite($handle, ";\n");
188
                    $size = 0;
189
                }
190
            }
191
192
            $res->close();
193
            if ($size) {
194
                fwrite($handle, ";\n");
195
            }
196
            fwrite($handle, 'ALTER ' . ($view ? 'VIEW' : 'TABLE') . ' ' . $this->delTable . " ENABLE KEYS;\n\n");
197
            fwrite($handle, "\n");
198
        
199
200
201
            $this->trigger($mode, $handle, $table);
202
        
203
        fwrite($handle, "\n");
204
    
205
    }
206
207
    private function trigger($mode, $handle, $table){
208
        if ($mode & self::TRIGGERS) {
209
            $res = $this->connection->query("SHOW TRIGGERS LIKE '" . $this->connection->real_escape_string($table) . "'");
210
            if ($res->num_rows) {
211
                fwrite($handle, "DELIMITER ;;\n\n");
212
                while ($row = $res->fetch_assoc()) {
213
                    fwrite($handle, "CREATE TRIGGER {$this->delimite($row['Trigger'])} $row[Timing] $row[Event] ON $this->delTable FOR EACH ROW\n$row[Statement];;\n\n");
214
                }
215
                fwrite($handle, "DELIMITER ;\n\n");
216
            }
217
            $res->close();
218
        }
219
220
    }
221
222
223
    private function delimite($s)
224
    {
225
        return '`' . str_replace('`', '``', $s) . '`';
226
    }
227
}
228