Generator.php 14.8 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;
Qiang Xue committed
13
use yii\gii\CodeFile;
Qiang Xue committed
14
use yii\helpers\Inflector;
Qiang Xue committed
15

Qiang Xue committed
16 17 18 19 20 21 22
/**
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Generator extends \yii\gii\Generator
{
Qiang Xue committed
23
	public $db = 'db';
Qiang Xue committed
24
	public $ns = 'app\models';
Qiang Xue committed
25 26
	public $tableName;
	public $modelClass;
Qiang Xue committed
27
	public $baseClass = 'yii\db\ActiveRecord';
Qiang Xue committed
28
	public $generateRelations = true;
Qiang Xue committed
29
	public $generateLabelsFromComments = false;
Qiang Xue committed
30 31


Qiang Xue committed
32 33 34 35 36 37 38
	public function getName()
	{
		return 'Model Generator';
	}

	public function getDescription()
	{
Qiang Xue committed
39
		return 'This generator generates an ActiveRecord class for the specified database table.';
Qiang Xue committed
40
	}
41

Qiang Xue committed
42 43 44
	public function rules()
	{
		return array_merge(parent::rules(), array(
Qiang Xue committed
45 46 47 48
			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
49
			array('tableName', 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'),
Qiang Xue committed
50 51
			array('db', 'validateDb'),
			array('ns', 'validateNamespace'),
Qiang Xue committed
52
			array('tableName', 'validateTableName'),
Qiang Xue committed
53
			array('modelClass', 'validateModelClass'),
Qiang Xue committed
54
			array('baseClass', 'validateClass', 'params' => array('extends' => ActiveRecord::className())),
Qiang Xue committed
55
			array('generateRelations, generateLabelsFromComments', 'boolean'),
Qiang Xue committed
56 57 58 59 60
		));
	}

	public function attributeLabels()
	{
Qiang Xue committed
61
		return array(
Qiang Xue committed
62 63
			'ns' => 'Namespace',
			'db' => 'Database Connection ID',
Qiang Xue committed
64 65 66
			'tableName' => 'Table Name',
			'modelClass' => 'Model Class',
			'baseClass' => 'Base Class',
Qiang Xue committed
67
			'generateRelations' => 'Generate Relations',
Qiang Xue committed
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
			'generateLabelsFromComments' => 'Generate Labels from DB Comments',
		);
	}

	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
87
				you may want to uncheck this option to accelerate the code generation proc	ess.',
Qiang Xue committed
88 89
			'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels
				by using the comments of the corresponding DB columns.',
Qiang Xue committed
90
		);
Qiang Xue committed
91 92 93 94 95 96 97 98 99 100 101
	}

	public function requiredTemplates()
	{
		return array(
			'model.php',
		);
	}

	public function stickyAttributes()
	{
Qiang Xue committed
102
		return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments');
Qiang Xue committed
103 104
	}

Qiang Xue committed
105 106 107
	/**
	 * @return Connection
	 */
