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.

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