Generator.php 13.5 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\crud;

Qiang Xue committed
10
use Yii;
Qiang Xue committed
11
use yii\db\ActiveRecord;
12
use yii\db\BaseActiveRecord;
Qiang Xue committed
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 17
use yii\web\Controller;

Qiang Xue committed
18
/**
19 20 21 22
 *
 * @property string $controllerID The controller ID (without the module ID prefix). This property is
 * read-only.
 * @property string $viewPath The action view file path. This property is read-only.
Qiang Xue committed
23 24 25 26 27 28
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Generator extends \yii\gii\Generator
{
Qiang Xue committed
29
	public $modelClass;
Qiang Xue committed
30 31
	public $moduleID;
	public $controllerClass;
Qiang Xue committed
32
	public $baseControllerClass = 'yii\web\Controller';
Qiang Xue committed
33 34
	public $indexWidgetType = 'grid';
	public $searchModelClass;
Qiang Xue committed
35

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

44 45 46
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
47 48 49 50 51
	public function getDescription()
	{
		return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete)
			operations for the specified data model.';
	}
52

53 54 55
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
56 57
	public function rules()
	{
Alexander Makarov committed
58
		return array_merge(parent::rules(), [
59 60 61 62
			[['moduleID', 'controllerClass', 'modelClass', 'searchModelClass', 'baseControllerClass'], 'filter', 'filter' => 'trim'],
			[['modelClass', 'searchModelClass', 'controllerClass', 'baseControllerClass', 'indexWidgetType'], 'required'],
			[['searchModelClass'], 'compare', 'compareAttribute' => 'modelClass', 'operator' => '!==', 'message' => 'Search Model Class must not be equal to Model Class.'],
			[['modelClass', 'controllerClass', 'baseControllerClass', 'searchModelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'],
63
			[['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]],
64 65 66 67 68 69
			[['baseControllerClass'], 'validateClass', 'params' => ['extends' => Controller::className()]],
			[['controllerClass'], 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'],
			[['controllerClass', 'searchModelClass'], 'validateNewClass'],
			[['indexWidgetType'], 'in', 'range' => ['grid', 'list']],
			[['modelClass'], 'validateModelClass'],
			[['moduleID'], 'validateModuleID'],
Alexander Makarov committed
70
		]);
Qiang Xue committed
71 72
	}

73 74 75
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
76 77
	public function attributeLabels()
	{
Alexander Makarov committed
78
		return array_merge(parent::attributeLabels(), [
Qiang Xue committed
79
			'modelClass' => 'Model Class',
Qiang Xue committed
80 81
			'moduleID' => 'Module ID',
			'controllerClass' => 'Controller Class',
Qiang Xue committed
82
			'baseControllerClass' => 'Base Controller Class',
Qiang Xue committed
83 84
			'indexWidgetType' => 'Widget Used in Index Page',
			'searchModelClass' => 'Search Model Class',
Alexander Makarov committed
85
		]);
Qiang Xue committed
86 87 88
	}

	/**
Qiang Xue committed
89
	 * @inheritdoc
Qiang Xue committed
90 91 92
	 */
	public function hints()
	{
Alexander Makarov committed
93
		return [
Qiang Xue committed
94 95
			'modelClass' => 'This is the ActiveRecord class associated with the table that CRUD will be built upon.
				You should provide a fully qualified class name, e.g., <code>app\models\Post</code>.',
Qiang Xue committed
96 97
			'controllerClass' => 'This is the name of the controller class to be generated. You should
				provide a fully qualified namespaced class, .e.g, <code>app\controllers\PostController</code>.',
Qiang Xue committed
98 99
			'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from.
				You should provide a fully qualified class name, e.g., <code>yii\web\Controller</code>.',
Qiang Xue committed
100 101
			'moduleID' => 'This is the ID of the module that the generated controller will belong to.
				If not set, it means the controller will belong to the application.',
Qiang Xue committed
102 103
			'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models.
				You may choose either <code>GridView</code> or <code>ListView</code>',
Carsten Brandt committed
104
			'searchModelClass' => 'This is the class representing the data being collected in the search form.
Qiang Xue committed
105
			 	A fully qualified namespaced class name is required, e.g., <code>app\models\search\PostSearch</code>.',
Alexander Makarov committed
106
		];
Qiang Xue committed
107 108
	}

