Completed
Branch master (099915)
by Fabio
08:02
created

TMysqlMetaData::processColumn()   C

Complexity

Conditions 12
Paths 288

Size

Total Lines 51
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 12.0053

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 30
c 2
b 0
f 0
nc 288
nop 2
dl 0
loc 51
ccs 29
cts 30
cp 0.9667
crap 12.0053
rs 5.0333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * TMysqlMetaData class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 * @package Prado\Data\Common\Mysql
9
 */
10
11
namespace Prado\Data\Common\Mysql;
12
13
/**
14
 * Load the base TDbMetaData class.
15
 */
16
use PDO;
17
use Prado\Data\Common\TDbMetaData;
18
use Prado\Data\TDbColumnCaseMode;
19
use Prado\Exceptions\TDbException;
20
use Prado\Prado;
21
22
/**
23
 * TMysqlMetaData loads Mysql version 4.1.x and 5.x database table and column information.
24
 *
25
 * For Mysql version 4.1.x, PHP 5.1.3 or later is required.
26
 * See http://netevil.org/node.php?nid=795&SC=1
27
 *
28
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
29
 * @package Prado\Data\Common\Mysql
30
 * @since 3.1
31
 */
32
class TMysqlMetaData extends TDbMetaData
33
{
34
	private $_serverVersion = 0;
35
36
	/**
37
	 * @return string TDbTableInfo class name.
38
	 */
39 11
	protected function getTableInfoClass()
40
	{
41 11
		return '\Prado\Data\Common\Mysql\TMysqlTableInfo';
42
	}
43
44
	/**
45
	 * @return string TDbTableColumn class name.
46
	 */
47 11
	protected function getTableColumnClass()
48
	{
49 11
		return '\Prado\Data\Common\Mysql\TMysqlTableColumn';
50
	}
51
52
	/**
53
	 * Quotes a table name for use in a query.
54
	 * @param string $name $name table name
55
	 * @return string the properly quoted table name
56
	 */
57
	public function quoteTableName($name)
58
	{
59
		return parent::quoteTableName($name, '`', '`');
60
	}
61
62
	/**
63
	 * Quotes a column name for use in a query.
64
	 * @param string $name $name column name
65
	 * @return string the properly quoted column name
66
	 */
67
	public function quoteColumnName($name)
68
	{
69
		return parent::quoteColumnName($name, '`', '`');
70
	}
71
72
	/**
73
	 * Quotes a column alias for use in a query.
74
	 * @param string $name $name column alias
75
	 * @return string the properly quoted column alias
76
	 */
77
	public function quoteColumnAlias($name)
78
	{
79
		return parent::quoteColumnAlias($name, '`', '`');
80
	}
81
82
	/**
83
	 * Get the column definitions for given table.
84
	 * @param string $table table name.
85
	 * @return TMysqlTableInfo table information.
86
	 */
87 11
	protected function createTableInfo($table)
88
	{
89 11
		[$schemaName, $tableName] = $this->getSchemaTableName($table);
90 11
		$find = $schemaName === null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
91 11
		$colCase = $this->getDbConnection()->getColumnCase();
92 11
		if ($colCase != TDbColumnCaseMode::Preserved) {
93
			$this->getDbConnection()->setColumnCase('Preserved');
94
		}
95 11
		$this->getDbConnection()->setActive(true);
96 11
		$sql = "SHOW FULL FIELDS FROM {$find}";
97 11
		$command = $this->getDbConnection()->createCommand($sql);
98 11
		$tableInfo = $this->createNewTableInfo($table);
99 11
		$index = 0;
100 11
		foreach ($command->query() as $col) {
101 11
			$col['index'] = $index++;
102 11
			$this->processColumn($tableInfo, $col);
103
		}
104 11
		if ($index === 0) {
105
			throw new TDbException('dbmetadata_invalid_table_view', $table);
106
		}
107 11
		if ($colCase != TDbColumnCaseMode::Preserved) {
108
			$this->getDbConnection()->setColumnCase($colCase);
109
		}
110 11
		return $tableInfo;
111
	}
112
113
	/**
114
	 * @return float server version.
115
	 */
116 11
	protected function getServerVersion()
117
	{
118 11
		if (!$this->_serverVersion) {
119 8
			$version = $this->getDbConnection()->getAttribute(PDO::ATTR_SERVER_VERSION);
120 8
			$digits = [];
121 8
			preg_match('/(\d+)\.(\d+)\.(\d+)/', $version, $digits);
122 8
			$this->_serverVersion = (float) ($digits[1] . '.' . $digits[2] . $digits[3]);
123
		}
124 11
		return $this->_serverVersion;
125
	}
126
127
	/**
128
	 * @param TMysqlTableInfo $tableInfo table information.
129
	 * @param array $col column information.
130
	 */
131 11
	protected function processColumn($tableInfo, $col)
132
	{
133 11
		$columnId = $col['Field'];
134
135 11
		$info['ColumnName'] = "`$columnId`"; //quote the column names!
0 ignored issues
show
Comprehensibility Best Practice introduced by
$info was never initialized. Although not strictly required by PHP, it is generally a good practice to add $info = array(); before regardless.
Loading history...
136 11
		$info['ColumnId'] = $columnId;
137 11
		$info['ColumnIndex'] = $col['index'];
138 11
		if ($col['Null'] === 'YES') {
139 9
			$info['AllowNull'] = true;
140
		}
141 11
		if (is_int(strpos(strtolower($col['Extra']), 'auto_increment'))) {
0 ignored issues
show
introduced by
The condition is_int(strpos(strtolower...']), 'auto_increment')) is always true.
Loading history...
142 5
			$info['AutoIncrement'] = true;
143
		}
144 11
		if ($col['Default'] !== "") {
145 11
			$info['DefaultValue'] = $col['Default'];
146
		}
147
148 11
		if ($col['Key'] === 'PRI' || in_array($columnId, $tableInfo->getPrimaryKeys())) {
149 11
			$info['IsPrimaryKey'] = true;
150
		}
151 11
		if ($this->isForeignKeyColumn($columnId, $tableInfo)) {
152
			$info['IsForeignKey'] = true;
153
		}
154
155 11
		$info['DbType'] = $col['Type'];
156 11
		$match = [];
157
		//find SET/ENUM values, column size, precision, and scale
158 11
		if (preg_match('/\((.*)\)/', $col['Type'], $match)) {
159 11
			$info['DbType'] = preg_replace('/\(.*\)/', '', $col['Type']);
160
161
			//find SET/ENUM values
162 11
			if ($this->isEnumSetType($info['DbType'])) {
163 1
				$info['DbTypeValues'] = preg_split("/[',]/S", $match[1], -1, PREG_SPLIT_NO_EMPTY);
164
			}
165
166
			//find column size, precision and scale
167 11
			$pscale = [];
168 11
			if (preg_match('/(\d+)(?:,(\d+))?+/', $match[1], $pscale)) {
169 11
				if ($this->isPrecisionType($info['DbType'])) {
170 7
					$info['NumericPrecision'] = (int) ($pscale[1]);
171 7
					if (count($pscale) > 2) {
172 7
						$info['NumericScale'] = (int) ($pscale[2]);
173
					}
174
				} else {
175 11
					$info['ColumnSize'] = (int) ($pscale[1]);
176
				}
177
			}
178
		}
179
180 11
		$class = $this->getTableColumnClass();
181 11
		$tableInfo->Columns[$columnId] = new $class($info);
0 ignored issues
show
Bug Best Practice introduced by
The property Columns does not exist on Prado\Data\Common\Mysql\TMysqlTableInfo. Since you implemented __get, consider adding a @property annotation.
Loading history...
182 11
	}
183
184
	/**
185
	 * @param mixed $type
186
	 * @return bool true if column type if "numeric", "interval" or begins with "time".
187
	 */
188 11
	protected function isPrecisionType($type)
189
	{
190 11
		$type = strtolower(trim($type));
191 11
		return $type === 'decimal' || $type === 'dec'
192 11
				|| $type === 'float' || $type === 'double'
193 11
				|| $type === 'double precision' || $type === 'real';
194
	}
195
196
	/**
197
	 * @param mixed $type
198
	 * @return bool true if column type if "enum" or "set".
199
	 */
200 11
	protected function isEnumSetType($type)
201
	{
202 11
		$type = strtolower(trim($type));
203 11
		return $type === 'set' || $type === 'enum';
204
	}
205
206
	/**
207
	 * @param string $table table name, may be quoted with back-ticks and may contain database name.
208
	 * @throws TDbException when table name contains invalid identifier bytes.
209
	 * @return array tuple ($schema,$table), $schema may be null.
210
	 */
211 11
	protected function getSchemaTableName($table)
212
	{
213
		//remove the back ticks and separate out the "database.table"
214 11
		$result = explode('.', str_replace('`', '', $table));
215 11
		foreach ($result as $name) {
216 11
			if (!$this->isValidIdentifier($name)) {
217
				$ref = 'http://dev.mysql.com/doc/refman/5.0/en/identifiers.html';
218 11
				throw new TDbException('dbcommon_invalid_identifier_name', $table, $ref);
219
			}
220
		}
221 11
		return count($result) > 1 ? $result : [null, $result[0]];
222
	}
223
224
	/**
225
	 * http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
226
	 * @param string $name identifier name
227
	 * @return bool true if valid identifier.
228
	 */
229 11
	protected function isValidIdentifier($name)
230
	{
231 11
		return !preg_match('#/|\\|.|\x00|\xFF#', $name);
232
	}
233
234
	/**
235
	 * @param string $table table schema name
236
	 * @return TMysqlTableInfo
237
	 */
238 11
	protected function createNewTableInfo($table)
239
	{
240 11
		[$schemaName, $tableName] = $this->getSchemaTableName($table);
241 11
		$info['SchemaName'] = $schemaName;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$info was never initialized. Although not strictly required by PHP, it is generally a good practice to add $info = array(); before regardless.
Loading history...
242 11
		$info['TableName'] = $tableName;
243 11
		if ($this->getIsView($schemaName, $tableName)) {
244
			$info['IsView'] = true;
245
		}
246 11
		[$primary, $foreign] = $this->getConstraintKeys($schemaName, $tableName);
247 11
		$class = $this->getTableInfoClass();
248 11
		return new $class($info, $primary, $foreign);
249
	}
250
251
	/**
252
	 * For MySQL version 5.0.1 or later we can use SHOW FULL TABLES
253
	 * http://dev.mysql.com/doc/refman/5.0/en/show-tables.html
254
	 *
255
	 * For MySQL version 5.0.1 or earlier, this always return false.
256
	 * @param string $schemaName database name, null to use default connection database.
257
	 * @param string $tableName table or view name.
258
	 * @throws TDbException if table or view does not exist.
259
	 * @return bool true if is view, false otherwise.
260
	 */
261 11
	protected function getIsView($schemaName, $tableName)
262
	{
263 11
		if ($this->getServerVersion() < 5.01) {
264
			return false;
265
		}
266 11
		if ($schemaName !== null) {
0 ignored issues
show
introduced by
The condition $schemaName !== null is always true.
Loading history...
267
			$sql = "SHOW FULL TABLES FROM `{$schemaName}` LIKE '{$tableName}'";
268
		} else {
269 11
			$sql = "SHOW FULL TABLES LIKE '{$tableName}'";
270
		}
271
272 11
		$command = $this->getDbConnection()->createCommand($sql);
273
		try {
274 11
			return count($result = $command->queryRow()) > 0 && $result['Table_type'] === 'VIEW';
275
		} catch (TDbException $e) {
276
			$table = $schemaName === null ? $tableName : $schemaName . '.' . $tableName;
0 ignored issues
show
introduced by
The condition $schemaName === null is always false.
Loading history...
277
			throw new TDbException('dbcommon_invalid_table_name', $table, $e->getMessage());
278
		}
279
	}
280
281
	/**
282
	 * Gets the primary and foreign key column details for the given table.
283
	 * @param string $schemaName schema name
284
	 * @param string $tableName table name.
285
	 * @return array tuple ($primary, $foreign)
286
	 */
287 11
	protected function getConstraintKeys($schemaName, $tableName)
288
	{
289 11
		$table = $schemaName === null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
0 ignored issues
show
introduced by
The condition $schemaName === null is always false.
Loading history...
290 11
		$sql = "SHOW INDEX FROM {$table}";
291 11
		$command = $this->getDbConnection()->createCommand($sql);
292 11
		$primary = [];
293 11
		foreach ($command->query() as $row) {
294 11
			if ($row['Key_name'] === 'PRIMARY') {
295 11
				$primary[] = $row['Column_name'];
296
			}
297
		}
298
		// MySQL version was increased to >=5.1.21 instead of 5.x
299
		// due to a MySQL bug (http://bugs.mysql.com/bug.php?id=19588)
300 11
		if ($this->getServerVersion() >= 5.121) {
301 11
			$foreign = $this->getForeignConstraints($schemaName, $tableName);
302
		} else {
303
			$foreign = $this->findForeignConstraints($schemaName, $tableName);
304
		}
305 11
		return [$primary, $foreign];
306
	}
307
308
	/**
309
	 * Gets foreign relationship constraint keys and table name
310
	 * @param string $schemaName database name
311
	 * @param string $tableName table name
312
	 * @return array foreign relationship table name and keys.
313
	 */
314 11
	protected function getForeignConstraints($schemaName, $tableName)
315
	{
316 11
		$andSchema = $schemaName !== null ? 'AND TABLE_SCHEMA LIKE :schema' : 'AND TABLE_SCHEMA LIKE DATABASE()';
0 ignored issues
show
introduced by
The condition $schemaName !== null is always true.
Loading history...
317
		$sql = <<<EOD
318
			SELECT
319
				CONSTRAINT_NAME as con,
320
				COLUMN_NAME as col,
321
				REFERENCED_TABLE_SCHEMA as fkschema,
322
				REFERENCED_TABLE_NAME as fktable,
323
				REFERENCED_COLUMN_NAME as fkcol
324
			FROM
325
				`INFORMATION_SCHEMA`.`KEY_COLUMN_USAGE`
326
			WHERE
327
				REFERENCED_TABLE_NAME IS NOT NULL
328
				AND TABLE_NAME LIKE :table
329 11
				$andSchema
330
EOD;
331 11
		$command = $this->getDbConnection()->createCommand($sql);
332 11
		$command->bindValue(':table', $tableName);
333 11
		if ($schemaName !== null) {
0 ignored issues
show
introduced by
The condition $schemaName !== null is always true.
Loading history...
334
			$command->bindValue(':schema', $schemaName);
335
		}
336 11
		$fkeys = [];
337 11
		foreach ($command->query() as $col) {
338
			$fkeys[$col['con']]['keys'][$col['col']] = $col['fkcol'];
339 11
			$fkeys[$col['con']]['table'] = $col['fktable'];
340
		}
341 11
		return count($fkeys) > 0 ? array_values($fkeys) : $fkeys;
342
	}
343
344
	/**
345
	 * @param string $schemaName database name
346
	 * @param string $tableName table name
347
	 * @throws TDbException if PHP version is less than 5.1.3
348
	 * @return string SQL command to create the table.
349
	 */
350
	protected function getShowCreateTable($schemaName, $tableName)
351
	{
352
		if (version_compare(PHP_VERSION, '5.1.3', '<')) {
353
			throw new TDbException('dbmetadata_requires_php_version', 'Mysql 4.1.x', '5.1.3');
354
		}
355
356
		//See http://netevil.org/node.php?nid=795&SC=1
357
		$this->getDbConnection()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
358
		if ($schemaName !== null) {
0 ignored issues
show
introduced by
The condition $schemaName !== null is always true.
Loading history...
359
			$sql = "SHOW CREATE TABLE `{$schemaName}`.`{$tableName}`";
360
		} else {
361
			$sql = "SHOW CREATE TABLE `{$tableName}`";
362
		}
363
		$command = $this->getDbConnection()->createCommand($sql);
364
		$result = $command->queryRow();
365
		return $result['Create Table'] ?? ($result['Create View'] ?? '');
366
	}
367
368
	/**
369
	 * Extract foreign key constraints by extracting the contraints from SHOW CREATE TABLE result.
370
	 * @param string $schemaName database name
371
	 * @param string $tableName table name
372
	 * @return array foreign relationship table name and keys.
373
	 */
374
	protected function findForeignConstraints($schemaName, $tableName)
375
	{
376
		$sql = $this->getShowCreateTable($schemaName, $tableName);
377
		$matches = [];
378
		$regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+`?([^`]+)`?\s\(([^\)]+)\)/mi';
379
		preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER);
380
		$foreign = [];
381
		foreach ($matches as $match) {
382
			$fields = array_map('trim', explode(',', str_replace('`', '', $match[1])));
383
			$fk_fields = array_map('trim', explode(',', str_replace('`', '', $match[3])));
384
			$keys = [];
385
			foreach ($fields as $k => $v) {
386
				$keys[$v] = $fk_fields[$k];
387
			}
388
			$foreign[] = ['keys' => $keys, 'table' => trim($match[2])];
389
		}
390
		return $foreign;
391
	}
392
393
	/**
394
	 * @param string $columnId column name.
395
	 * @param TMysqlTableInfo $tableInfo table information.
396
	 * @return bool true if column is a foreign key.
397
	 */
398 11
	protected function isForeignKeyColumn($columnId, $tableInfo)
399
	{
400 11
		foreach ($tableInfo->getForeignKeys() as $fk) {
401
			if (in_array($columnId, array_keys($fk['keys']))) {
402
				return true;
403
			}
404
		}
405 11
		return false;
406
	}
407
408
	/**
409
	 * Returns all table names in the database.
410
	 * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
411
	 * If not empty, the returned table names will be prefixed with the schema name.
412
	 * @return array all table names in the database.
413
	 */
414
	public function findTableNames($schema = '')
415
	{
416
		if ($schema === '') {
417
			return $this->getDbConnection()->createCommand('SHOW TABLES')->queryColumn();
418
		}
419
		$names = $this->getDbConnection()->createCommand('SHOW TABLES FROM ' . $this->quoteTableName($schema))->queryColumn();
420
		foreach ($names as &$name) {
421
			$name = $schema . '.' . $name;
422
		}
423
		return $names;
424
	}
425
}
426