Generator.php 16.3 KB
Newer Older
Qiang Xue committed
1 2 3 4 5 6 7 8 9
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\gii\generators\model;

Qiang Xue committed
10
use Yii;
Qiang Xue committed
11
use yii\db\ActiveRecord;
Qiang Xue committed
12
use yii\db\Connection;
13
use yii\db\Schema;
Qiang Xue committed
14
use yii\gii\CodeFile;
Qiang Xue committed
15
use yii\helpers\Inflector;
Qiang Xue committed
16

Qiang Xue committed
17
/**
Qiang Xue committed
18
 * This generator will generate one or multiple ActiveRecord classes for the specified database table.
Qiang Xue committed
19 20 21 22 23 24
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Generator extends \yii\gii\Generator
{
Qiang Xue committed
25
	public $db = 'db';
Qiang Xue committed
26
	public $ns = 'app\models';
Qiang Xue committed
27 28
	public $tableName;
	public $modelClass;
Qiang Xue committed
29
	public $baseClass = 'yii\db\ActiveRecord';
Qiang Xue committed
30
	public $generateRelations = true;
Qiang Xue committed
31
	public $generateLabelsFromComments = false;
Qiang Xue committed
32 33


Qiang Xue committed
34 35 36
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
37 38 39 40 41
	public function getName()
	{
		return 'Model Generator';
	}

Qiang Xue committed
42 43 44
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
45 46
	public function getDescription()
	{
Qiang Xue committed
47
		return 'This generator generates an ActiveRecord class for the specified database table.';
Qiang Xue committed
48
	}
49

Qiang Xue committed
50 51 52
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
53 54 55
	public function rules()
	{
		return array_merge(parent::rules(), array(
Qiang Xue committed
56 57 58 59
			array('db, ns, tableName, modelClass, baseClass', 'filter', 'filter' => 'trim'),
			array('db, ns, tableName, baseClass', 'required'),
			array('db, modelClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'),
			array('ns, baseClass', 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'),
Qiang Xue committed
60
			array('tableName', 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'),
Qiang Xue committed
61 62
			array('db', 'validateDb'),
			array('ns', 'validateNamespace'),
Qiang Xue committed
63
			array('tableName', 'validateTableName'),
Qiang Xue committed
64
			array('modelClass', 'validateModelClass'),
Qiang Xue committed
65
			array('baseClass', 'validateClass', 'params' => array('extends' => ActiveRecord::className())),
Qiang Xue committed
66
			array('generateRelations, generateLabelsFromComments', 'boolean'),
Qiang Xue committed
67 68 69
		));
	}

Qiang Xue committed
70 71 72
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
73 74
	public function attributeLabels()
	{
Qiang Xue committed
75
		return array(
Qiang Xue committed
76 77
			'ns' => 'Namespace',
			'db' => 'Database Connection ID',
Qiang Xue committed
78 79 80
			'tableName' => 'Table Name',
			'modelClass' => 'Model Class',
			'baseClass' => 'Base Class',
Qiang Xue committed
81
			'generateRelations' => 'Generate Relations',
Qiang Xue committed
82 83 84 85
			'generateLabelsFromComments' => 'Generate Labels from DB Comments',
		);
	}

Qiang Xue committed
86 87 88
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
	public function hints()
	{
		return array(
			'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>',
			'db' => 'This is the ID of the DB application component.',
			'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. <code>tbl_post</code>.
				The table name may consist of the DB schema part if needed, e.g. <code>public.tbl_post</code>.
				The table name may contain an asterisk at the end to match multiple table names, e.g. <code>tbl_*</code>.
				In this case, multiple ActiveRecord classes will be generated, one for each matching table name.',
			'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
				the namespace part as it is specified in "Namespace". You do not need to specify the class name
				if "Table Name" contains an asterisk at the end, in which case multiple ActiveRecord classes will be generated.',
			'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
			'generateRelations' => 'This indicates whether the generator should generate relations based on
				foreign key constraints it detects in the database. Note that if your database contains too many tables,
Qiang Xue committed
104
				you may want to uncheck this option to accelerate the code generation proc	ess.',
Qiang Xue committed
105 106
			'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels
				by using the comments of the corresponding DB columns.',
Qiang Xue committed
107
		);
Qiang Xue committed
108 109
	}

Qiang Xue committed
110 111 112
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
113 114 115 116 117 118 119
	public function requiredTemplates()
	{
		return array(
			'model.php',
		);
	}

Qiang Xue committed
120 121 122
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
123 124
	public function stickyAttributes()
	{
Qiang Xue committed
125
		return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments');
Qiang Xue committed
126 127
	}

Qiang Xue committed
128
	/**
Qiang Xue committed
129
	 * @inheritdoc
Qiang Xue committed
130
	 */