109 110 111
	/**
	 * @inheritdoc
	 */
Qiang Xue committed
112 113
	public function requiredTemplates()
	{
Alexander Makarov committed
114
		return ['controller.php'];
Qiang Xue committed
115 116 117
	}

	/**
Qiang Xue committed
118
	 * @inheritdoc
Qiang Xue committed
119 120 121
	 */
	public function stickyAttributes()
	{
Alexander Makarov committed
122
		return ['baseControllerClass', 'moduleID', 'indexWidgetType'];
Qiang Xue committed
123 124
	}

125 126 127
	/**
	 * Checks if model class is valid
	 */
Qiang Xue committed
128 129 130 131 132 133 134 135
	public function validateModelClass()
	{
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pk = $class::primaryKey();
		if (empty($pk)) {
			$this->addError('modelClass', "The table associated with $class must have primary key(s).");
		}
Qiang Xue committed
136 137
	}

138 139 140
	/**
	 * Checks if model ID is valid
	 */
Qiang Xue committed
141 142 143 144 145 146 147 148 149 150
	public function validateModuleID()
	{
		if (!empty($this->moduleID)) {
			$module = Yii::$app->getModule($this->moduleID);
			if ($module === null) {
				$this->addError('moduleID', "Module '{$this->moduleID}' does not exist.");
			}
		}
	}

151
	/**
Qiang Xue committed
152
	 * @inheritdoc
153 154 155
	 */
	public function generate()
	{
Qiang Xue committed
156 157
		$controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php');
		$searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php'));
Alexander Makarov committed
158
		$files = [
Qiang Xue committed
159 160
			new CodeFile($controllerFile, $this->render('controller.php')),
			new CodeFile($searchModel, $this->render('search.php')),
Alexander Makarov committed
161
		];
Qiang Xue committed
162

Qiang Xue committed
163
		$viewPath = $this->getViewPath();
Qiang Xue committed
164 165 166 167
		$templatePath = $this->getTemplatePath() . '/views';
		foreach (scandir($templatePath) as $file) {
			if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
				$files[] = new CodeFile("$viewPath/$file", $this->render("views/$file"));
Qiang Xue committed
168 169 170
			}
		}

Qiang Xue committed
171

Qiang Xue committed
172
		return $files;
173
	}
Qiang Xue committed
174 175 176 177 178 179

	/**
	 * @return string the controller ID (without the module ID prefix)
	 */
	public function getControllerID()
	{
Qiang Xue committed
180 181 182
		$pos = strrpos($this->controllerClass, '\\');
		$class = substr(substr($this->controllerClass, $pos + 1), 0, -10);
		return Inflector::camel2id($class);
Qiang Xue committed
183 184 185 186 187 188 189
	}

	/**
	 * @return string the action view file path
	 */
	public function getViewPath()
	{
Qiang Xue committed
190
		$module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID);
Qiang Xue committed
191 192
		return $module->getViewPath() . '/' . $this->getControllerID() ;
	}
Qiang Xue committed
193 194 195

	public function getNameAttribute()
	{
196
		foreach ($this->getColumnNames() as $name) {
Qiang Xue committed
197 198 199 200
			if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) {
				return $name;
			}
		}
201 202
		/** @var \yii\db\ActiveRecord $class */
		$class = $this->modelClass;
Qiang Xue committed
203 204 205
		$pk = $class::primaryKey();
		return $pk[0];
	}
Qiang Xue committed
206 207

	/**
208
	 * Generates code for active field
Qiang Xue committed
209 210 211
	 * @param string $attribute
	 * @return string
	 */
Qiang Xue committed
212
	public function generateActiveField($attribute)
Qiang Xue committed
213
	{
Qiang Xue committed
214
		$tableSchema = $this->getTableSchema();
215 216
		if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) {
			if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) {
Carsten Brandt committed
217
				return "\$form->field(\$model, '$attribute')->passwordInput()";
218
			} else {
Carsten Brandt committed
219
				return "\$form->field(\$model, '$attribute')";
220
			}