Qiang Xue committed
108 109 110
	public function getDbConnection()
	{
		return Yii::$app->{$this->db};
Qiang Xue committed
111 112 113 114
	}

	public function generate()
	{
Qiang Xue committed
115
		$files = array();
Qiang Xue committed
116 117 118
		foreach ($this->getTableNames() as $tableName) {
			$className = $this->generateClassName($tableName);
			$tableSchema = $this->getTableSchema($tableName);
Qiang Xue committed
119
			$params = array(
Qiang Xue committed
120
				'tableName' => $tableName,
Qiang Xue committed
121
				'className' => $className,
Qiang Xue committed
122 123
				'tableSchema' => $tableSchema,
				'labels' => $this->generateLabels($tableSchema),
Qiang Xue committed
124
			);
Qiang Xue committed
125
			$files[] = new CodeFile(
Qiang Xue committed
126
				Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
Qiang Xue committed
127
				$this->render('model.php', $params)
Qiang Xue committed
128 129
			);
		}
Qiang Xue committed
130 131

		return $files;
Qiang Xue committed
132 133 134 135
	}

	public function getTableSchema($tableName)
	{
Qiang Xue committed
136
		return $this->getDbConnection()->getTableSchema($tableName, true);
Qiang Xue committed
137 138 139 140 141 142
	}

	public function generateLabels($table)
	{
		$labels = array();
		foreach ($table->columns as $column) {
Qiang Xue committed
143
			if ($this->generateLabelsFromComments && !empty($column->comment)) {
Qiang Xue committed
144
				$labels[$column->name] = $column->comment;
Qiang Xue committed
145 146
			} elseif (!strcasecmp($column->name, 'id')) {
				$labels[$column->name] = 'ID';
Qiang Xue committed
147
			} else {
Qiang Xue committed
148
				$label = Inflector::camel2words($column->name);
Qiang Xue committed
149
				if (strcasecmp(substr($label, -3), ' id') === 0) {
Qiang Xue committed
150
					$label = substr($label, 0, -3) . ' ID';
Qiang Xue committed
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 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 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
				}
				$labels[$column->name] = $label;
			}
		}
		return $labels;
	}

	public function generateRules($table)
	{
		$rules = array();
		$required = array();
		$integers = array();
		$numerical = array();
		$length = array();
		$safe = array();
		foreach ($table->columns as $column) {
			if ($column->autoIncrement) {
				continue;
			}
			$r = !$column->allowNull && $column->defaultValue === null;
			if ($r) {
				$required[] = $column->name;
			}
			if ($column->type === 'integer') {
				$integers[] = $column->name;
			} elseif ($column->type === 'double') {
				$numerical[] = $column->name;
			} elseif ($column->type === 'string' && $column->size > 0) {
				$length[$column->size][] = $column->name;
			} elseif (!$column->isPrimaryKey && !$r) {
				$safe[] = $column->name;
			}
		}
		if ($required !== array()) {
			$rules[] = "array('" . implode(', ', $required) . "', 'required')";
		}
		if ($integers !== array()) {
			$rules[] = "array('" . implode(', ', $integers) . "', 'numerical', 'integerOnly'=>true)";
		}
		if ($numerical !== array()) {
			$rules[] = "array('" . implode(', ', $numerical) . "', 'numerical')";
		}
		if ($length !== array()) {
			foreach ($length as $len => $cols) {
				$rules[] = "array('" . implode(', ', $cols) . "', 'length', 'max'=>$len)";
			}
		}
		if ($safe !== array()) {
			$rules[] = "array('" . implode(', ', $safe) . "', 'safe')";
		}

		return $rules;
	}

	public function getRelations($className)
	{
		return isset($this->relations[$className]) ? $this->relations[$className] : array();
	}

	protected function removePrefix($tableName, $addBrackets = true)
	{
		if ($addBrackets && Yii::$app->{$this->connectionId}->tablePrefix == '') {
			return $tableName;
		}
		$prefix = $this->tablePrefix != '' ? $this->tablePrefix : Yii::$app->{$this->connectionId}->tablePrefix;
		if ($prefix != '') {
			if ($addBrackets && Yii::$app->{$this->connectionId}->tablePrefix != '') {
				$prefix = Yii::$app->{$this->connectionId}->tablePrefix;
				$lb = '{{';
				$rb = '}}';
			} else {
				$lb = $rb = '';
			}
			if (($pos = strrpos($tableName, '.')) !== false) {
				$schema = substr($tableName, 0, $pos);
				$name = substr($tableName, $pos + 1);
				if (strpos($name, $prefix) === 0) {
					return $schema . '.' . $lb . substr($name, strlen($prefix)) . $rb;
				}
			} elseif (strpos($tableName, $prefix) === 0) {
				return $lb . substr($tableName, strlen($prefix)) . $rb;
			}
		}
		return $tableName;
	}

	protected function generateRelations()
	{
Qiang Xue committed
239
		if (!$this->generateRelations) {
Qiang Xue committed
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
			return array();
		}

		$schemaName = '';
		if (($pos = strpos($this->tableName, '.')) !== false) {
			$schemaName = substr($this->tableName, 0, $pos);
		}

		$relations = array();
		foreach (Yii::$app->{$this->connectionId}->schema->getTables($schemaName) as $table) {
			if ($this->tablePrefix != '' && strpos($table->name, $this->tablePrefix) !== 0) {
				continue;
			}
			$tableName = $table->name;

			if ($this->isRelationTable($table)) {
				$pks = $table->primaryKey;
				$fks = $table->foreignKeys;

				$table0 = $fks[$pks[0]][0];
				$table1 = $fks[$pks[1]][0];
				$className0 = $this->generateClassName($table0);
				$className1 = $this->generateClassName($table1);

				$unprefixedTableName = $this->removePrefix($tableName);

				$relationName = $this->generateRelationName($table0, $table1, true);
				$relations[$className0][$relationName] = "array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')";

				$relationName = $this->generateRelationName($table1, $table0, true);

				$i = 1;
				$rawName = $relationName;
				while (isset($relations[$className1][$relationName])) {
					$relationName = $rawName . $i++;
				}

				$relations[$className1][$relationName] = "array(self::MANY_MANY, '$className0', '$unprefixedTableName($pks[1], $pks[0])')";
			} else {
				$className = $this->generateClassName($tableName);
				foreach ($table->foreignKeys as $fkName => $fkEntry) {
					// Put table and key name in variables for easier reading
					$refTable = $fkEntry[0]; // Table name that current fk references to
					$refKey = $fkEntry[1]; // Key in that table being referenced
					$refClassName = $this->generateClassName($refTable);

					// Add relation for this table
					$relationName = $this->generateRelationName($tableName, $fkName, false);
					$relations[$className][$relationName] = "array(self::BELONGS_TO, '$refClassName', '$fkName')";

					// Add relation for the referenced table
					$relationType = $table->primaryKey === $fkName ? 'HAS_ONE' : 'HAS_MANY';
					$relationName = $this->generateRelationName($refTable, $this->removePrefix($tableName, false), $relationType === 'HAS_MANY');
					$i = 1;
					$rawName = $relationName;
					while (isset($relations[$refClassName][$relationName])) {
						$relationName = $rawName . ($i++);
					}
					$relations[$refClassName][$relationName] = "array(self::$relationType, '$className', '$fkName')";
				}
			}
		}
		return $relations;
	}