Qiang Xue committed
131 132
	public function generate()
	{
Qiang Xue committed
133
		$files = array();
Qiang Xue committed
134 135
		$relations = $this->generateRelations();
		$db = $this->getDbConnection();
Qiang Xue committed
136 137
		foreach ($this->getTableNames() as $tableName) {
			$className = $this->generateClassName($tableName);
Qiang Xue committed
138
			$tableSchema = $db->getTableSchema($tableName);
Qiang Xue committed
139
			$params = array(
Qiang Xue committed
140
				'tableName' => $tableName,
Qiang Xue committed
141
				'className' => $className,
Qiang Xue committed
142 143
				'tableSchema' => $tableSchema,
				'labels' => $this->generateLabels($tableSchema),
144
				'rules' => $this->generateRules($tableSchema),
Qiang Xue committed
145
				'relations' => isset($relations[$className]) ? $relations[$className] : array(),
Qiang Xue committed
146
			);
Qiang Xue committed
147
			$files[] = new CodeFile(
Qiang Xue committed
148
				Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
Qiang Xue committed
149
				$this->render('model.php', $params)
Qiang Xue committed
150 151
			);
		}
Qiang Xue committed
152 153

		return $files;
Qiang Xue committed
154 155
	}

Qiang Xue committed
156 157 158 159 160
	/**
	 * Generates the attribute labels for the specified table.
	 * @param \yii\db\TableSchema $table the table schema
	 * @return array the generated attribute labels (name => label)
	 */
Qiang Xue committed
161 162 163 164
	public function generateLabels($table)
	{
		$labels = array();
		foreach ($table->columns as $column) {
Qiang Xue committed
165
			if ($this->generateLabelsFromComments && !empty($column->comment)) {
Qiang Xue committed
166
				$labels[$column->name] = $column->comment;
Qiang Xue committed
167 168
			} elseif (!strcasecmp($column->name, 'id')) {
				$labels[$column->name] = 'ID';
Qiang Xue committed
169
			} else {
Qiang Xue committed
170
				$label = Inflector::camel2words($column->name);
Qiang Xue committed
171
				if (strcasecmp(substr($label, -3), ' id') === 0) {
Qiang Xue committed
172
					$label = substr($label, 0, -3) . ' ID';
Qiang Xue committed
173 174 175 176 177 178 179
				}
				$labels[$column->name] = $label;
			}
		}
		return $labels;
	}

180
	/**
Qiang Xue committed
181 182 183
	 * Generates validation rules for the specified table.
	 * @param \yii\db\TableSchema $table the table schema
	 * @return array the generated validation rules
184
	 */
Qiang Xue committed
185 186
	public function generateRules($table)
	{
187 188
		$types = array();
		$lengths = array();
Qiang Xue committed
189 190 191 192
		foreach ($table->columns as $column) {
			if ($column->autoIncrement) {
				continue;
			}
193 194
			if (!$column->allowNull && $column->defaultValue === null) {
				$types['required'][] = $column->name;
Qiang Xue committed
195
			}
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
			switch ($column->type) {
				case Schema::TYPE_SMALLINT:
				case Schema::TYPE_INTEGER:
				case Schema::TYPE_BIGINT:
					$types['integer'][] = $column->name;
					break;
				case Schema::TYPE_BOOLEAN:
					$types['boolean'][] = $column->name;
					break;
				case Schema::TYPE_FLOAT:
				case Schema::TYPE_DECIMAL:
				case Schema::TYPE_MONEY:
					$types['number'][] = $column->name;
					break;
				case Schema::TYPE_DATE:
				case Schema::TYPE_TIME:
				case Schema::TYPE_DATETIME:
				case Schema::TYPE_TIMESTAMP:
					$types['safe'][] = $column->name;
					break;
				default: // strings
					if ($column->size > 0) {
						$lengths[$column->size][] = $column->name;
					} else {
						$types['string'][] = $column->name;
					}
Qiang Xue committed
222 223
			}
		}
224 225 226 227

		$rules = array();
		foreach ($types as $type => $columns) {
			$rules[] = "array('" . implode(', ', $columns) . "', '$type')";
Qiang Xue committed
228
		}
229 230
		foreach ($lengths as $length => $columns) {
			$rules[] = "array('" . implode(', ', $columns) . "', 'string', 'max' => $length)";
Qiang Xue committed
231 232 233 234 235
		}

		return $rules;
	}