Qiang Xue committed
221 222 223
		}
		$column = $tableSchema->columns[$attribute];
		if ($column->phpType === 'boolean') {
224
			return "\$form->field(\$model, '$attribute')->checkbox()";
Qiang Xue committed
225
		} elseif ($column->type === 'text') {
226
			return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])";
Qiang Xue committed
227 228 229 230 231 232 233
		} else {
			if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) {
				$input = 'passwordInput';
			} else {
				$input = 'textInput';
			}
			if ($column->phpType !== 'string' || $column->size === null) {
234
				return "\$form->field(\$model, '$attribute')->$input()";
Qiang Xue committed
235
			} else {
236
				return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])";
Qiang Xue committed
237 238 239 240
			}
		}
	}

Qiang Xue committed
241
	/**
242
	 * Generates code for active search field
Qiang Xue committed
243 244 245 246 247 248
	 * @param string $attribute
	 * @return string
	 */
	public function generateActiveSearchField($attribute)
	{
		$tableSchema = $this->getTableSchema();
249 250 251
		if ($tableSchema === false) {
			return "\$form->field(\$model, '$attribute')";
		}
Qiang Xue committed
252 253
		$column = $tableSchema->columns[$attribute];
		if ($column->phpType === 'boolean') {
254
			return "\$form->field(\$model, '$attribute')->checkbox()";
Qiang Xue committed
255
		} else {
256
			return "\$form->field(\$model, '$attribute')";
Qiang Xue committed
257 258 259
		}
	}

Qiang Xue committed
260
	/**
261
	 * Generates column format
Qiang Xue committed
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
	 * @param \yii\db\ColumnSchema $column
	 * @return string
	 */
	public function generateColumnFormat($column)
	{
		if ($column->phpType === 'boolean') {
			return 'boolean';
		} elseif ($column->type === 'text') {
			return 'ntext';
		} elseif (stripos($column->name, 'time') !== false && $column->phpType === 'integer') {
			return 'datetime';
		} elseif (stripos($column->name, 'email') !== false) {
			return 'email';
		} elseif (stripos($column->name, 'url') !== false) {
			return 'url';
		} else {
			return 'text';
		}
	}
Qiang Xue committed
281 282 283 284 285 286 287

	/**
	 * Generates validation rules for the search model.
	 * @return array the generated validation rules
	 */
	public function generateSearchRules()
	{
288 289 290
		if (($table = $this->getTableSchema()) === false) {
			return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"];
		}
Alexander Makarov committed
291
		$types = [];
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 314 315 316
		foreach ($table->columns as $column) {
			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:
				default:
					$types['safe'][] = $column->name;
					break;
			}
		}

Alexander Makarov committed
317
		$rules = [];
Qiang Xue committed
318
		foreach ($types as $type => $columns) {
KiTE committed
319
			$rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
Qiang Xue committed
320 321 322 323 324
		}

		return $rules;
	}

325 326 327
	/**
	 * @return array searchable attributes
	 */
Qiang Xue committed
328 329
	public function getSearchAttributes()
	{
330
		return $this->getColumnNames();
Qiang Xue committed
331 332 333 334 335 336 337 338
	}

	/**
	 * Generates the attribute labels for the search model.
	 * @return array the generated attribute labels (name => label)
	 */
	public function generateSearchLabels()
	{
339
		/** @var \yii\base\Model $model */
340 341
		$model = new $this->modelClass();
		$attributeLabels = $model->attributeLabels();
Alexander Makarov committed
342
		$labels = [];
343
		foreach ($this->getColumnNames() as $name) {
344 345
			if (isset($attributeLabels[$name])) {
				$labels[$name] = $attributeLabels[$name];
Qiang Xue committed
346
			} else {
347 348 349 350 351 352 353 354
				if (!strcasecmp($name, 'id')) {
					$labels[$name] = 'ID';
				} else {
					$label = Inflector::camel2words($name);
					if (strcasecmp(substr($label, -3), ' id') === 0) {
						$label = substr($label, 0, -3) . ' ID';
					}
					$labels[$name] = $label;
Qiang Xue committed
355 356 357 358 359 360
				}
			}
		}
		return $labels;
	}

361 362 363 364
	/**
	 * Generates search conditions
	 * @return array
	 */
