You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

543 lines
19KB

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