Qiang Xue committed
236 237 238
	/**
	 * @return array the generated relation declarations
	 */
Qiang Xue committed
239 240
	protected function generateRelations()
	{
Qiang Xue committed
241
		if (!$this->generateRelations) {
Qiang Xue committed
242 243 244
			return array();
		}

Qiang Xue committed
245 246
		$db = $this->getDbConnection();

Qiang Xue committed
247 248
		if (($pos = strpos($this->tableName, '.')) !== false) {
			$schemaName = substr($this->tableName, 0, $pos);
Qiang Xue committed
249 250
		} else {
			$schemaName = '';
Qiang Xue committed
251 252 253
		}

		$relations = array();
Qiang Xue committed
254
		foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) {
Qiang Xue committed
255
			$tableName = $table->name;
Qiang Xue committed
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
			$className = $this->generateClassName($tableName);
			foreach ($table->foreignKeys as $refs) {
				$refTable = $refs[0];
				unset($refs[0]);
				$fks = array_keys($refs);
				$refClassName = $this->generateClassName($refTable);

				// Add relation for this table
				$link = $this->generateRelationLink(array_flip($refs));
				$relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false);
				$relations[$className][$relationName] = array(
					"return \$this->hasOne('$refClassName', $link);",
					$refClassName,
					false,
				);

				// Add relation for the referenced table
				$hasMany = false;
				foreach ($fks as $key) {
					if (!in_array($key, $table->primaryKey, true)) {
						$hasMany = true;
						break;
Qiang Xue committed
278 279
					}
				}
Qiang Xue committed
280 281 282 283 284 285 286 287 288 289 290
				$link = $this->generateRelationLink($refs);
				$relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany);
				$relations[$refClassName][$relationName] = array(
					"return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "('$className', $link);",
					$className,
					$hasMany,
				);
			}

			if (($fks = $this->checkPivotTable($table)) === false) {
				continue;
Qiang Xue committed
291
			}
Qiang Xue committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
			$table0 = $fks[$table->primaryKey[0]][0];
			$table1 = $fks[$table->primaryKey[1]][0];
			$className0 = $this->generateClassName($table0);
			$className1 = $this->generateClassName($table1);

			$link = $this->generateRelationLink(array($fks[$table->primaryKey[1]][1] => $table->primaryKey[1]));
			$viaLink = $this->generateRelationLink(array($table->primaryKey[0] => $fks[$table->primaryKey[0]][1]));
			$relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true);
			$relations[$className0][$relationName] = array(
				"return \$this->hasMany('$className1', $link)->viaTable('{$table->name}', $viaLink);",
				$className0,
				true,
			);

			$link = $this->generateRelationLink(array($fks[$table->primaryKey[0]][1] => $table->primaryKey[0]));
			$viaLink = $this->generateRelationLink(array($table->primaryKey[1] => $fks[$table->primaryKey[1]][1]));
			$relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true);
			$relations[$className1][$relationName] = array(
				"return \$this->hasMany('$className0', $link)->viaTable('{$table->name}', $viaLink);",
				$className1,
				true,
			);
Qiang Xue committed
314 315 316 317
		}
		return $relations;
	}

318
	/**
Qiang Xue committed
319 320 321
	 * Generates the link parameter to be used in generating the relation declaration.
	 * @param array $refs reference constraint
	 * @return string the generated link parameter.
322
	 */