Qiang Xue committed
365 366
	public function generateSearchConditions()
	{
367 368 369
		$columns = [];
		if (($table = $this->getTableSchema()) === false) {
			$class = $this->modelClass;
370
			/** @var \yii\base\Model $model */
371 372 373 374 375 376 377 378 379
			$model = new $class();
			foreach ($model->attributes() as $attribute) {
				$columns[$attribute] = 'unknown';
			}
		} else {
			foreach ($table->columns as $column) {
				$columns[$column->name] = $column->type;
			}
		}
Alexander Makarov committed
380
		$conditions = [];
381 382
		foreach ($columns as $column => $type) {
			switch ($type) {
Qiang Xue committed
383 384 385 386 387 388 389 390 391 392 393
				case Schema::TYPE_SMALLINT:
				case Schema::TYPE_INTEGER:
				case Schema::TYPE_BIGINT:
				case Schema::TYPE_BOOLEAN:
				case Schema::TYPE_FLOAT:
				case Schema::TYPE_DECIMAL:
				case Schema::TYPE_MONEY:
				case Schema::TYPE_DATE:
				case Schema::TYPE_TIME:
				case Schema::TYPE_DATETIME:
				case Schema::TYPE_TIMESTAMP:
394
					$conditions[] = "\$this->addCondition(\$query, '{$column}');";
Qiang Xue committed
395 396
					break;
				default:
397
					$conditions[] = "\$this->addCondition(\$query, '{$column}', true);";
Qiang Xue committed
398 399 400 401 402 403 404
					break;
			}
		}

		return $conditions;
	}

405 406 407 408
	/**
	 * Generates URL parameters
	 * @return string
	 */
Qiang Xue committed
409 410
	public function generateUrlParams()
	{
411 412 413
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
Qiang Xue committed
414 415 416
		if (count($pks) === 1) {
			return "'id' => \$model->{$pks[0]}";
		} else {
Alexander Makarov committed
417
			$params = [];
Qiang Xue committed
418 419 420 421 422 423 424
			foreach ($pks as $pk) {
				$params[] = "'$pk' => \$model->$pk";
			}
			return implode(', ', $params);
		}
	}

425 426 427 428
	/**
	 * Generates action parameters
	 * @return string
	 */
Qiang Xue committed
429 430
	public function generateActionParams()
	{
431 432 433
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
Qiang Xue committed
434 435 436 437 438 439 440
		if (count($pks) === 1) {
			return '$id';
		} else {
			return '$' . implode(', $', $pks);
		}
	}

441 442 443 444
	/**
	 * Generates parameter tags for phpdoc
	 * @return array parameter tags for phpdoc
	 */
Qiang Xue committed
445 446
	public function generateActionParamComments()
	{
447 448 449
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
450 451 452 453 454 455 456
		if (($table = $this->getTableSchema()) === false) {
			$params = [];
			foreach ($pks as $pk) {
				$params[] = '@param ' . (substr(strtolower($pk), -2) == 'id' ? 'integer' : 'string') . ' $' . $pk;
			}
			return $params;
		}
Qiang Xue committed
457
		if (count($pks) === 1) {
Alexander Makarov committed
458
			return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id'];
Qiang Xue committed
459
		} else {
Alexander Makarov committed
460
			$params = [];
Qiang Xue committed
461 462 463 464 465 466 467
			foreach ($pks as $pk) {
				$params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk;
			}
			return $params;
		}
	}

468 469 470 471
	/**
	 * Returns table schema for current model class or false if it is not an active record
	 * @return boolean|\yii\db\TableSchema
	 */
Qiang Xue committed
472 473 474 475
	public function getTableSchema()
	{
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
476 477 478 479 480
		if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
			return $class::getTableSchema();
		} else {
			return false;
		}
Qiang Xue committed
481
	}
482

483 484 485
	/**
	 * @return array model column names
	 */
486 487 488 489 490 491 492
	public function getColumnNames()
	{
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
			return $class::getTableSchema()->getColumnNames();
		} else {
493
			/** @var \yii\base\Model $model */
494 495 496 497 498
			$model = new $class();
			return $model->attributes();
		}
	}

Qiang Xue committed
499
}