Generator.php 12.2 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 23 24
/**
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Generator extends \yii\gii\Generator
{
Qiang Xue committed
25
	public $modelClass;
Qiang Xue committed
26 27
	public $moduleID;
	public $controllerClass;
Qiang Xue committed
28
	public $baseControllerClass = 'yii\web\Controller';
Qiang Xue committed
29 30
	public $indexWidgetType = 'grid';
	public $searchModelClass;
Qiang Xue committed
31

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

	public function getDescription()
	{
		return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete)
			operations for the specified data model.';
	}
42

Qiang Xue committed
43 44
	public function rules()
	{
Alexander Makarov committed
45
		return array_merge(parent::rules(), [
46 47 48 49
			[['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.'],
50
			[['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]],
51 52 53 54 55 56
			[['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
57
		]);
Qiang Xue committed
58 59 60 61
	}

	public function attributeLabels()
	{
Alexander Makarov committed
62
		return array_merge(parent::attributeLabels(), [
Qiang Xue committed
63
			'modelClass' => 'Model Class',
Qiang Xue committed
64 65
			'moduleID' => 'Module ID',
			'controllerClass' => 'Controller Class',
Qiang Xue committed
66
			'baseControllerClass' => 'Base Controller Class',
Qiang Xue committed
67 68
			'indexWidgetType' => 'Widget Used in Index Page',
			'searchModelClass' => 'Search Model Class',
Alexander Makarov committed
69
		]);
Qiang Xue committed
70 71 72
	}

	/**
Qiang Xue committed
73
	 * @inheritdoc
Qiang Xue committed
74 75 76
	 */
	public function hints()
	{
Alexander Makarov committed
77
		return [
Qiang Xue committed
78 79
			'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
80 81
			'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
82 83
			'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
84 85
			'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
86 87
			'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
88
			'searchModelClass' => 'This is the class representing the data being collected in the search form.
Qiang Xue committed
89
			 	A fully qualified namespaced class name is required, e.g., <code>app\models\search\PostSearch</code>.',
Alexander Makarov committed
90
		];
Qiang Xue committed
91 92 93 94
	}

	public function requiredTemplates()
	{
Alexander Makarov committed
95
		return ['controller.php'];
Qiang Xue committed
96 97 98
	}

	/**
Qiang Xue committed
99
	 * @inheritdoc
Qiang Xue committed
100 101 102
	 */
	public function stickyAttributes()
	{
Alexander Makarov committed
103
		return ['baseControllerClass', 'moduleID', 'indexWidgetType'];
Qiang Xue committed
104 105 106 107 108 109 110 111 112 113
	}

	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
114 115
	}

Qiang Xue committed
116 117 118 119 120 121 122 123 124 125
	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.");
			}
		}
	}

126
	/**
Qiang Xue committed
127
	 * @inheritdoc
128 129 130
	 */
	public function generate()
	{
Qiang Xue committed
131 132
		$controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php');
		$searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php'));
Alexander Makarov committed
133
		$files = [
Qiang Xue committed
134 135
			new CodeFile($controllerFile, $this->render('controller.php')),
			new CodeFile($searchModel, $this->render('search.php')),
Alexander Makarov committed
136
		];
Qiang Xue committed
137

Qiang Xue committed
138
		$viewPath = $this->getViewPath();
Qiang Xue committed
139 140 141 142
		$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
143 144 145
			}
		}

Qiang Xue committed
146

Qiang Xue committed
147
		return $files;
148
	}
Qiang Xue committed
149 150 151 152 153 154

	/**
	 * @return string the controller ID (without the module ID prefix)
	 */
	public function getControllerID()
	{
Qiang Xue committed
155 156 157
		$pos = strrpos($this->controllerClass, '\\');
		$class = substr(substr($this->controllerClass, $pos + 1), 0, -10);
		return Inflector::camel2id($class);
Qiang Xue committed
158 159 160 161 162 163 164
	}

	/**
	 * @return string the action view file path
	 */
	public function getViewPath()
	{
Qiang Xue committed
165
		$module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID);
Qiang Xue committed
166 167
		return $module->getViewPath() . '/' . $this->getControllerID() ;
	}
Qiang Xue committed
168 169 170

	public function getNameAttribute()
	{
171
		foreach ($this->getColumnNames() as $name) {
Qiang Xue committed
172 173 174 175
			if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) {
				return $name;
			}
		}
176 177
		/** @var \yii\db\ActiveRecord $class */
		$class = $this->modelClass;
Qiang Xue committed
178 179 180
		$pk = $class::primaryKey();
		return $pk[0];
	}
Qiang Xue committed
181 182 183 184 185

	/**
	 * @param string $attribute
	 * @return string
	 */
Qiang Xue committed
186
	public function generateActiveField($attribute)
Qiang Xue committed
187
	{
Qiang Xue committed
188
		$tableSchema = $this->getTableSchema();
189 190
		if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) {
			if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) {
Carsten Brandt committed
191
				return "\$form->field(\$model, '$attribute')->passwordInput()";
192
			} else {
Carsten Brandt committed
193
				return "\$form->field(\$model, '$attribute')";
194
			}
Qiang Xue committed
195 196 197
		}
		$column = $tableSchema->columns[$attribute];
		if ($column->phpType === 'boolean') {
198
			return "\$form->field(\$model, '$attribute')->checkbox()";
Qiang Xue committed
199
		} elseif ($column->type === 'text') {
200
			return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])";