Qiang Xue committed
323
	protected function generateRelationLink($refs)
Qiang Xue committed
324
	{
Qiang Xue committed
325 326 327 328 329
		$pairs = array();
		foreach ($refs as $a => $b) {
			$pairs[] = "'$a' => '$b'";
		}
		return 'array(' . implode(', ', $pairs) . ')';
Qiang Xue committed
330 331 332
	}

	/**
Qiang Xue committed
333 334 335 336 337 338
	 * Checks if the given table is a pivot table.
	 * For simplicity, this method only deals with the case where the pivot contains two PK columns,
	 * each referencing a column in a different table.
	 * @param \yii\db\TableSchema the table being checked
	 * @return array|boolean the relevant foreign key constraint information if the table is a pivot table,
	 * or false if the table is not a pivot table.
Qiang Xue committed
339
	 */
Qiang Xue committed
340
	protected function checkPivotTable($table)
Qiang Xue committed
341
	{
Qiang Xue committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
		$pk = $table->primaryKey;
		if (count($pk) !== 2) {
			return false;
		}
		$fks = array();
		foreach ($table->foreignKeys as $refs) {
			if (count($refs) === 2) {
				if (isset($refs[$pk[0]])) {
					$fks[$pk[0]] = array($refs[0], $refs[$pk[0]]);
				} elseif (isset($refs[$pk[1]])) {
					$fks[$pk[1]] = array($refs[0], $refs[$pk[1]]);
				}
			}
		}
		if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) {
			return $fks;
Qiang Xue committed
358
		} else {
Qiang Xue committed
359
			return false;
Qiang Xue committed
360
		}
Qiang Xue committed
361
	}
Qiang Xue committed
362

Qiang Xue committed
363 364 365 366 367 368 369 370 371 372 373 374 375
	/**
	 * Generate a relation name for the specified table and a base name.
	 * @param array $relations the relations being generated currently.
	 * @param string $className the class name that will contain the relation declarations
	 * @param \yii\db\TableSchema $table the table schema
	 * @param string $key a base name that the relation name may be generated from
	 * @param boolean $multiple whether this is a has-many relation
	 * @return string the relation name
	 */
	protected function generateRelationName($relations, $className, $table, $key, $multiple)
	{
		if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) {
			$key = rtrim(substr($key, 0, -2), '_');
Qiang Xue committed
376
		}
Qiang Xue committed
377 378
		if ($multiple) {
			$key = Inflector::pluralize($key);
Qiang Xue committed
379
		}
Qiang Xue committed
380
		$name = $rawName = Inflector::id2camel($key, '_');
Qiang Xue committed
381 382 383 384
		$i = 0;
		while (isset($table->columns[$name])) {
			$name = $rawName . ($i++);
		}
Qiang Xue committed
385 386 387
		while (isset($relations[$className][$name])) {
			$name = $rawName . ($i++);
		}
Qiang Xue committed
388 389 390 391

		return $name;
	}

Qiang Xue committed
392 393 394
	/**
	 * Validates the [[db]] attribute.
	 */
Qiang Xue committed
395
	public function validateDb()
396
	{
Qiang Xue committed
397 398 399 400
		if (Yii::$app->hasComponent($this->db) === false) {
			$this->addError('db', 'There is no application component named "db".');
		} elseif (!Yii::$app->getComponent($this->db) instanceof Connection) {
			$this->addError('db', 'The "db" application component must be a DB connection instance.');
Qiang Xue committed
401 402 403
		}
	}

Qiang Xue committed
404 405 406
	/**
	 * Validates the [[ns]] attribute.
	 */
Qiang Xue committed
407 408
	public function validateNamespace()
	{
Qiang Xue committed
409 410
		$this->ns = ltrim($this->ns, '\\');
		$path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
Qiang Xue committed
411 412 413
		if ($path === false) {
			$this->addError('ns', 'Namespace must be associated with an existing directory.');
		}
Qiang Xue committed
414 415
	}

Qiang Xue committed
416 417 418
	/**
	 * Validates the [[modelClass]] attribute.
	 */
