* @since 2.0 */ class Generator extends \yii\gii\Generator { public $modelClass; public $moduleID; public $controllerClass; public $baseControllerClass = 'yii\web\Controller'; public $indexWidgetType = 'grid'; public $searchModelClass = ''; /** * @inheritdoc */ public function getName() { return 'CRUD Generator'; } /** * @inheritdoc */ public function getDescription() { return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete) operations for the specified data model.'; } /** * @inheritdoc */ public function rules() { return array_merge(parent::rules(), [ [['moduleID', 'controllerClass', 'modelClass', 'searchModelClass', 'baseControllerClass'], 'filter', 'filter' => 'trim'], [['modelClass', '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.'], [['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]], [['baseControllerClass'], 'validateClass', 'params' => ['extends' => Controller::className()]], [['controllerClass'], 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'], [['controllerClass'], 'match', 'pattern' => '/(^|\\\\)[A-Z][^\\\\]+Controller$/', 'message' => 'Controller class name must start with an uppercase letter.'], [['controllerClass', 'searchModelClass'], 'validateNewClass'], [['indexWidgetType'], 'in', 'range' => ['grid', 'list']], [['modelClass'], 'validateModelClass'], [['moduleID'], 'validateModuleID'], [['enableI18N'], 'boolean'], [['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false], ]); } /** * @inheritdoc */ public function attributeLabels() { return array_merge(parent::attributeLabels(), [ 'modelClass' => 'Model Class', 'moduleID' => 'Module ID', 'controllerClass' => 'Controller Class', 'baseControllerClass' => 'Base Controller Class', 'indexWidgetType' => 'Widget Used in Index Page', 'searchModelClass' => 'Search Model Class', ]); } /** * @inheritdoc */ public function hints() { return array_merge(parent::hints(), [ '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., app\models\Post.', 'controllerClass' => 'This is the name of the controller class to be generated. You should provide a fully qualified namespaced class (e.g. app\controllers\PostController), and class name should be in CamelCase with an uppercase first letter. Make sure the class is using the same namespace as specified by your application\'s controllerNamespace property.', 'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from. You should provide a fully qualified class name, e.g., yii\web\Controller.', '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.', 'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models. You may choose either GridView or ListView', 'searchModelClass' => 'This is the name of the search model class to be generated. You should provide a fully qualified namespaced class name, e.g., app\models\PostSearch.', ]); } /** * @inheritdoc */ public function requiredTemplates() { return ['controller.php']; } /** * @inheritdoc */ public function stickyAttributes() { return array_merge(parent::stickyAttributes(), ['baseControllerClass', 'moduleID', 'indexWidgetType']); } /** * Checks if model class is valid */ public function validateModelClass() { /* @var $class ActiveRecord */ $class = $this->modelClass; $pk = $class::primaryKey(); if (empty($pk)) { $this->addError('modelClass', "The table associated with $class must have primary key(s)."); } } /** * Checks if model ID is valid */ 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."); } } } /** * @inheritdoc */ public function generate() { $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php'); $files = [ new CodeFile($controllerFile, $this->render('controller.php')), ]; if (!empty($this->searchModelClass)) { $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php')); $files[] = new CodeFile($searchModel, $this->render('search.php')); } $viewPath = $this->getViewPath(); $templatePath = $this->getTemplatePath() . '/views'; foreach (scandir($templatePath) as $file) { if (empty($this->searchModelClass) && $file === '_search.php') { continue; } if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') { $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file")); } } return $files; } /** * @return string the controller ID (without the module ID prefix) */ public function getControllerID() { $pos = strrpos($this->controllerClass, '\\'); $class = substr(substr($this->controllerClass, $pos + 1), 0, -10); return Inflector::camel2id($class); } /** * @return string the action view file path */ public function getViewPath() { $module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID); return $module->getViewPath() . '/' . $this->getControllerID() ; } public function getNameAttribute() { foreach ($this->getColumnNames() as $name) { if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) { return $name; } } /* @var $class \yii\db\ActiveRecord */ $class = $this->modelClass; $pk = $class::primaryKey(); return $pk[0]; } /** * Generates code for active field * @param string $attribute * @return string */ public function generateActiveField($attribute) { $tableSchema = $this->getTableSchema(); if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) { if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) { return "\$form->field(\$model, '$attribute')->passwordInput()"; } else { return "\$form->field(\$model, '$attribute')"; } } $column = $tableSchema->columns[$attribute]; if ($column->phpType === 'boolean') { return "\$form->field(\$model, '$attribute')->checkbox()"; } elseif ($column->type === 'text') { return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])"; } else { if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) { $input = 'passwordInput'; } else { $input = 'textInput'; } if (is_array($column->enumValues) && count($column->enumValues) > 0) { $dropDownOptions = []; foreach ($column->enumValues as $enumValue) { $dropDownOptions[$enumValue] = Inflector::humanize($enumValue); } return "\$form->field(\$model, '$attribute')->dropDownList(" . preg_replace("/\n\s*/", ' ', VarDumper::export($dropDownOptions)).", ['prompt' => ''])"; } elseif ($column->phpType !== 'string' || $column->size === null) { return "\$form->field(\$model, '$attribute')->$input()"; } else { return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])"; } } } /** * Generates code for active search field * @param string $attribute * @return string */ public function generateActiveSearchField($attribute) { $tableSchema = $this->getTableSchema(); if ($tableSchema === false) { return "\$form->field(\$model, '$attribute')"; } $column = $tableSchema->columns[$attribute]; if ($column->phpType === 'boolean') { return "\$form->field(\$model, '$attribute')->checkbox()"; } else { return "\$form->field(\$model, '$attribute')"; } } /** * Generates column format * @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'; } } /** * Generates validation rules for the search model. * @return array the generated validation rules */ public function generateSearchRules() { if (($table = $this->getTableSchema()) === false) { return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"]; } $types = []; 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; } } $rules = []; foreach ($types as $type => $columns) { $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; } return $rules; } /** * @return array searchable attributes */ public function getSearchAttributes() { return $this->getColumnNames(); } /** * Generates the attribute labels for the search model. * @return array the generated attribute labels (name => label) */ public function generateSearchLabels() { /* @var $model \yii\base\Model */ $model = new $this->modelClass(); $attributeLabels = $model->attributeLabels(); $labels = []; foreach ($this->getColumnNames() as $name) { if (isset($attributeLabels[$name])) { $labels[$name] = $attributeLabels[$name]; } else { if (!strcasecmp($name, 'id')) { $labels[$name] = 'ID'; } else { $label = Inflector::camel2words($name); if (!empty($label) && substr_compare($label, ' id', -3, 3, true) === 0) { $label = substr($label, 0, -3) . ' ID'; } $labels[$name] = $label; } } } return $labels; } /** * Generates search conditions * @return array */ public function generateSearchConditions() { $columns = []; if (($table = $this->getTableSchema()) === false) { $class = $this->modelClass; /* @var $model \yii\base\Model */ $model = new $class(); foreach ($model->attributes() as $attribute) { $columns[$attribute] = 'unknown'; } } else { foreach ($table->columns as $column) { $columns[$column->name] = $column->type; } } $likeConditions = []; $hashConditions = []; foreach ($columns as $column => $type) { switch ($type) { 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: $hashConditions[] = "'{$column}' => \$this->{$column},"; break; default: $likeConditions[] = "->andFilterWhere(['like', '{$column}', \$this->{$column}])"; break; } } $conditions = []; if (!empty($hashConditions)) { $conditions[] = "\$query->andFilterWhere([\n" . str_repeat(' ', 12) . implode("\n" . str_repeat(' ', 12), $hashConditions) . "\n" . str_repeat(' ', 8) . "]);\n"; } if (!empty($likeConditions)) { $conditions[] = "\$query" . implode("\n" . str_repeat(' ', 12), $likeConditions) . ";\n"; } return $conditions; } /** * Generates URL parameters * @return string */ public function generateUrlParams() { /* @var $class ActiveRecord */ $class = $this->modelClass; $pks = $class::primaryKey(); if (count($pks) === 1) { if (is_subclass_of($class, 'yii\mongodb\ActiveRecord')) { return "'id' => (string)\$model->{$pks[0]}"; } else { return "'id' => \$model->{$pks[0]}"; } } else { $params = []; foreach ($pks as $pk) { if (is_subclass_of($class, 'yii\mongodb\ActiveRecord')) { $params[] = "'$pk' => (string)\$model->$pk"; } else { $params[] = "'$pk' => \$model->$pk"; } } return implode(', ', $params); } } /** * Generates action parameters * @return string */ public function generateActionParams() { /* @var $class ActiveRecord */ $class = $this->modelClass; $pks = $class::primaryKey(); if (count($pks) === 1) { return '$id'; } else { return '$' . implode(', $', $pks); } } /** * Generates parameter tags for phpdoc * @return array parameter tags for phpdoc */ public function generateActionParamComments() { /* @var $class ActiveRecord */ $class = $this->modelClass; $pks = $class::primaryKey(); if (($table = $this->getTableSchema()) === false) { $params = []; foreach ($pks as $pk) { $params[] = '@param ' . (substr(strtolower($pk), -2) == 'id' ? 'integer' : 'string') . ' $' . $pk; } return $params; } if (count($pks) === 1) { return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id']; } else { $params = []; foreach ($pks as $pk) { $params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk; } return $params; } } /** * Returns table schema for current model class or false if it is not an active record * @return boolean|\yii\db\TableSchema */ public function getTableSchema() { /* @var $class ActiveRecord */ $class = $this->modelClass; if (is_subclass_of($class, 'yii\db\ActiveRecord')) { return $class::getTableSchema(); } else { return false; } } /** * @return array model column names */ public function getColumnNames() { /* @var $class ActiveRecord */ $class = $this->modelClass; if (is_subclass_of($class, 'yii\db\ActiveRecord')) { return $class::getTableSchema()->getColumnNames(); } else { /* @var $model \yii\base\Model */ $model = new $class(); return $model->attributes(); } } }