Generator.php 17 KB

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