305
	/**
Qiang Xue committed
306 307 308 309
	 * Checks if the given table is a "many to many" pivot table.
	 * Their PK has 2 fields, and both of those fields are also FK to other separate tables.
	 * @param CDbTableSchema table to inspect
	 * @return boolean true if table matches description of helpter table.
310
	 */
Qiang Xue committed
311 312 313 314 315 316 317 318 319 320 321
	protected function isRelationTable($table)
	{
		$pk = $table->primaryKey;
		return (count($pk) === 2 // we want 2 columns
			&& isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key
			&& isset($table->foreignKeys[$pk[1]]) // pk column 2 is also a foriegn key
			&& $table->foreignKeys[$pk[0]][0] !== $table->foreignKeys[$pk[1]][0]); // and the foreign keys point different tables
	}

	protected function generateClassName($tableName)
	{
Qiang Xue committed
322 323
		if (($pos = strrpos($tableName, '.')) !== false) {
			$tableName = substr($tableName, $pos + 1);
Qiang Xue committed
324 325
		}

Qiang Xue committed
326 327 328 329 330 331 332 333
		$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) . '$/';
Qiang Xue committed
334
		}
Qiang Xue committed
335 336 337 338 339 340 341 342
		if (!empty($db->tablePrefix)) {
			$patterns[] = "/^{$db->tablePrefix}(.*?)|(.*?){$db->tablePrefix}$/";
		}

		$className = $tableName;
		foreach ($patterns as $pattern) {
			if (preg_match($pattern, $tableName, $matches)) {
				$className = $matches[1];
Qiang Xue committed
343 344
			}
		}
