Database::list_indexes()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 39
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 18
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 39
ccs 0
cts 34
cp 0
crap 30
rs 9.3554
1
<?php
2
/**
3
 * @name      OpenImporter
4
 * @copyright OpenImporter contributors
5
 * @license   BSD https://opensource.org/licenses/BSD-3-Clause
6
 *
7
 * @version 1.0
8
 */
9
10
namespace OpenImporter;
11
12
/**
13
 * Class Database
14
 *
15
 * This class provides an easy wrapper around the common database
16
 * functions we work with.
17
 *
18
 * @class Database
19
 */
20
class Database
21
{
22
	/** @var \mysqli */
23
	protected $connect;
24
25
	/** @var bool Allows to run a query two times on certain errors. */
26
	protected $second_try = true;
27
28
	/**
29
	 * Database constructor.
30
	 *
31
	 * @param string $db_server
32
	 * @param string $db_user
33
	 * @param string $db_password
34
	 * @param bool|int $db_persist
35
	 */
36
	public function __construct($db_server, $db_user, $db_password, $db_persist)
37
	{
38
		$this->connect = mysqli_connect(($db_persist == 1 ? 'p:' : '') . $db_server, $db_user, $db_password);
0 ignored issues
show
Bug introduced by
The call to mysqli_connect() has too few arguments starting with database. ( Ignorable by Annotation )

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

38
		$this->connect = /** @scrutinizer ignore-call */ mysqli_connect(($db_persist == 1 ? 'p:' : '') . $db_server, $db_user, $db_password);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
39
40
		if (mysqli_connect_error())
41
		{
42
			die('Database error: ' . mysqli_connect_error());
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
43
		}
44
	}
45
46
	/**
47
	 * Execute an SQL query.
48
	 *
49
	 * @param string $string
50
	 * @param bool $return_error
51
	 *
52
	 * @return \mysqli_result
53
	 */
54
	public function query($string, $return_error = false)
55
	{
56
		$result = @mysqli_query($this->connect, $string);
57
58
		if ($result !== false || $return_error)
59
		{
60
			$this->second_try = true;
61
62
			return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type boolean which is incompatible with the documented return type mysqli_result.
Loading history...
63
		}
64
65
		return $this->sendError($string);
66
	}
67
68
	/**
69
	 * Returns the last MySQL error occurred with the current connection.
70
	 *
71
	 * @return string
72
	 */
73
	public function getLastError()
74
	{
75
		return mysqli_error($this->connect);
76
	}
77
78
	/**
79
	 * Analyze and sends an error.
80
	 *
81
	 * @param string $string
82
	 *
83
	 * @throws DatabaseException If a SQL fails
84
	 *
85
	 * @return \mysqli_result
86
	 */
87
	protected function sendError($string)
88
	{
89
		$mysql_error = mysqli_error($this->connect);
90
		$mysql_errno = mysqli_errno($this->connect);
91
92
		// 1016: Can't open file '....MYI'
93
		// 2013: Lost connection to server during query.
94
		if ($this->second_try && in_array($mysql_errno, array(1016, 2013)))
95
		{
96
			$this->second_try = false;
97
98
			// Try to repair the table and run the query again.
99
			if ($mysql_errno === 1016 && preg_match('~(?:\'([^.\']+)~', $mysql_error, $match) !== 0 && !empty($match[1]))
100
			{
101
				mysqli_query($this->connect, "
102
					REPAIR TABLE $match[1]");
103
			}
104
105
			return $this->query($string, false);
106
		}
107
108
		$action_url = $this->buildActionUrl();
109
110
		throw new DatabaseException('
111
			<b>Unsuccessful!</b><br />
112
			This query:<blockquote>' . nl2br(htmlspecialchars(trim($string))) . ';</blockquote>
113
			Caused the error:<br />
114
			<blockquote>' . nl2br(htmlspecialchars($mysql_error)) . '</blockquote>
115
			<form action="' . $action_url . '" method="post">
116
				<input type="submit" value="Try again" />
117
			</form>
118
			</div>');
119
	}
120
121
	/**
122
	 * Puts together the url used in the DatabaseException of sendError to go
123
	 * back to the last step.
124
	 *
125
	 * @return string
126
	 */
127
	protected function buildActionUrl()
128
	{
129
		// @todo $_GET and $_REQUEST
130
		// Get the query string so we pass everything.
131
		if (isset($_REQUEST['start']))
132
		{
133
			$_GET['start'] = $_REQUEST['start'];
134
		}
135
136
		$query_string = '';
137
		foreach ($_GET as $k => $v)
138
		{
139
			$query_string .= '&' . $k . '=' . $v;
140
		}
141
142
		if (trim($query_string) !== '')
143
		{
144
			$query_string = '?' . strtr(substr($query_string, 1), array('&' => '&amp;'));
145
		}
146
147
		return $_SERVER['PHP_SELF'] . $query_string;
148
	}
149
150
	/**
151
	 * Wrapper for mysqli_free_result.
152
	 *
153
	 * @param \mysqli_result $result
154
	 */
155
	public function free_result($result)
156
	{
157
		mysqli_free_result($result);
158
	}
159
160
	/**
161
	 * Wrapper for mysqli_fetch_assoc.
162
	 *
163
	 * @param \mysqli_result $result
164
	 *
165
	 * @return array
166
	 */
167
	public function fetch_assoc($result)
168
	{
169
		return mysqli_fetch_assoc($result);
170
	}
171
172
	/**
173
	 * wrapper for mysqli_fetch_row
174
	 *
175
	 * @param \mysqli_result $result
176
	 *
177
	 * @return array
178
	 */
179
	public function fetch_row($result)
180
	{
181
		return mysqli_fetch_row($result);
182
	}
183
184
	/**
185
	 * wrapper for mysqli_num_rows
186
	 *
187
	 * @param \mysqli_result $result
188
	 *
189
	 * @return integer
190
	 */
191
	public function num_rows($result)
192
	{
193
		return mysqli_num_rows($result);
0 ignored issues
show
Bug Best Practice introduced by
The expression return mysqli_num_rows($result) also could return the type string which is incompatible with the documented return type integer.
Loading history...
194
	}
195
196
	/**
197
	 * wrapper for mysqli_insert_id
198
	 *
199
	 * @return integer
200
	 */
201
	public function insert_id()
202
	{
203
		return mysqli_insert_id($this->connect);
0 ignored issues
show
Bug Best Practice introduced by
The expression return mysqli_insert_id($this->connect) also could return the type string which is incompatible with the documented return type integer.
Loading history...
204
	}
205
206
	/**
207
	 * Add an index.
208
	 *
209
	 * @param string $table_name
210
	 * @param array $index_info
211
	 *
212
	 * @return bool
213
	 */
214
	public function add_index($table_name, $index_info)
215
	{
216
		// No columns = no index.
217
		if (empty($index_info['columns']))
218
		{
219
			return false;
220
		}
221
222
		$columns = implode(',', $index_info['columns']);
223
224
		$index_info['name'] = $this->calculateIndexName($index_info);
225
226
		if ($this->indexExists($table_name, $index_info))
0 ignored issues
show
Bug introduced by
$index_info of type array is incompatible with the type string expected by parameter $index_info of OpenImporter\Database::indexExists(). ( Ignorable by Annotation )

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

226
		if ($this->indexExists($table_name, /** @scrutinizer ignore-type */ $index_info))
Loading history...
227
		{
228
			return false;
229
		}
230
231
		// If we're here we know we don't have the index - so just add it.
232
		if (!empty($index_info['type']) && $index_info['type'] === 'primary')
233
		{
234
			$this->query('
235
				ALTER TABLE ' . $table_name . '
236
				ADD PRIMARY KEY (' . $columns . ')');
237
		}
238
		else
239
		{
240
			if (!isset($index_info['type']) || !in_array($index_info['type'], array('unique', 'index', 'key')))
241
			{
242
				$type = 'INDEX';
243
			}
244
			else
245
			{
246
				$type = strtoupper($index_info['type']);
247
			}
248
249
			$this->query('
250
				ALTER TABLE ' . $table_name . '
251
				ADD ' . $type . ' ' . $index_info['name'] . ' (' . $columns . ')');
252
		}
253
	}
254
255
	/**
256
	 * Set a name for the index
257
	 *
258
	 * @param $index_info
259
	 *
260
	 * @return string
261
	 */
262
	protected function calculateIndexName($index_info)
263
	{
264
		// No name - make it up!
265
		if (empty($index_info['name']))
266
		{
267
			// No need for primary.
268
			if (isset($index_info['type']) && $index_info['type'] === 'primary')
269
			{
270
				return '';
271
			}
272
273
			return implode('_', $index_info['columns']);
274
		}
275
276
		return $index_info['name'];
277
	}
278
279
	/**
280
	 * Check if an index exists or not
281
	 *
282
	 * @param string $table_name
283
	 * @param string $index_info
284
	 *
285
	 * @return bool
286
	 */
287
	protected function indexExists($table_name, $index_info)
288
	{
289
		// Let's get all our indexes.
290
		$indexes = $this->list_indexes($table_name, true);
291
292
		// Do we already have it?
293
		foreach ($indexes as $index)
294
		{
295
			if ($index['name'] === $index_info['name']
296
				|| ($index['type'] === 'primary' && isset($index_info['type']) && $index_info['type'] === 'primary'))
297
			{
298
				return true;
299
			}
300
		}
301
302
		return false;
303
	}
304
305
	/**
306
	 * Get index information.
307
	 *
308
	 * @param string $table_name
309
	 * @param bool $detail
310
	 *
311
	 * @return array
312
	 */
313
	public function list_indexes($table_name, $detail = false)
314
	{
315
		$result = $this->query("
316
			SHOW KEYS
317
			FROM {$table_name}");
318
319
		$indexes = array();
320
		while ($row = $this->fetch_assoc($result))
321
		{
322
			if ($detail)
323
			{
324
				// This is the first column we've seen?
325
				if (empty($indexes[$row['Key_name']]))
326
				{
327
					$indexes[$row['Key_name']] = array(
328
						'name' => $row['Key_name'],
329
						'type' => $this->determineIndexType($row),
330
						'columns' => array(),
331
					);
332
				}
333
334
				// Is it a partial index?
335
				if (!empty($row['Sub_part']))
336
				{
337
					$indexes[$row['Key_name']]['columns'][] = $row['Column_name'] . '(' . $row['Sub_part'] . ')';
338
				}
339
				else
340
				{
341
					$indexes[$row['Key_name']]['columns'][] = $row['Column_name'];
342
				}
343
			}
344
			else
345
			{
346
				$indexes[] = $row['Key_name'];
347
			}
348
		}
349
		$this->free_result($result);
350
351
		return $indexes;
352
	}
353
354
	/**
355
	 * What is the index type?
356
	 *
357
	 * @param string[] $row
358
	 *
359
	 * @return string
360
	 */
361
	protected function determineIndexType($row)
362
	{
363
		if ($row['Key_name'] === 'PRIMARY')
364
		{
365
			return 'primary';
366
		}
367
368
		if (empty($row['Non_unique']))
369
		{
370
			return 'unique';
371
		}
372
373
		if (isset($row['Index_type']) && $row['Index_type'] === 'FULLTEXT')
374
		{
375
			return 'fulltext';
376
		}
377
378
		return 'index';
379
	}
380
}
381