Generator.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\gii\generators\crud;
  8. use Yii;
  9. use yii\db\ActiveRecord;
  10. use yii\db\BaseActiveRecord;
  11. use yii\db\Schema;
  12. use yii\gii\CodeFile;
  13. use yii\helpers\Inflector;
  14. use yii\web\Controller;
  15. /**
  16. * Generates CRUD
  17. *
  18. * @property array $columnNames Model column names. This property is read-only.
  19. * @property string $controllerID The controller ID (without the module ID prefix). This property is
  20. * read-only.
  21. * @property array $searchAttributes Searchable attributes. This property is read-only.
  22. * @property boolean|\yii\db\TableSchema $tableSchema This property is read-only.
  23. * @property string $viewPath The action view file path. This property is read-only.
  24. *
  25. * @author Qiang Xue <qiang.xue@gmail.com>
  26. * @since 2.0
  27. */
  28. class Generator extends \yii\gii\Generator
  29. {
  30. public $modelClass;
  31. public $moduleID;
  32. public $controllerClass;
  33. public $baseControllerClass = 'yii\web\Controller';
  34. public $indexWidgetType = 'grid';
  35. public $searchModelClass;
  36. /**
  37. * @inheritdoc
  38. */
  39. public function getName()
  40. {
  41. return 'CRUD Generator';
  42. }
  43. /**
  44. * @inheritdoc
  45. */
  46. public function getDescription()
  47. {
  48. return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete)
  49. operations for the specified data model.';
  50. }
  51. /**
  52. * @inheritdoc
  53. */
  54. public function rules()
  55. {
  56. return array_merge(parent::rules(), [
  57. [['moduleID', 'controllerClass', 'modelClass', 'searchModelClass', 'baseControllerClass'], 'filter', 'filter' => 'trim'],
  58. [['modelClass', 'searchModelClass', 'controllerClass', 'baseControllerClass', 'indexWidgetType'], 'required'],
  59. [['searchModelClass'], 'compare', 'compareAttribute' => 'modelClass', 'operator' => '!==', 'message' => 'Search Model Class must not be equal to Model Class.'],
  60. [['modelClass', 'controllerClass', 'baseControllerClass', 'searchModelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'],
  61. [['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]],
  62. [['baseControllerClass'], 'validateClass', 'params' => ['extends' => Controller::className()]],
  63. [['controllerClass'], 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'],
  64. [['controllerClass', 'searchModelClass'], 'validateNewClass'],
  65. [['indexWidgetType'], 'in', 'range' => ['grid', 'list']],
  66. [['modelClass'], 'validateModelClass'],
  67. [['moduleID'], 'validateModuleID'],
  68. [['enableI18N'], 'boolean'],
  69. [['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false],
  70. ]);
  71. }
  72. /**
  73. * @inheritdoc
  74. */
  75. public function attributeLabels()
  76. {
  77. return array_merge(parent::attributeLabels(), [
  78. 'modelClass' => 'Model Class',
  79. 'moduleID' => 'Module ID',
  80. 'controllerClass' => 'Controller Class',
  81. 'baseControllerClass' => 'Base Controller Class',
  82. 'indexWidgetType' => 'Widget Used in Index Page',
  83. 'searchModelClass' => 'Search Model Class',
  84. ]);
  85. }
  86. /**
  87. * @inheritdoc
  88. */
  89. public function hints()
  90. {
  91. return array_merge(parent::hints(), [
  92. 'modelClass' => 'This is the ActiveRecord class associated with the table that CRUD will be built upon.
  93. You should provide a fully qualified class name, e.g., <code>app\models\Post</code>.',
  94. 'controllerClass' => 'This is the name of the controller class to be generated. You should
  95. provide a fully qualified namespaced class, .e.g, <code>app\controllers\PostController</code>.',
  96. 'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from.
  97. You should provide a fully qualified class name, e.g., <code>yii\web\Controller</code>.',
  98. 'moduleID' => 'This is the ID of the module that the generated controller will belong to.
  99. If not set, it means the controller will belong to the application.',
  100. 'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models.
  101. You may choose either <code>GridView</code> or <code>ListView</code>',
  102. 'searchModelClass' => 'This is the name of the search model class to be generated. You should provide a fully
  103. qualified namespaced class name, e.g., <code>app\models\search\PostSearch</code>.',
  104. ]);
  105. }
  106. /**
  107. * @inheritdoc
  108. */
  109. public function requiredTemplates()
  110. {
  111. return ['controller.php'];
  112. }
  113. /**
  114. * @inheritdoc
  115. */
  116. public function stickyAttributes()
  117. {
  118. return array_merge(parent::stickyAttributes(), ['baseControllerClass', 'moduleID', 'indexWidgetType']);
  119. }
  120. /**
  121. * Checks if model class is valid
  122. */
  123. public function validateModelClass()
  124. {
  125. /** @var ActiveRecord $class */
  126. $class = $this->modelClass;
  127. $pk = $class::primaryKey();
  128. if (empty($pk)) {
  129. $this->addError('modelClass', "The table associated with $class must have primary key(s).");
  130. }
  131. }
  132. /**
  133. * Checks if model ID is valid
  134. */
  135. public function validateModuleID()
  136. {
  137. if (!empty($this->moduleID)) {
  138. $module = Yii::$app->getModule($this->moduleID);
  139. if ($module === null) {
  140. $this->addError('moduleID', "Module '{$this->moduleID}' does not exist.");
  141. }
  142. }
  143. }
  144. /**
  145. * @inheritdoc
  146. */
  147. public function generate()
  148. {
  149. $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php');
  150. $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php'));
  151. $files = [
  152. new CodeFile($controllerFile, $this->render('controller.php')),
  153. new CodeFile($searchModel, $this->render('search.php')),
  154. ];
  155. $viewPath = $this->getViewPath();
  156. $templatePath = $this->getTemplatePath() . '/views';
  157. foreach (scandir($templatePath) as $file) {
  158. if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
  159. $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file"));
  160. }
  161. }
  162. return $files;
  163. }
  164. /**
  165. * @return string the controller ID (without the module ID prefix)
  166. */
  167. public function getControllerID()
  168. {
  169. $pos = strrpos($this->controllerClass, '\\');
  170. $class = substr(substr($this->controllerClass, $pos + 1), 0, -10);
  171. return Inflector::camel2id($class);
  172. }
  173. /**
  174. * @return string the action view file path
  175. */
  176. public function getViewPath()
  177. {
  178. $module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID);
  179. return $module->getViewPath() . '/' . $this->getControllerID() ;
  180. }
  181. public function getNameAttribute()
  182. {
  183. foreach ($this->getColumnNames() as $name) {
  184. if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) {
  185. return $name;
  186. }
  187. }
  188. /** @var \yii\db\ActiveRecord $class */
  189. $class = $this->modelClass;
  190. $pk = $class::primaryKey();
  191. return $pk[0];
  192. }
  193. /**
  194. * Generates code for active field
  195. * @param string $attribute
  196. * @return string
  197. */
  198. public function generateActiveField($attribute)
  199. {
  200. $tableSchema = $this->getTableSchema();
  201. if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) {
  202. if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) {
  203. return "\$form->field(\$model, '$attribute')->passwordInput()";
  204. } else {
  205. return "\$form->field(\$model, '$attribute')";
  206. }
  207. }
  208. $column = $tableSchema->columns[$attribute];
  209. if ($column->phpType === 'boolean') {
  210. return "\$form->field(\$model, '$attribute')->checkbox()";
  211. } elseif ($column->type === 'text') {
  212. return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])";
  213. } else {
  214. if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) {
  215. $input = 'passwordInput';
  216. } else {
  217. $input = 'textInput';
  218. }
  219. if ($column->phpType !== 'string' || $column->size === null) {
  220. return "\$form->field(\$model, '$attribute')->$input()";
  221. } else {
  222. return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])";
  223. }
  224. }
  225. }
  226. /**
  227. * Generates code for active search field
  228. * @param string $attribute
  229. * @return string
  230. */
  231. public function generateActiveSearchField($attribute)
  232. {
  233. $tableSchema = $this->getTableSchema();
  234. if ($tableSchema === false) {
  235. return "\$form->field(\$model, '$attribute')";
  236. }
  237. $column = $tableSchema->columns[$attribute];
  238. if ($column->phpType === 'boolean') {
  239. return "\$form->field(\$model, '$attribute')->checkbox()";
  240. } else {
  241. return "\$form->field(\$model, '$attribute')";
  242. }
  243. }
  244. /**
  245. * Generates column format
  246. * @param \yii\db\ColumnSchema $column
  247. * @return string
  248. */
  249. public function generateColumnFormat($column)
  250. {
  251. if ($column->phpType === 'boolean') {
  252. return 'boolean';
  253. } elseif ($column->type === 'text') {
  254. return 'ntext';
  255. } elseif (stripos($column->name, 'time') !== false && $column->phpType === 'integer') {
  256. return 'datetime';
  257. } elseif (stripos($column->name, 'email') !== false) {
  258. return 'email';
  259. } elseif (stripos($column->name, 'url') !== false) {
  260. return 'url';
  261. } else {
  262. return 'text';
  263. }
  264. }
  265. /**
  266. * Generates validation rules for the search model.
  267. * @return array the generated validation rules
  268. */
  269. public function generateSearchRules()
  270. {
  271. if (($table = $this->getTableSchema()) === false) {
  272. return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"];
  273. }
  274. $types = [];
  275. foreach ($table->columns as $column) {
  276. switch ($column->type) {
  277. case Schema::TYPE_SMALLINT:
  278. case Schema::TYPE_INTEGER:
  279. case Schema::TYPE_BIGINT:
  280. $types['integer'][] = $column->name;
  281. break;
  282. case Schema::TYPE_BOOLEAN:
  283. $types['boolean'][] = $column->name;
  284. break;
  285. case Schema::TYPE_FLOAT:
  286. case Schema::TYPE_DECIMAL:
  287. case Schema::TYPE_MONEY:
  288. $types['number'][] = $column->name;
  289. break;
  290. case Schema::TYPE_DATE:
  291. case Schema::TYPE_TIME:
  292. case Schema::TYPE_DATETIME:
  293. case Schema::TYPE_TIMESTAMP:
  294. default:
  295. $types['safe'][] = $column->name;
  296. break;
  297. }
  298. }
  299. $rules = [];
  300. foreach ($types as $type => $columns) {
  301. $rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
  302. }
  303. return $rules;
  304. }
  305. /**
  306. * @return array searchable attributes
  307. */
  308. public function getSearchAttributes()
  309. {
  310. return $this->getColumnNames();
  311. }
  312. /**
  313. * Generates the attribute labels for the search model.
  314. * @return array the generated attribute labels (name => label)
  315. */
  316. public function generateSearchLabels()
  317. {
  318. /** @var \yii\base\Model $model */
  319. $model = new $this->modelClass();
  320. $attributeLabels = $model->attributeLabels();
  321. $labels = [];
  322. foreach ($this->getColumnNames() as $name) {
  323. if (isset($attributeLabels[$name])) {
  324. $labels[$name] = $attributeLabels[$name];
  325. } else {
  326. if (!strcasecmp($name, 'id')) {
  327. $labels[$name] = 'ID';
  328. } else {
  329. $label = Inflector::camel2words($name);
  330. if (strcasecmp(substr($label, -3), ' id') === 0) {
  331. $label = substr($label, 0, -3) . ' ID';
  332. }
  333. $labels[$name] = $label;
  334. }
  335. }
  336. }
  337. return $labels;
  338. }
  339. /**
  340. * Generates search conditions
  341. * @return array
  342. */
  343. public function generateSearchConditions()
  344. {
  345. $columns = [];
  346. if (($table = $this->getTableSchema()) === false) {
  347. $class = $this->modelClass;
  348. /** @var \yii\base\Model $model */
  349. $model = new $class();
  350. foreach ($model->attributes() as $attribute) {
  351. $columns[$attribute] = 'unknown';
  352. }
  353. } else {
  354. foreach ($table->columns as $column) {
  355. $columns[$column->name] = $column->type;
  356. }
  357. }
  358. $likeConditions = [];
  359. $hashConditions = [];
  360. foreach ($columns as $column => $type) {
  361. switch ($type) {
  362. case Schema::TYPE_SMALLINT:
  363. case Schema::TYPE_INTEGER:
  364. case Schema::TYPE_BIGINT:
  365. case Schema::TYPE_BOOLEAN:
  366. case Schema::TYPE_FLOAT:
  367. case Schema::TYPE_DECIMAL:
  368. case Schema::TYPE_MONEY:
  369. case Schema::TYPE_DATE:
  370. case Schema::TYPE_TIME:
  371. case Schema::TYPE_DATETIME:
  372. case Schema::TYPE_TIMESTAMP:
  373. $hashConditions[] = "'{$column}' => \$this->{$column},";
  374. break;
  375. default:
  376. $likeConditions[] = "->andFilterWhere(['like', '{$column}', \$this->{$column}])";
  377. break;
  378. }
  379. }
  380. $conditions = [];
  381. if (!empty($hashConditions)) {
  382. $conditions[] = "\$query->andFilterWhere([\n"
  383. . str_repeat(' ', 12) . implode("\n" . str_repeat(' ', 12), $hashConditions)
  384. . "\n" . str_repeat(' ', 8) . "]);\n";
  385. }
  386. if (!empty($likeConditions)) {
  387. $conditions[] = "\$query" . implode("\n" . str_repeat(' ', 12), $likeConditions) . ";\n";
  388. }
  389. return $conditions;
  390. }
  391. /**
  392. * Generates URL parameters
  393. * @return string
  394. */
  395. public function generateUrlParams()
  396. {
  397. /** @var ActiveRecord $class */
  398. $class = $this->modelClass;
  399. $pks = $class::primaryKey();
  400. if (count($pks) === 1) {
  401. return "'id' => \$model->{$pks[0]}";
  402. } else {
  403. $params = [];
  404. foreach ($pks as $pk) {
  405. $params[] = "'$pk' => \$model->$pk";
  406. }
  407. return implode(', ', $params);
  408. }
  409. }
  410. /**
  411. * Generates action parameters
  412. * @return string
  413. */
  414. public function generateActionParams()
  415. {
  416. /** @var ActiveRecord $class */
  417. $class = $this->modelClass;
  418. $pks = $class::primaryKey();
  419. if (count($pks) === 1) {
  420. return '$id';
  421. } else {
  422. return '$' . implode(', $', $pks);
  423. }
  424. }
  425. /**
  426. * Generates parameter tags for phpdoc
  427. * @return array parameter tags for phpdoc
  428. */
  429. public function generateActionParamComments()
  430. {
  431. /** @var ActiveRecord $class */
  432. $class = $this->modelClass;
  433. $pks = $class::primaryKey();
  434. if (($table = $this->getTableSchema()) === false) {
  435. $params = [];
  436. foreach ($pks as $pk) {
  437. $params[] = '@param ' . (substr(strtolower($pk), -2) == 'id' ? 'integer' : 'string') . ' $' . $pk;
  438. }
  439. return $params;
  440. }
  441. if (count($pks) === 1) {
  442. return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id'];
  443. } else {
  444. $params = [];
  445. foreach ($pks as $pk) {
  446. $params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk;
  447. }
  448. return $params;
  449. }
  450. }
  451. /**
  452. * Returns table schema for current model class or false if it is not an active record
  453. * @return boolean|\yii\db\TableSchema
  454. */
  455. public function getTableSchema()
  456. {
  457. /** @var ActiveRecord $class */
  458. $class = $this->modelClass;
  459. if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
  460. return $class::getTableSchema();
  461. } else {
  462. return false;
  463. }
  464. }
  465. /**
  466. * @return array model column names
  467. */
  468. public function getColumnNames()
  469. {
  470. /** @var ActiveRecord $class */
  471. $class = $this->modelClass;
  472. if (is_subclass_of($class, 'yii\db\ActiveRecord')) {
  473. return $class::getTableSchema()->getColumnNames();
  474. } else {
  475. /** @var \yii\base\Model $model */
  476. $model = new $class();
  477. return $model->attributes();
  478. }
  479. }
  480. }