Qiang Xue committed
345
		return Inflector::id2camel($className, '_');
Qiang Xue committed
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
	}

	/**
	 * Generate a name for use as a relation name (inside relations() function in a model).
	 * @param string the name of the table to hold the relation
	 * @param string the foreign key name
	 * @param boolean whether the relation would contain multiple objects
	 * @return string the relation name
	 */
	protected function generateRelationName($tableName, $fkName, $multiple)
	{
		if (strcasecmp(substr($fkName, -2), 'id') === 0 && strcasecmp($fkName, 'id')) {
			$relationName = rtrim(substr($fkName, 0, -2), '_');
		} else {
			$relationName = $fkName;
		}
		$relationName[0] = strtolower($relationName);

		if ($multiple) {
			$relationName = $this->pluralize($relationName);
		}

		$names = preg_split('/_+/', $relationName, -1, PREG_SPLIT_NO_EMPTY);
		if (empty($names)) {
			return $relationName;
		} // unlikely
		for ($name = $names[0], $i = 1; $i < count($names); ++$i) {
			$name .= ucfirst($names[$i]);
		}

		$rawName = $name;
		$table = Yii::$app->{$this->connectionId}->schema->getTable($tableName);
		$i = 0;
		while (isset($table->columns[$name])) {
			$name = $rawName . ($i++);
		}

		return $name;
	}

Qiang Xue committed
386
	public function validateDb()
387
	{
Qiang Xue committed
388 389 390 391
		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
392 393 394 395 396
		}
	}

	public function validateNamespace()
	{
Qiang Xue committed
397 398
		$this->ns = ltrim($this->ns, '\\');
		$path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
Qiang Xue committed
399 400 401
		if ($path === false) {
			$this->addError('ns', 'Namespace must be associated with an existing directory.');
		}
Qiang Xue committed
402 403 404 405 406
	}

	public function validateModelClass()
	{
		if ($this->isReservedKeyword($this->modelClass)) {
Qiang Xue committed
407
			$this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
Qiang Xue committed
408 409 410 411 412 413 414 415
		}
		if (strpos($this->tableName, '*') === false && $this->modelClass == '') {
			$this->addError('modelClass', 'Model Class cannot be blank.');
		}
	}

	public function validateTableName()
	{
Qiang Xue committed
416 417 418 419
		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
420 421
		$tables = $this->getTableNames();
		if (empty($tables)) {
Qiang Xue committed
422
			$this->addError('tableName', "Table '{$this->tableName}' does not exist.");
Qiang Xue committed
423 424 425 426
		} else {
			foreach ($tables as $table) {
				$class = $this->generateClassName($table);
				if ($this->isReservedKeyword($class)) {
Qiang Xue committed
427
					$this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword.");
Qiang Xue committed
428 429 430 431 432
					break;
				}
			}
		}
	}
Qiang Xue committed
433

Qiang Xue committed
434 435 436 437
	protected function getTableNames()
	{
		$db = $this->getDbConnection();
		$tableNames = array();
Qiang Xue committed
438
		if (strpos($this->tableName, '*') !== false) {
Qiang Xue committed
439 440
			if (($pos = strrpos($this->tableName, '.')) !== false) {
				$schema = substr($this->tableName, 0, $pos);
Qiang Xue committed
441
				$pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/';
Qiang Xue committed
442 443
			} else {
				$schema = '';
Qiang Xue committed
444
				$pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/';
Qiang Xue committed
445 446
			}

Qiang Xue committed
447 448 449
			foreach ($db->schema->getTableNames($schema) as $table) {
				if (preg_match($pattern, $table)) {
					$tableNames[] = $schema === '' ? $table : ($schema . '.' . $table);
Qiang Xue committed
450 451
				}
			}
Qiang Xue committed
452 453
		} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
			$tableNames[] = $this->tableName;
Qiang Xue committed
454
		}
Qiang Xue committed
455
		return $tableNames;
456
	}
Qiang Xue committed
457
}