Qiang Xue committed
201 202 203 204 205 206 207
		} else {
			if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) {
				$input = 'passwordInput';
			} else {
				$input = 'textInput';
			}
			if ($column->phpType !== 'string' || $column->size === null) {
208
				return "\$form->field(\$model, '$attribute')->$input()";
Qiang Xue committed
209
			} else {
210
				return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])";
Qiang Xue committed
211 212 213 214
			}
		}
	}

Qiang Xue committed
215 216 217 218 219 220 221
	/**
	 * @param string $attribute
	 * @return string
	 */
	public function generateActiveSearchField($attribute)
	{
		$tableSchema = $this->getTableSchema();
222 223 224
		if ($tableSchema === false) {
			return "\$form->field(\$model, '$attribute')";
		}
Qiang Xue committed
225 226
		$column = $tableSchema->columns[$attribute];
		if ($column->phpType === 'boolean') {
227
			return "\$form->field(\$model, '$attribute')->checkbox()";
Qiang Xue committed
228
		} else {
229
			return "\$form->field(\$model, '$attribute')";
Qiang Xue committed
230 231 232
		}
	}

Qiang Xue committed
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
	/**
	 * @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
253 254 255 256 257 258 259

	/**
	 * Generates validation rules for the search model.
	 * @return array the generated validation rules
	 */
	public function generateSearchRules()
	{
260 261 262
		if (($table = $this->getTableSchema()) === false) {
			return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"];
		}
Alexander Makarov committed
263
		$types = [];
Qiang Xue committed
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
		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
289
		$rules = [];
Qiang Xue committed
290
		foreach ($types as $type => $columns) {
KiTE committed
291
			$rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
Qiang Xue committed
292 293 294 295 296 297 298
		}

		return $rules;
	}

	public function getSearchAttributes()
	{
299
		return $this->getColumnNames();
Qiang Xue committed
300 301 302 303 304 305 306 307
	}

	/**
	 * Generates the attribute labels for the search model.
	 * @return array the generated attribute labels (name => label)
	 */
	public function generateSearchLabels()
	{
Alexander Makarov committed
308
		$labels = [];
309 310 311
		foreach ($this->getColumnNames() as $name) {
			if (!strcasecmp($name, 'id')) {
				$labels[$name] = 'ID';
Qiang Xue committed
312
			} else {
313
				$label = Inflector::camel2words($name);
Qiang Xue committed
314 315 316
				if (strcasecmp(substr($label, -3), ' id') === 0) {
					$label = substr($label, 0, -3) . ' ID';
				}
317
				$labels[$name] = $label;
Qiang Xue committed
318 319 320 321 322 323 324
			}
		}
		return $labels;
	}

	public function generateSearchConditions()
	{
325 326 327 328 329 330 331 332 333 334 335 336
		$columns = [];
		if (($table = $this->getTableSchema()) === false) {
			$class = $this->modelClass;
			$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
337
		$conditions = [];
338 339
		foreach ($columns as $column => $type) {
			switch ($type) {
Qiang Xue committed
340 341 342 343 344 345 346 347 348 349 350
				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:
351
					$conditions[] = "\$this->addCondition(\$query, '{$column}');";
Qiang Xue committed
352 353
					break;
				default:
354
					$conditions[] = "\$this->addCondition(\$query, '{$column}', true);";
Qiang Xue committed
355 356 357 358 359 360 361 362 363
					break;
			}
		}

		return $conditions;
	}

	public function generateUrlParams()
	{
364 365 366
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
Qiang Xue committed
367 368 369
		if (count($pks) === 1) {
			return "'id' => \$model->{$pks[0]}";
		} else {
Alexander Makarov committed
370
			$params = [];
Qiang Xue committed
371 372 373 374 375 376 377 378 379
			foreach ($pks as $pk) {
				$params[] = "'$pk' => \$model->$pk";
			}
			return implode(', ', $params);
		}
	}

	public function generateActionParams()
	{
380 381 382
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
Qiang Xue committed
383 384 385 386 387 388 389 390 391
		if (count($pks) === 1) {
			return '$id';
		} else {
			return '$' . implode(', $', $pks);
		}
	}

	public function generateActionParamComments()
	{
392 393 394
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		$pks = $class::primaryKey();
395 396 397 398 399 400 401
		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
402
		if (count($pks) === 1) {
Alexander Makarov committed
403
			return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id'];
Qiang Xue committed
404
		} else {
Alexander Makarov committed
405
			$params = [];
Qiang Xue committed
406 407 408 409 410 411 412 413 414 415 416
			foreach ($pks as $pk) {
				$params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk;
			}
			return $params;
		}
	}

	public function getTableSchema()
	{
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
417 418 419 420 421
		if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
			return $class::getTableSchema();
		} else {
			return false;
		}
Qiang Xue committed
422
	}
423 424 425 426 427 428 429 430 431 432 433 434 435

	public function getColumnNames()
	{
		/** @var ActiveRecord $class */
		$class = $this->modelClass;
		if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
			return $class::getTableSchema()->getColumnNames();
		} else {
			$model = new $class();
			return $model->attributes();
		}
	}

Qiang Xue committed
436
}