Qiang Xue committed
419 420 421
	public function validateModelClass()
	{
		if ($this->isReservedKeyword($this->modelClass)) {
Qiang Xue committed
422
			$this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
Qiang Xue committed
423 424 425 426 427 428
		}
		if (strpos($this->tableName, '*') === false && $this->modelClass == '') {
			$this->addError('modelClass', 'Model Class cannot be blank.');
		}
	}

Qiang Xue committed
429 430 431
	/**
	 * Validates the [[tableName]] attribute.
	 */
Qiang Xue committed
432 433
	public function validateTableName()
	{
Qiang Xue committed
434 435 436 437
		if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) {
			$this->addError('tableName', 'At most one asterisk is allowed.');
			return;
		}
Qiang Xue committed
438 439
		$tables = $this->getTableNames();
		if (empty($tables)) {
Qiang Xue committed
440
			$this->addError('tableName', "Table '{$this->tableName}' does not exist.");
Qiang Xue committed
441 442 443 444
		} else {
			foreach ($tables as $table) {
				$class = $this->generateClassName($table);
				if ($this->isReservedKeyword($class)) {
Qiang Xue committed
445
					$this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword.");
Qiang Xue committed
446 447 448 449 450
					break;
				}
			}
		}
	}
Qiang Xue committed
451

452 453 454
	private $_tableNames;
	private $_classNames;

Qiang Xue committed
455 456 457
	/**
	 * @return array the table names that match the pattern specified by [[tableName]].
	 */
Qiang Xue committed
458 459
	protected function getTableNames()
	{
460 461 462
		if ($this->_tableNames !== null) {
			return $this->_tableNames;
		}
Qiang Xue committed
463 464
		$db = $this->getDbConnection();
		$tableNames = array();
Qiang Xue committed
465
		if (strpos($this->tableName, '*') !== false) {
Qiang Xue committed
466 467
			if (($pos = strrpos($this->tableName, '.')) !== false) {
				$schema = substr($this->tableName, 0, $pos);
Qiang Xue committed
468
				$pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/';
Qiang Xue committed
469 470
			} else {
				$schema = '';
Qiang Xue committed
471
				$pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/';
Qiang Xue committed
472 473
			}

Qiang Xue committed
474 475 476
			foreach ($db->schema->getTableNames($schema) as $table) {
				if (preg_match($pattern, $table)) {
					$tableNames[] = $schema === '' ? $table : ($schema . '.' . $table);
Qiang Xue committed
477 478
				}
			}
Qiang Xue committed
479 480
		} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
			$tableNames[] = $this->tableName;
481 482 483 484 485
			$this->_classNames[$this->tableName] = $this->modelClass;
		}
		return $this->_tableNames = $tableNames;
	}

Qiang Xue committed
486 487 488 489 490
	/**
	 * Generates a class name from the specified table name.
	 * @param string $tableName the table name (which may contain schema prefix)
	 * @return string the generated class name
	 */
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
	protected function generateClassName($tableName)
	{
		if (isset($this->_classNames[$tableName])) {
			return $this->_classNames[$tableName];
		}

		if (($pos = strrpos($tableName, '.')) !== false) {
			$tableName = substr($tableName, $pos + 1);
		}

		$db = $this->getDbConnection();
		$patterns = array();
		if (strpos($this->tableName, '*') !== false) {
			$pattern = $this->tableName;
			if (($pos = strrpos($pattern, '.')) !== false) {
				$pattern = substr($pattern, $pos + 1);
			}
			$patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
		}
		if (!empty($db->tablePrefix)) {
			$patterns[] = "/^{$db->tablePrefix}(.*?)|(.*?){$db->tablePrefix}$/";
		} else {
			$patterns[] = "/^tbl_(.*?)$/";
		}

		$className = $tableName;
		foreach ($patterns as $pattern) {
			if (preg_match($pattern, $tableName, $matches)) {
				$className = $matches[1];
			}
Qiang Xue committed
521
		}
522
		return $this->_classNames[$tableName] = Inflector::id2camel($className, '_');
523
	}
Qiang Xue committed
524 525 526 527 528 529 530 531

	/**
	 * @return Connection the DB connection as specified by [[db]].
	 */
	protected function getDbConnection()
	{
		return Yii::$app->{$this->db};
	}
Qiang Xue committed
532
}