Test Failed
Push — prado-4.0 ( 8053ac...dcc89f )
by Fabio
08:56 queued 03:57
created

TMysqlMetaData::getIsView()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 13
nop 2
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
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
	protected function getTableInfoClass()
40
	{
41
		return '\Prado\Data\Common\Mysql\TMysqlTableInfo';
42
	}
43
44
	/**
45
	 * @return string TDbTableColumn class name.
46
	 */
47
	protected function getTableColumnClass()
48
	{
49
		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
	protected function createTableInfo($table)
88
	{
89
		list($schemaName, $tableName) = $this->getSchemaTableName($table);
90
		$find = $schemaName === null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
91
		$colCase = $this->getDbConnection()->getColumnCase();
92
		if ($colCase != TDbColumnCaseMode::Preserved) {
93
			$this->getDbConnection()->setColumnCase('Preserved');
94
		}
95
		$this->getDbConnection()->setActive(true);
96
		$sql = "SHOW FULL FIELDS FROM {$find}";
97
		$command = $this->getDbConnection()->createCommand($sql);
98
		$tableInfo = $this->createNewTableInfo($table);
99
		$index = 0;
100
		foreach ($command->query() as $col) {
101
			$col['index'] = $index++;
102
			$this->processColumn($tableInfo, $col);
103
		}
104
		if ($index === 0) {
105
			throw new TDbException('dbmetadata_invalid_table_view', $table);
106
		}
107
		if ($colCase != TDbColumnCaseMode::Preserved) {
108
			$this->getDbConnection()->setColumnCase($colCase);
109
		}
110
		return $tableInfo;
111
	}
112
113
	/**
114
	 * @return float server version.
115
	 */
116
	protected function getServerVersion()
117
	{
118
		if (!$this->_serverVersion) {
119
			$version = $this->getDbConnection()->getAttribute(PDO::ATTR_SERVER_VERSION);
120
			$digits = [];
121
			preg_match('/(\d+)\.(\d+)\.(\d+)/', $version, $digits);
122
			$this->_serverVersion = (float) ($digits[1] . '.' . $digits[2] . $digits[3]);
0 ignored issues
show
Documentation Bug introduced by
The property $_serverVersion was declared of type integer, but (double) ($digits[1] . '...digits[2] . $digits[3]) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
123
		}
124
		return $this->_serverVersion;
125
	}
126
127
	/**
128
	 * @param TMysqlTableInfo $tableInfo table information.
129
	 * @param array $col column information.
130
	 */
131
	protected function processColumn($tableInfo, $col)
132
	{
133
		$columnId = $col['Field'];
134
135
		$info['ColumnName'] = "`$columnId`"; //quote the column names!
0 ignored issues
show
Coding Style Comprehensibility introduced by
$info was never initialized. Although not strictly required by PHP, it is generally a good practice to add $info = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
136
		$info['ColumnId'] = $columnId;
137
		$info['ColumnIndex'] = $col['index'];
138
		if ($col['Null'] === 'YES') {
139
			$info['AllowNull'] = true;
140
		}
141
		if (is_int(strpos(strtolower($col['Extra']), 'auto_increment'))) {
142
			$info['AutoIncrement'] = true;
143
		}
144
		if ($col['Default'] !== "") {
145
			$info['DefaultValue'] = $col['Default'];
146
		}
147
148
		if ($col['Key'] === 'PRI' || in_array($columnId, $tableInfo->getPrimaryKeys())) {
149
			$info['IsPrimaryKey'] = true;
150
		}
151
		if ($this->isForeignKeyColumn($columnId, $tableInfo)) {
152
			$info['IsForeignKey'] = true;
153
		}
154
155
		$info['DbType'] = $col['Type'];
156
		$match = [];
157
		//find SET/ENUM values, column size, precision, and scale
158
		if (preg_match('/\((.*)\)/', $col['Type'], $match)) {
159
			$info['DbType'] = preg_replace('/\(.*\)/', '', $col['Type']);
160
161
			//find SET/ENUM values
162
			if ($this->isEnumSetType($info['DbType'])) {
163
				$info['DbTypeValues'] = preg_split("/[',]/S", $match[1], -1, PREG_SPLIT_NO_EMPTY);
164
			}
165
166
			//find column size, precision and scale
167
			$pscale = [];
168
			if (preg_match('/(\d+)(?:,(\d+))?+/', $match[1], $pscale)) {
169
				if ($this->isPrecisionType($info['DbType'])) {
170
					$info['NumericPrecision'] = (int) ($pscale[1]);
171
					if (count($pscale) > 2) {
172
						$info['NumericScale'] = (int) ($pscale[2]);
173
					}
174
				} else {
175
					$info['ColumnSize'] = (int) ($pscale[1]);
176
				}
177
			}
178
		}
179
180
		$class = $this->getTableColumnClass();
181
		$tableInfo->Columns[$columnId] = new $class($info);
0 ignored issues
show
Bug introduced by
The property Columns does not seem to exist. Did you mean _columns?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
182
	}
183
184
	/**
185
	 * @param mixed $type
186
	 * @return bool true if column type if "numeric", "interval" or begins with "time".
187
	 */
188
	protected function isPrecisionType($type)
189
	{
190
		$type = strtolower(trim($type));
191
		return $type === 'decimal' || $type === 'dec'
192
				|| $type === 'float' || $type === 'double'
193
				|| $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
	protected function isEnumSetType($type)
201
	{
202
		$type = strtolower(trim($type));
203
		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
	protected function getSchemaTableName($table)
212
	{
213
		//remove the back ticks and separate out the "database.table"
214
		$result = explode('.', str_replace('`', '', $table));
215
		foreach ($result as $name) {
216
			if (!$this->isValidIdentifier($name)) {
217
				$ref = 'http://dev.mysql.com/doc/refman/5.0/en/identifiers.html';
218
				throw new TDbException('dbcommon_invalid_identifier_name', $table, $ref);
219
			}
220
		}
221
		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
	protected function isValidIdentifier($name)
230
	{
231
		return !preg_match('#/|\\|.|\x00|\xFF#', $name);
232
	}
233
234
	/**
235
	 * @param string $table table schema name
236
	 * @return TMysqlTableInfo
237
	 */
238
	protected function createNewTableInfo($table)
239
	{
240
		list($schemaName, $tableName) = $this->getSchemaTableName($table);
241
		$info['SchemaName'] = $schemaName;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$info was never initialized. Although not strictly required by PHP, it is generally a good practice to add $info = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
242
		$info['TableName'] = $tableName;
243
		if ($this->getIsView($schemaName, $tableName)) {
244
			$info['IsView'] = true;
245
		}
246
		list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
247
		$class = $this->getTableInfoClass();
248
		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
	protected function getIsView($schemaName, $tableName)
262
	{
263
		if ($this->getServerVersion() < 5.01) {
264
			return false;
265
		}
266
		if ($schemaName !== null) {
267
			$sql = "SHOW FULL TABLES FROM `{$schemaName}` LIKE '{$tableName}'";
268
		} else {
269
			$sql = "SHOW FULL TABLES LIKE '{$tableName}'";
270
		}
271
272
		$command = $this->getDbConnection()->createCommand($sql);
273
		try {
274
			return count($result = $command->queryRow()) > 0 && $result['Table_type'] === 'VIEW';
275
		} catch (TDbException $e) {
276
			$table = $schemaName === null ? $tableName : $schemaName . '.' . $tableName;
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
	protected function getConstraintKeys($schemaName, $tableName)
288
	{
289
		$table = $schemaName === null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
290
		$sql = "SHOW INDEX FROM {$table}";
291
		$command = $this->getDbConnection()->createCommand($sql);
292
		$primary = [];
293
		foreach ($command->query() as $row) {
294
			if ($row['Key_name'] === 'PRIMARY') {
295
				$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
		if ($this->getServerVersion() >= 5.121) {
301
			$foreign = $this->getForeignConstraints($schemaName, $tableName);
302
		} else {
303
			$foreign = $this->findForeignConstraints($schemaName, $tableName);
304
		}
305
		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
	protected function getForeignConstraints($schemaName, $tableName)
315
	{
316
		$andSchema = $schemaName !== null ? 'AND TABLE_SCHEMA LIKE :schema' : 'AND TABLE_SCHEMA LIKE DATABASE()';
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
				$andSchema
330
EOD;
331
		$command = $this->getDbConnection()->createCommand($sql);
332
		$command->bindValue(':table', $tableName);
333
		if ($schemaName !== null) {
334
			$command->bindValue(':schema', $schemaName);
335
		}
336
		$fkeys = [];
337 View Code Duplication
		foreach ($command->query() as $col) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
			$fkeys[$col['con']]['keys'][$col['col']] = $col['fkcol'];
339
			$fkeys[$col['con']]['table'] = $col['fktable'];
340
		}
341
		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) {
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 isset($result['Create Table']) ? $result['Create Table'] : (isset($result['Create View']) ? $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) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 View Code Duplication
	protected function isForeignKeyColumn($columnId, $tableInfo)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399
	{
400
		foreach ($tableInfo->getForeignKeys() as $fk) {
401
			if (in_array($columnId, array_keys($fk['keys']))) {
402
				return true;
403
			}
404
		}
405
		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