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.

522 line
17KB

  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;
  8. use Yii;
  9. use ReflectionClass;
  10. use yii\base\InvalidConfigException;
  11. use yii\base\Model;
  12. use yii\helpers\VarDumper;
  13. use yii\web\View;
  14. /**
  15. * This is the base class for all generator classes.
  16. *
  17. * A generator instance is responsible for taking user inputs, validating them,
  18. * and using them to generate the corresponding code based on a set of code template files.
  19. *
  20. * A generator class typically needs to implement the following methods:
  21. *
  22. * - [[getName()]]: returns the name of the generator
  23. * - [[getDescription()]]: returns the detailed description of the generator
  24. * - [[generate()]]: generates the code based on the current user input and the specified code template files.
  25. * This is the place where main code generation code resides.
  26. *
  27. * @property string $description The detailed description of the generator. This property is read-only.
  28. * @property string $stickyDataFile The file path that stores the sticky attribute values. This property is
  29. * read-only.
  30. * @property string $templatePath The root path of the template files that are currently being used. This
  31. * property is read-only.
  32. *
  33. * @author Qiang Xue <qiang.xue@gmail.com>
  34. * @since 2.0
  35. */
  36. abstract class Generator extends Model
  37. {
  38. /**
  39. * @var array a list of available code templates. The array keys are the template names,
  40. * and the array values are the corresponding template paths or path aliases.
  41. */
  42. public $templates = [];
  43. /**
  44. * @var string the name of the code template that the user has selected.
  45. * The value of this property is internally managed by this class.
  46. */
  47. public $template = 'default';
  48. /**
  49. * @var boolean whether the strings will be generated using `Yii::t()` or normal strings.
  50. */
  51. public $enableI18N = false;
  52. /**
  53. * @var string the message category used by `Yii::t()` when `$enableI18N` is `true`.
  54. * Defaults to `app`.
  55. */
  56. public $messageCategory = 'app';
  57. /**
  58. * @return string name of the code generator
  59. */
  60. abstract public function getName();
  61. /**
  62. * Generates the code based on the current user input and the specified code template files.
  63. * This is the main method that child classes should implement.
  64. * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example
  65. * on how to implement this method.
  66. * @return CodeFile[] a list of code files to be created.
  67. */
  68. abstract public function generate();
  69. /**
  70. * @inheritdoc
  71. */
  72. public function init()
  73. {
  74. parent::init();
  75. if (!isset($this->templates['default'])) {
  76. $this->templates['default'] = $this->defaultTemplate();
  77. }
  78. foreach ($this->templates as $i => $template) {
  79. $this->templates[$i] = Yii::getAlias($template);
  80. }
  81. }
  82. /**
  83. * @inheritdoc
  84. */
  85. public function attributeLabels()
  86. {
  87. return [
  88. 'enableI18N' => 'Enable I18N',
  89. 'messageCategory' => 'Message Category',
  90. ];
  91. }
  92. /**
  93. * Returns a list of code template files that are required.
  94. * Derived classes usually should override this method if they require the existence of
  95. * certain template files.
  96. * @return array list of code template files that are required. They should be file paths
  97. * relative to [[templatePath]].
  98. */
  99. public function requiredTemplates()
  100. {
  101. return [];
  102. }
  103. /**
  104. * Returns the list of sticky attributes.
  105. * A sticky attribute will remember its value and will initialize the attribute with this value
  106. * when the generator is restarted.
  107. * @return array list of sticky attributes
  108. */
  109. public function stickyAttributes()
  110. {
  111. return ['template', 'enableI18N', 'messageCategory'];
  112. }
  113. /**
  114. * Returns the list of hint messages.
  115. * The array keys are the attribute names, and the array values are the corresponding hint messages.
  116. * Hint messages will be displayed to end users when they are filling the form for the generator.
  117. * @return array the list of hint messages
  118. */
  119. public function hints()
  120. {
  121. return [
  122. 'enableI18N' => 'This indicates whether the generator should generate strings using <code>Yii::t()</code> method.
  123. Set this to <code>true</code> if you are planning to make your application translatable.',
  124. 'messageCategory' => 'This is the category used by <code>Yii::t()</code> in case you enable I18N.',
  125. ];
  126. }
  127. /**
  128. * Returns the list of auto complete values.
  129. * The array keys are the attribute names, and the array values are the corresponding auto complete values.
  130. * Auto complete values can also be callable typed in order one want to make postponed data generation.
  131. * @return array the list of auto complete values
  132. */
  133. public function autoCompleteData()
  134. {
  135. return [];
  136. }
  137. /**
  138. * Returns the message to be displayed when the newly generated code is saved successfully.
  139. * Child classes may override this method to customize the message.
  140. * @return string the message to be displayed when the newly generated code is saved successfully.
  141. */
  142. public function successMessage()
  143. {
  144. return 'The code has been generated successfully.';
  145. }
  146. /**
  147. * Returns the view file for the input form of the generator.
  148. * The default implementation will return the "form.php" file under the directory
  149. * that contains the generator class file.
  150. * @return string the view file for the input form of the generator.
  151. */
  152. public function formView()
  153. {
  154. $class = new ReflectionClass($this);
  155. return dirname($class->getFileName()) . '/form.php';
  156. }
  157. /**
  158. * Returns the root path to the default code template files.
  159. * The default implementation will return the "templates" subdirectory of the
  160. * directory containing the generator class file.
  161. * @return string the root path to the default code template files.
  162. */
  163. public function defaultTemplate()
  164. {
  165. $class = new ReflectionClass($this);
  166. return dirname($class->getFileName()) . '/default';
  167. }
  168. /**
  169. * @return string the detailed description of the generator.
  170. */
  171. public function getDescription()
  172. {
  173. return '';
  174. }
  175. /**
  176. * @inheritdoc
  177. *
  178. * Child classes should override this method like the following so that the parent
  179. * rules are included:
  180. *
  181. * ~~~
  182. * return array_merge(parent::rules(), [
  183. * ...rules for the child class...
  184. * ]);
  185. * ~~~
  186. */
  187. public function rules()
  188. {
  189. return [
  190. [['template'], 'required', 'message' => 'A code template must be selected.'],
  191. [['template'], 'validateTemplate'],
  192. ];
  193. }
  194. /**
  195. * Loads sticky attributes from an internal file and populates them into the generator.
  196. * @internal
  197. */
  198. public function loadStickyAttributes()
  199. {
  200. $stickyAttributes = $this->stickyAttributes();
  201. $path = $this->getStickyDataFile();
  202. if (is_file($path)) {
  203. $result = json_decode(file_get_contents($path), true);
  204. if (is_array($result)) {
  205. foreach ($stickyAttributes as $name) {
  206. if (isset($result[$name])) {
  207. $this->$name = $result[$name];
  208. }
  209. }
  210. }
  211. }
  212. }
  213. /**
  214. * Saves sticky attributes into an internal file.
  215. * @internal
  216. */
  217. public function saveStickyAttributes()
  218. {
  219. $stickyAttributes = $this->stickyAttributes();
  220. $stickyAttributes[] = 'template';
  221. $values = [];
  222. foreach ($stickyAttributes as $name) {
  223. $values[$name] = $this->$name;
  224. }
  225. $path = $this->getStickyDataFile();
  226. @mkdir(dirname($path), 0755, true);
  227. file_put_contents($path, json_encode($values));
  228. }
  229. /**
  230. * @return string the file path that stores the sticky attribute values.
  231. * @internal
  232. */
  233. public function getStickyDataFile()
  234. {
  235. return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.json';
  236. }
  237. /**
  238. * Saves the generated code into files.
  239. * @param CodeFile[] $files the code files to be saved
  240. * @param array $answers
  241. * @param string $results this parameter receives a value from this method indicating the log messages
  242. * generated while saving the code files.
  243. * @return boolean whether files are successfully saved without any error.
  244. */
  245. public function save($files, $answers, &$results)
  246. {
  247. $lines = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
  248. $hasError = false;
  249. foreach ($files as $file) {
  250. $relativePath = $file->getRelativePath();
  251. if (isset($answers[$file->id]) && !empty($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) {
  252. $error = $file->save();
  253. if (is_string($error)) {
  254. $hasError = true;
  255. $lines[] = "generating $relativePath\n<span class=\"error\">$error</span>";
  256. } else {
  257. $lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath";
  258. }
  259. } else {
  260. $lines[] = " skipped $relativePath";
  261. }
  262. }
  263. $lines[] = "done!\n";
  264. $results = implode("\n", $lines);
  265. return !$hasError;
  266. }
  267. /**
  268. * @return string the root path of the template files that are currently being used.
  269. * @throws InvalidConfigException if [[template]] is invalid
  270. */
  271. public function getTemplatePath()
  272. {
  273. if (isset($this->templates[$this->template])) {
  274. return $this->templates[$this->template];
  275. } else {
  276. throw new InvalidConfigException("Unknown template: {$this->template}");
  277. }
  278. }
  279. /**
  280. * Generates code using the specified code template and parameters.
  281. * Note that the code template will be used as a PHP file.
  282. * @param string $template the code template file. This must be specified as a file path
  283. * relative to [[templatePath]].
  284. * @param array $params list of parameters to be passed to the template file.
  285. * @return string the generated code
  286. */
  287. public function render($template, $params = [])
  288. {
  289. $view = new View();
  290. $params['generator'] = $this;
  291. return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this);
  292. }
  293. /**
  294. * Validates the template selection.
  295. * This method validates whether the user selects an existing template
  296. * and the template contains all required template files as specified in [[requiredTemplates()]].
  297. */
  298. public function validateTemplate()
  299. {
  300. $templates = $this->templates;
  301. if (!isset($templates[$this->template])) {
  302. $this->addError('template', 'Invalid template selection.');
  303. } else {
  304. $templatePath = $this->templates[$this->template];
  305. foreach ($this->requiredTemplates() as $template) {
  306. if (!is_file($templatePath . '/' . $template)) {
  307. $this->addError('template', "Unable to find the required code template file '$template'.");
  308. }
  309. }
  310. }
  311. }
  312. /**
  313. * An inline validator that checks if the attribute value refers to an existing class name.
  314. * If the `extends` option is specified, it will also check if the class is a child class
  315. * of the class represented by the `extends` option.
  316. * @param string $attribute the attribute being validated
  317. * @param array $params the validation options
  318. */
  319. public function validateClass($attribute, $params)
  320. {
  321. $class = $this->$attribute;
  322. try {
  323. if (class_exists($class)) {
  324. if (isset($params['extends'])) {
  325. if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) {
  326. $this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class.");
  327. }
  328. }
  329. } else {
  330. $this->addError($attribute, "Class '$class' does not exist or has syntax error.");
  331. }
  332. } catch (\Exception $e) {
  333. $this->addError($attribute, "Class '$class' does not exist or has syntax error.");
  334. }
  335. }
  336. /**
  337. * An inline validator that checks if the attribute value refers to a valid namespaced class name.
  338. * The validator will check if the directory containing the new class file exist or not.
  339. * @param string $attribute the attribute being validated
  340. * @param array $params the validation options
  341. */
  342. public function validateNewClass($attribute, $params)
  343. {
  344. $class = ltrim($this->$attribute, '\\');
  345. if (($pos = strrpos($class, '\\')) === false) {
  346. $this->addError($attribute, "The class name must contain fully qualified namespace name.");
  347. } else {
  348. $ns = substr($class, 0, $pos);
  349. $path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false);
  350. if ($path === false) {
  351. $this->addError($attribute, "The class namespace is invalid: $ns");
  352. } elseif (!is_dir($path)) {
  353. $this->addError($attribute, "Please make sure the directory containing this class exists: $path");
  354. }
  355. }
  356. }
  357. /**
  358. * Checks if message category is not empty when I18N is enabled.
  359. */
  360. public function validateMessageCategory()
  361. {
  362. if ($this->enableI18N && empty($this->messageCategory)) {
  363. $this->addError('messageCategory', "Message Category cannot be blank.");
  364. }
  365. }
  366. /**
  367. * @param string $value the attribute to be validated
  368. * @return boolean whether the value is a reserved PHP keyword.
  369. */
  370. public function isReservedKeyword($value)
  371. {
  372. static $keywords = [
  373. '__class__',
  374. '__dir__',
  375. '__file__',
  376. '__function__',
  377. '__line__',
  378. '__method__',
  379. '__namespace__',
  380. '__trait__',
  381. 'abstract',
  382. 'and',
  383. 'array',
  384. 'as',
  385. 'break',
  386. 'case',
  387. 'catch',
  388. 'callable',
  389. 'cfunction',
  390. 'class',
  391. 'clone',
  392. 'const',
  393. 'continue',
  394. 'declare',
  395. 'default',
  396. 'die',
  397. 'do',
  398. 'echo',
  399. 'else',
  400. 'elseif',
  401. 'empty',
  402. 'enddeclare',
  403. 'endfor',
  404. 'endforeach',
  405. 'endif',
  406. 'endswitch',
  407. 'endwhile',
  408. 'eval',
  409. 'exception',
  410. 'exit',
  411. 'extends',
  412. 'final',
  413. 'finally',
  414. 'for',
  415. 'foreach',
  416. 'function',
  417. 'global',
  418. 'goto',
  419. 'if',
  420. 'implements',
  421. 'include',
  422. 'include_once',
  423. 'instanceof',
  424. 'insteadof',
  425. 'interface',
  426. 'isset',
  427. 'list',
  428. 'namespace',
  429. 'new',
  430. 'old_function',
  431. 'or',
  432. 'parent',
  433. 'php_user_filter',
  434. 'print',
  435. 'private',
  436. 'protected',
  437. 'public',
  438. 'require',
  439. 'require_once',
  440. 'return',
  441. 'static',
  442. 'switch',
  443. 'this',
  444. 'throw',
  445. 'trait',
  446. 'try',
  447. 'unset',
  448. 'use',
  449. 'var',
  450. 'while',
  451. 'xor',
  452. ];
  453. return in_array(strtolower($value), $keywords, true);
  454. }
  455. /**
  456. * Generates a string depending on enableI18N property
  457. *
  458. * @param string $string the text be generated
  459. * @param array $placeholders the placeholders to use by `Yii::t()`
  460. * @return string
  461. */
  462. public function generateString($string = '', $placeholders = [])
  463. {
  464. $string = addslashes($string);
  465. if ($this->enableI18N) {
  466. // If there are placeholders, use them
  467. if (!empty($placeholders)) {
  468. $ph = ', ' . VarDumper::export($placeholders);
  469. } else {
  470. $ph = '';
  471. }
  472. $str = "Yii::t('" . $this->messageCategory . "', '" . $string . "'" . $ph . ")";
  473. } else {
  474. // No I18N, replace placeholders by real words, if any
  475. if (!empty($placeholders)) {
  476. $phKeys = array_map(function($word) {
  477. return '{' . $word . '}';
  478. }, array_keys($placeholders));
  479. $phValues = array_values($placeholders);
  480. $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
  481. } else {
  482. // No placeholders, just the given string
  483. $str = "'" . $string . "'";
  484. }
  485. }
  486. return $str;
  487. }
  488. }