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.

852 line
33KB

  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\model;
  8. use Yii;
  9. use yii\db\ActiveQuery;
  10. use yii\db\ActiveRecord;
  11. use yii\db\Connection;
  12. use yii\db\Schema;
  13. use yii\db\TableSchema;
  14. use yii\gii\CodeFile;
  15. use yii\helpers\Inflector;
  16. use yii\base\NotSupportedException;
  17. /**
  18. * This generator will generate one or multiple ActiveRecord classes for the specified database table.
  19. *
  20. * @author Qiang Xue <qiang.xue@gmail.com>
  21. * @since 2.0
  22. */
  23. class Generator extends \yii\gii\Generator
  24. {
  25. const RELATIONS_NONE = 'none';
  26. const RELATIONS_ALL = 'all';
  27. const RELATIONS_ALL_INVERSE = 'all-inverse';
  28. public $db = 'db';
  29. public $ns = 'app\models';
  30. public $tableName;
  31. public $modelClass;
  32. public $baseClass = 'yii\db\ActiveRecord';
  33. public $generateRelations = self::RELATIONS_ALL;
  34. public $generateLabelsFromComments = false;
  35. public $useTablePrefix = false;
  36. public $useSchemaName = true;
  37. public $generateQuery = false;
  38. public $queryNs = 'app\models';
  39. public $queryClass;
  40. public $queryBaseClass = 'yii\db\ActiveQuery';
  41. /**
  42. * @inheritdoc
  43. */
  44. public function getName()
  45. {
  46. return 'Model Generator';
  47. }
  48. /**
  49. * @inheritdoc
  50. */
  51. public function getDescription()
  52. {
  53. return 'This generator generates an ActiveRecord class for the specified database table.';
  54. }
  55. /**
  56. * @inheritdoc
  57. */
  58. public function rules()
  59. {
  60. return array_merge(parent::rules(), [
  61. [['db', 'ns', 'tableName', 'modelClass', 'baseClass', 'queryNs', 'queryClass', 'queryBaseClass'], 'filter', 'filter' => 'trim'],
  62. [['ns', 'queryNs'], 'filter', 'filter' => function($value) { return trim($value, '\\'); }],
  63. [['db', 'ns', 'tableName', 'baseClass', 'queryNs', 'queryBaseClass'], 'required'],
  64. [['db', 'modelClass', 'queryClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
  65. [['ns', 'baseClass', 'queryNs', 'queryBaseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
  66. [['tableName'], 'match', 'pattern' => '/^([\w ]+\.)?([\w\* ]+)$/', 'message' => 'Only word characters, and optionally spaces, an asterisk and/or a dot are allowed.'],
  67. [['db'], 'validateDb'],
  68. [['ns', 'queryNs'], 'validateNamespace'],
  69. [['tableName'], 'validateTableName'],
  70. [['modelClass'], 'validateModelClass', 'skipOnEmpty' => false],
  71. [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],
  72. [['queryBaseClass'], 'validateClass', 'params' => ['extends' => ActiveQuery::className()]],
  73. [['generateRelations'], 'in', 'range' => [self::RELATIONS_NONE, self::RELATIONS_ALL, self::RELATIONS_ALL_INVERSE]],
  74. [['generateLabelsFromComments', 'useTablePrefix', 'useSchemaName', 'generateQuery'], 'boolean'],
  75. [['enableI18N'], 'boolean'],
  76. [['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false],
  77. ]);
  78. }
  79. /**
  80. * @inheritdoc
  81. */
  82. public function attributeLabels()
  83. {
  84. return array_merge(parent::attributeLabels(), [
  85. 'ns' => 'Namespace',
  86. 'db' => 'Database Connection ID',
  87. 'tableName' => 'Table Name',
  88. 'modelClass' => 'Model Class',
  89. 'baseClass' => 'Base Class',
  90. 'generateRelations' => 'Generate Relations',
  91. 'generateLabelsFromComments' => 'Generate Labels from DB Comments',
  92. 'generateQuery' => 'Generate ActiveQuery',
  93. 'queryNs' => 'ActiveQuery Namespace',
  94. 'queryClass' => 'ActiveQuery Class',
  95. 'queryBaseClass' => 'ActiveQuery Base Class',
  96. 'useSchemaName' => 'Use Schema Name',
  97. ]);
  98. }
  99. /**
  100. * @inheritdoc
  101. */
  102. public function hints()
  103. {
  104. return array_merge(parent::hints(), [
  105. 'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>',
  106. 'db' => 'This is the ID of the DB application component.',
  107. 'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. <code>post</code>.
  108. The table name may consist of the DB schema part if needed, e.g. <code>public.post</code>.
  109. The table name may end with asterisk to match multiple table names, e.g. <code>tbl_*</code>
  110. will match tables who name starts with <code>tbl_</code>. In this case, multiple ActiveRecord classes
  111. will be generated, one for each matching table name; and the class names will be generated from
  112. the matching characters. For example, table <code>tbl_post</code> will generate <code>Post</code>
  113. class.',
  114. 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
  115. the namespace part as it is specified in "Namespace". You do not need to specify the class name
  116. if "Table Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.',
  117. 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
  118. 'generateRelations' => 'This indicates whether the generator should generate relations based on
  119. foreign key constraints it detects in the database. Note that if your database contains too many tables,
  120. you may want to uncheck this option to accelerate the code generation process.',
  121. 'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels
  122. by using the comments of the corresponding DB columns.',
  123. 'useTablePrefix' => 'This indicates whether the table name returned by the generated ActiveRecord class
  124. should consider the <code>tablePrefix</code> setting of the DB connection. For example, if the
  125. table name is <code>tbl_post</code> and <code>tablePrefix=tbl_</code>, the ActiveRecord class
  126. will return the table name as <code>{{%post}}</code>.',
  127. 'useSchemaName' => 'This indicates whether to include the schema name in the ActiveRecord class
  128. when it\'s auto generated. Only non default schema would be used.',
  129. 'generateQuery' => 'This indicates whether to generate ActiveQuery for the ActiveRecord class.',
  130. 'queryNs' => 'This is the namespace of the ActiveQuery class to be generated, e.g., <code>app\models</code>',
  131. 'queryClass' => 'This is the name of the ActiveQuery class to be generated. The class name should not contain
  132. the namespace part as it is specified in "ActiveQuery Namespace". You do not need to specify the class name
  133. if "Table Name" ends with asterisk, in which case multiple ActiveQuery classes will be generated.',
  134. 'queryBaseClass' => 'This is the base class of the new ActiveQuery class. It should be a fully qualified namespaced class name.',
  135. ]);
  136. }
  137. /**
  138. * @inheritdoc
  139. */
  140. public function autoCompleteData()
  141. {
  142. $db = $this->getDbConnection();
  143. if ($db !== null) {
  144. return [
  145. 'tableName' => function () use ($db) {
  146. return $db->getSchema()->getTableNames();
  147. },
  148. ];
  149. } else {
  150. return [];
  151. }
  152. }
  153. /**
  154. * @inheritdoc
  155. */
  156. public function requiredTemplates()
  157. {
  158. // @todo make 'query.php' to be required before 2.1 release
  159. return ['model.php'/*, 'query.php'*/];
  160. }
  161. /**
  162. * @inheritdoc
  163. */
  164. public function stickyAttributes()
  165. {
  166. return array_merge(parent::stickyAttributes(), ['ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments', 'queryNs', 'queryBaseClass']);
  167. }
  168. /**
  169. * Returns the `tablePrefix` property of the DB connection as specified
  170. *
  171. * @return string
  172. * @since 2.0.5
  173. * @see getDbConnection
  174. */
  175. public function getTablePrefix()
  176. {
  177. $db = $this->getDbConnection();
  178. if ($db !== null) {
  179. return $db->tablePrefix;
  180. } else {
  181. return '';
  182. }
  183. }
  184. /**
  185. * @inheritdoc
  186. */
  187. public function generate()
  188. {
  189. $files = [];
  190. $relations = $this->generateRelations();
  191. $db = $this->getDbConnection();
  192. foreach ($this->getTableNames() as $tableName) {
  193. // model :
  194. $modelClassName = $this->generateClassName($tableName);
  195. $queryClassName = ($this->generateQuery) ? $this->generateQueryClassName($modelClassName) : false;
  196. $tableSchema = $db->getTableSchema($tableName);
  197. $params = [
  198. 'tableName' => $tableName,
  199. 'className' => $modelClassName,
  200. 'queryClassName' => $queryClassName,
  201. 'tableSchema' => $tableSchema,
  202. 'labels' => $this->generateLabels($tableSchema),
  203. 'rules' => $this->generateRules($tableSchema),
  204. 'relations' => isset($relations[$tableName]) ? $relations[$tableName] : [],
  205. ];
  206. $files[] = new CodeFile(
  207. Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $modelClassName . '.php',
  208. $this->render('model.php', $params)
  209. );
  210. // query :
  211. if ($queryClassName) {
  212. $params['className'] = $queryClassName;
  213. $params['modelClassName'] = $modelClassName;
  214. $files[] = new CodeFile(
  215. Yii::getAlias('@' . str_replace('\\', '/', $this->queryNs)) . '/' . $queryClassName . '.php',
  216. $this->render('query.php', $params)
  217. );
  218. }
  219. }
  220. return $files;
  221. }
  222. /**
  223. * Generates the attribute labels for the specified table.
  224. * @param \yii\db\TableSchema $table the table schema
  225. * @return array the generated attribute labels (name => label)
  226. */
  227. public function generateLabels($table)
  228. {
  229. $labels = [];
  230. foreach ($table->columns as $column) {
  231. if ($this->generateLabelsFromComments && !empty($column->comment)) {
  232. $labels[$column->name] = $column->comment;
  233. } elseif (!strcasecmp($column->name, 'id')) {
  234. $labels[$column->name] = 'ID';
  235. } else {
  236. $label = Inflector::camel2words($column->name);
  237. if (!empty($label) && substr_compare($label, ' id', -3, 3, true) === 0) {
  238. $label = substr($label, 0, -3) . ' ID';
  239. }
  240. $labels[$column->name] = $label;
  241. }
  242. }
  243. return $labels;
  244. }
  245. /**
  246. * Generates validation rules for the specified table.
  247. * @param \yii\db\TableSchema $table the table schema
  248. * @return array the generated validation rules
  249. */
  250. public function generateRules($table)
  251. {
  252. $types = [];
  253. $lengths = [];
  254. foreach ($table->columns as $column) {
  255. if ($column->autoIncrement) {
  256. continue;
  257. }
  258. if (!$column->allowNull && $column->defaultValue === null) {
  259. $types['required'][] = $column->name;
  260. }
  261. switch ($column->type) {
  262. case Schema::TYPE_SMALLINT:
  263. case Schema::TYPE_INTEGER:
  264. case Schema::TYPE_BIGINT:
  265. $types['integer'][] = $column->name;
  266. break;
  267. case Schema::TYPE_BOOLEAN:
  268. $types['boolean'][] = $column->name;
  269. break;
  270. case Schema::TYPE_FLOAT:
  271. case 'double': // Schema::TYPE_DOUBLE, which is available since Yii 2.0.3
  272. case Schema::TYPE_DECIMAL:
  273. case Schema::TYPE_MONEY:
  274. $types['number'][] = $column->name;
  275. break;
  276. case Schema::TYPE_DATE:
  277. case Schema::TYPE_TIME:
  278. case Schema::TYPE_DATETIME:
  279. case Schema::TYPE_TIMESTAMP:
  280. $types['safe'][] = $column->name;
  281. break;
  282. default: // strings
  283. if ($column->size > 0) {
  284. $lengths[$column->size][] = $column->name;
  285. } else {
  286. $types['string'][] = $column->name;
  287. }
  288. }
  289. }
  290. $rules = [];
  291. foreach ($types as $type => $columns) {
  292. $rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
  293. }
  294. foreach ($lengths as $length => $columns) {
  295. $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]";
  296. }
  297. $db = $this->getDbConnection();
  298. // Unique indexes rules
  299. try {
  300. $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table);
  301. foreach ($uniqueIndexes as $uniqueColumns) {
  302. // Avoid validating auto incremental columns
  303. if (!$this->isColumnAutoIncremental($table, $uniqueColumns)) {
  304. $attributesCount = count($uniqueColumns);
  305. if ($attributesCount === 1) {
  306. $rules[] = "[['" . $uniqueColumns[0] . "'], 'unique']";
  307. } elseif ($attributesCount > 1) {
  308. $labels = array_intersect_key($this->generateLabels($table), array_flip($uniqueColumns));
  309. $lastLabel = array_pop($labels);
  310. $columnsList = implode("', '", $uniqueColumns);
  311. $rules[] = "[['$columnsList'], 'unique', 'targetAttribute' => ['$columnsList'], 'message' => 'The combination of " . implode(', ', $labels) . " and $lastLabel has already been taken.']";
  312. }
  313. }
  314. }
  315. } catch (NotSupportedException $e) {
  316. // doesn't support unique indexes information...do nothing
  317. }
  318. // Exist rules for foreign keys
  319. foreach ($table->foreignKeys as $refs) {
  320. $refTable = $refs[0];
  321. $refTableSchema = $db->getTableSchema($refTable);
  322. if ($refTableSchema === null) {
  323. // Foreign key could point to non-existing table: https://github.com/yiisoft/yii2-gii/issues/34
  324. continue;
  325. }
  326. $refClassName = $this->generateClassName($refTable);
  327. unset($refs[0]);
  328. $attributes = implode("', '", array_keys($refs));
  329. $targetAttributes = [];
  330. foreach ($refs as $key => $value) {
  331. $targetAttributes[] = "'$key' => '$value'";
  332. }
  333. $targetAttributes = implode(', ', $targetAttributes);
  334. $rules[] = "[['$attributes'], 'exist', 'skipOnError' => true, 'targetClass' => $refClassName::className(), 'targetAttribute' => [$targetAttributes]]";
  335. }
  336. return $rules;
  337. }
  338. /**
  339. * Generates relations using a junction table by adding an extra viaTable().
  340. * @param \yii\db\TableSchema the table being checked
  341. * @param array $fks obtained from the checkJunctionTable() method
  342. * @param array $relations
  343. * @return array modified $relations
  344. */
  345. private function generateManyManyRelations($table, $fks, $relations)
  346. {
  347. $db = $this->getDbConnection();
  348. foreach ($fks as $pair) {
  349. list($firstKey, $secondKey) = $pair;
  350. $table0 = $firstKey[0];
  351. $table1 = $secondKey[0];
  352. unset($firstKey[0], $secondKey[0]);
  353. $className0 = $this->generateClassName($table0);
  354. $className1 = $this->generateClassName($table1);
  355. $table0Schema = $db->getTableSchema($table0);
  356. $table1Schema = $db->getTableSchema($table1);
  357. $link = $this->generateRelationLink(array_flip($secondKey));
  358. $viaLink = $this->generateRelationLink($firstKey);
  359. $relationName = $this->generateRelationName($relations, $table0Schema, key($secondKey), true);
  360. $relations[$table0Schema->fullName][$relationName] = [
  361. "return \$this->hasMany($className1::className(), $link)->viaTable('"
  362. . $this->generateTableName($table->name) . "', $viaLink);",
  363. $className1,
  364. true,
  365. ];
  366. $link = $this->generateRelationLink(array_flip($firstKey));
  367. $viaLink = $this->generateRelationLink($secondKey);
  368. $relationName = $this->generateRelationName($relations, $table1Schema, key($firstKey), true);
  369. $relations[$table1Schema->fullName][$relationName] = [
  370. "return \$this->hasMany($className0::className(), $link)->viaTable('"
  371. . $this->generateTableName($table->name) . "', $viaLink);",
  372. $className0,
  373. true,
  374. ];
  375. }
  376. return $relations;
  377. }
  378. /**
  379. * @return string[] all db schema names or an array with a single empty string
  380. * @throws NotSupportedException
  381. * @since 2.0.5
  382. */
  383. protected function getSchemaNames()
  384. {
  385. $db = $this->getDbConnection();
  386. $schema = $db->getSchema();
  387. if ($schema->hasMethod('getSchemaNames')) { // keep BC to Yii versions < 2.0.4
  388. try {
  389. $schemaNames = $schema->getSchemaNames();
  390. } catch (NotSupportedException $e) {
  391. // schema names are not supported by schema
  392. }
  393. }
  394. if (!isset($schemaNames)) {
  395. if (($pos = strpos($this->tableName, '.')) !== false) {
  396. $schemaNames = [substr($this->tableName, 0, $pos)];
  397. } else {
  398. $schemaNames = [''];
  399. }
  400. }
  401. return $schemaNames;
  402. }
  403. /**
  404. * @return array the generated relation declarations
  405. */
  406. protected function generateRelations()
  407. {
  408. if ($this->generateRelations === self::RELATIONS_NONE) {
  409. return [];
  410. }
  411. $db = $this->getDbConnection();
  412. $relations = [];
  413. foreach ($this->getSchemaNames() as $schemaName) {
  414. foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) {
  415. $className = $this->generateClassName($table->fullName);
  416. foreach ($table->foreignKeys as $refs) {
  417. $refTable = $refs[0];
  418. $refTableSchema = $db->getTableSchema($refTable);
  419. if ($refTableSchema === null) {
  420. // Foreign key could point to non-existing table: https://github.com/yiisoft/yii2-gii/issues/34
  421. continue;
  422. }
  423. unset($refs[0]);
  424. $fks = array_keys($refs);
  425. $refClassName = $this->generateClassName($refTable);
  426. // Add relation for this table
  427. $link = $this->generateRelationLink(array_flip($refs));
  428. $relationName = $this->generateRelationName($relations, $table, $fks[0], false);
  429. $relations[$table->fullName][$relationName] = [
  430. "return \$this->hasOne($refClassName::className(), $link);",
  431. $refClassName,
  432. false,
  433. ];
  434. // Add relation for the referenced table
  435. $hasMany = $this->isHasManyRelation($table, $fks);
  436. $link = $this->generateRelationLink($refs);
  437. $relationName = $this->generateRelationName($relations, $refTableSchema, $className, $hasMany);
  438. $relations[$refTableSchema->fullName][$relationName] = [
  439. "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "($className::className(), $link);",
  440. $className,
  441. $hasMany,
  442. ];
  443. }
  444. if (($junctionFks = $this->checkJunctionTable($table)) === false) {
  445. continue;
  446. }
  447. $relations = $this->generateManyManyRelations($table, $junctionFks, $relations);
  448. }
  449. }
  450. if ($this->generateRelations === self::RELATIONS_ALL_INVERSE) {
  451. return $this->addInverseRelations($relations);
  452. }
  453. return $relations;
  454. }
  455. /**
  456. * Adds inverse relations
  457. *
  458. * @param array $relations relation declarations
  459. * @return array relation declarations extended with inverse relation names
  460. * @since 2.0.5
  461. */
  462. protected function addInverseRelations($relations)
  463. {
  464. $relationNames = [];
  465. foreach ($this->getSchemaNames() as $schemaName) {
  466. foreach ($this->getDbConnection()->getSchema()->getTableSchemas($schemaName) as $table) {
  467. $className = $this->generateClassName($table->fullName);
  468. foreach ($table->foreignKeys as $refs) {
  469. $refTable = $refs[0];
  470. $refTableSchema = $this->getDbConnection()->getTableSchema($refTable);
  471. unset($refs[0]);
  472. $fks = array_keys($refs);
  473. $leftRelationName = $this->generateRelationName($relationNames, $table, $fks[0], false);
  474. $relationNames[$table->fullName][$leftRelationName] = true;
  475. $hasMany = $this->isHasManyRelation($table, $fks);
  476. $rightRelationName = $this->generateRelationName(
  477. $relationNames,
  478. $refTableSchema,
  479. $className,
  480. $hasMany
  481. );
  482. $relationNames[$refTableSchema->fullName][$rightRelationName] = true;
  483. $relations[$table->fullName][$leftRelationName][0] =
  484. rtrim($relations[$table->fullName][$leftRelationName][0], ';')
  485. . "->inverseOf('".lcfirst($rightRelationName)."');";
  486. $relations[$refTableSchema->fullName][$rightRelationName][0] =
  487. rtrim($relations[$refTableSchema->fullName][$rightRelationName][0], ';')
  488. . "->inverseOf('".lcfirst($leftRelationName)."');";
  489. }
  490. }
  491. }
  492. return $relations;
  493. }
  494. /**
  495. * Determines if relation is of has many type
  496. *
  497. * @param TableSchema $table
  498. * @param array $fks
  499. * @return boolean
  500. * @since 2.0.5
  501. */
  502. protected function isHasManyRelation($table, $fks)
  503. {
  504. $uniqueKeys = [$table->primaryKey];
  505. try {
  506. $uniqueKeys = array_merge($uniqueKeys, $this->getDbConnection()->getSchema()->findUniqueIndexes($table));
  507. } catch (NotSupportedException $e) {
  508. // ignore
  509. }
  510. foreach ($uniqueKeys as $uniqueKey) {
  511. if (count(array_diff(array_merge($uniqueKey, $fks), array_intersect($uniqueKey, $fks))) === 0) {
  512. return false;
  513. }
  514. }
  515. return true;
  516. }
  517. /**
  518. * Generates the link parameter to be used in generating the relation declaration.
  519. * @param array $refs reference constraint
  520. * @return string the generated link parameter.
  521. */
  522. protected function generateRelationLink($refs)
  523. {
  524. $pairs = [];
  525. foreach ($refs as $a => $b) {
  526. $pairs[] = "'$a' => '$b'";
  527. }
  528. return '[' . implode(', ', $pairs) . ']';
  529. }
  530. /**
  531. * Checks if the given table is a junction table, that is it has at least one pair of unique foreign keys.
  532. * @param \yii\db\TableSchema the table being checked
  533. * @return array|boolean all unique foreign key pairs if the table is a junction table,
  534. * or false if the table is not a junction table.
  535. */
  536. protected function checkJunctionTable($table)
  537. {
  538. if (count($table->foreignKeys) < 2) {
  539. return false;
  540. }
  541. $uniqueKeys = [$table->primaryKey];
  542. try {
  543. $uniqueKeys = array_merge($uniqueKeys, $this->getDbConnection()->getSchema()->findUniqueIndexes($table));
  544. } catch (NotSupportedException $e) {
  545. // ignore
  546. }
  547. $result = [];
  548. // find all foreign key pairs that have all columns in an unique constraint
  549. $foreignKeys = array_values($table->foreignKeys);
  550. for ($i = 0; $i < count($foreignKeys); $i++) {
  551. $firstColumns = $foreignKeys[$i];
  552. unset($firstColumns[0]);
  553. for ($j = $i + 1; $j < count($foreignKeys); $j++) {
  554. $secondColumns = $foreignKeys[$j];
  555. unset($secondColumns[0]);
  556. $fks = array_merge(array_keys($firstColumns), array_keys($secondColumns));
  557. foreach ($uniqueKeys as $uniqueKey) {
  558. if (count(array_diff(array_merge($uniqueKey, $fks), array_intersect($uniqueKey, $fks))) === 0) {
  559. // save the foreign key pair
  560. $result[] = [$foreignKeys[$i], $foreignKeys[$j]];
  561. break;
  562. }
  563. }
  564. }
  565. }
  566. return empty($result) ? false : $result;
  567. }
  568. /**
  569. * Generate a relation name for the specified table and a base name.
  570. * @param array $relations the relations being generated currently.
  571. * @param \yii\db\TableSchema $table the table schema
  572. * @param string $key a base name that the relation name may be generated from
  573. * @param boolean $multiple whether this is a has-many relation
  574. * @return string the relation name
  575. */
  576. protected function generateRelationName($relations, $table, $key, $multiple)
  577. {
  578. if (!empty($key) && substr_compare($key, 'id', -2, 2, true) === 0 && strcasecmp($key, 'id')) {
  579. $key = rtrim(substr($key, 0, -2), '_');
  580. }
  581. if ($multiple) {
  582. $key = Inflector::pluralize($key);
  583. }
  584. $name = $rawName = Inflector::id2camel($key, '_');
  585. $i = 0;
  586. while (isset($table->columns[lcfirst($name)])) {
  587. $name = $rawName . ($i++);
  588. }
  589. while (isset($relations[$table->fullName][$name])) {
  590. $name = $rawName . ($i++);
  591. }
  592. return $name;
  593. }
  594. /**
  595. * Validates the [[db]] attribute.
  596. */
  597. public function validateDb()
  598. {
  599. if (!Yii::$app->has($this->db)) {
  600. $this->addError('db', 'There is no application component named "db".');
  601. } elseif (!Yii::$app->get($this->db) instanceof Connection) {
  602. $this->addError('db', 'The "db" application component must be a DB connection instance.');
  603. }
  604. }
  605. /**
  606. * Validates the namespace.
  607. *
  608. * @param string $attribute Namespace variable.
  609. */
  610. public function validateNamespace($attribute)
  611. {
  612. $value = $this->$attribute;
  613. $value = ltrim($value, '\\');
  614. $path = Yii::getAlias('@' . str_replace('\\', '/', $value), false);
  615. if ($path === false) {
  616. $this->addError($attribute, 'Namespace must be associated with an existing directory.');
  617. }
  618. }
  619. /**
  620. * Validates the [[modelClass]] attribute.
  621. */
  622. public function validateModelClass()
  623. {
  624. if ($this->isReservedKeyword($this->modelClass)) {
  625. $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
  626. }
  627. if ((empty($this->tableName) || substr_compare($this->tableName, '*', -1, 1)) && $this->modelClass == '') {
  628. $this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.');
  629. }
  630. }
  631. /**
  632. * Validates the [[tableName]] attribute.
  633. */
  634. public function validateTableName()
  635. {
  636. if (strpos($this->tableName, '*') !== false && substr_compare($this->tableName, '*', -1, 1)) {
  637. $this->addError('tableName', 'Asterisk is only allowed as the last character.');
  638. return;
  639. }
  640. $tables = $this->getTableNames();
  641. if (empty($tables)) {
  642. $this->addError('tableName', "Table '{$this->tableName}' does not exist.");
  643. } else {
  644. foreach ($tables as $table) {
  645. $class = $this->generateClassName($table);
  646. if ($this->isReservedKeyword($class)) {
  647. $this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword.");
  648. break;
  649. }
  650. }
  651. }
  652. }
  653. protected $tableNames;
  654. protected $classNames;
  655. /**
  656. * @return array the table names that match the pattern specified by [[tableName]].
  657. */
  658. protected function getTableNames()
  659. {
  660. if ($this->tableNames !== null) {
  661. return $this->tableNames;
  662. }
  663. $db = $this->getDbConnection();
  664. if ($db === null) {
  665. return [];
  666. }
  667. $tableNames = [];
  668. if (strpos($this->tableName, '*') !== false) {
  669. if (($pos = strrpos($this->tableName, '.')) !== false) {
  670. $schema = substr($this->tableName, 0, $pos);
  671. $pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/';
  672. } else {
  673. $schema = '';
  674. $pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/';
  675. }
  676. foreach ($db->schema->getTableNames($schema) as $table) {
  677. if (preg_match($pattern, $table)) {
  678. $tableNames[] = $schema === '' ? $table : ($schema . '.' . $table);
  679. }
  680. }
  681. } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
  682. $tableNames[] = $this->tableName;
  683. $this->classNames[$this->tableName] = $this->modelClass;
  684. }
  685. return $this->tableNames = $tableNames;
  686. }
  687. /**
  688. * Generates the table name by considering table prefix.
  689. * If [[useTablePrefix]] is false, the table name will be returned without change.
  690. * @param string $tableName the table name (which may contain schema prefix)
  691. * @return string the generated table name
  692. */
  693. public function generateTableName($tableName)
  694. {
  695. if (!$this->useTablePrefix) {
  696. return $tableName;
  697. }
  698. $db = $this->getDbConnection();
  699. if (preg_match("/^{$db->tablePrefix}(.*?)$/", $tableName, $matches)) {
  700. $tableName = '{{%' . $matches[1] . '}}';
  701. } elseif (preg_match("/^(.*?){$db->tablePrefix}$/", $tableName, $matches)) {
  702. $tableName = '{{' . $matches[1] . '%}}';
  703. }
  704. return $tableName;
  705. }
  706. /**
  707. * Generates a class name from the specified table name.
  708. * @param string $tableName the table name (which may contain schema prefix)
  709. * @param boolean $useSchemaName should schema name be included in the class name, if present
  710. * @return string the generated class name
  711. */
  712. protected function generateClassName($tableName, $useSchemaName = null)
  713. {
  714. if (isset($this->classNames[$tableName])) {
  715. return $this->classNames[$tableName];
  716. }
  717. $schemaName = '';
  718. $fullTableName = $tableName;
  719. if (($pos = strrpos($tableName, '.')) !== false) {
  720. if (($useSchemaName === null && $this->useSchemaName) || $useSchemaName) {
  721. $schemaName = substr($tableName, 0, $pos) . '_';
  722. }
  723. $tableName = substr($tableName, $pos + 1);
  724. }
  725. $db = $this->getDbConnection();
  726. $patterns = [];
  727. $patterns[] = "/^{$db->tablePrefix}(.*?)$/";
  728. $patterns[] = "/^(.*?){$db->tablePrefix}$/";
  729. if (strpos($this->tableName, '*') !== false) {
  730. $pattern = $this->tableName;
  731. if (($pos = strrpos($pattern, '.')) !== false) {
  732. $pattern = substr($pattern, $pos + 1);
  733. }
  734. $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
  735. }
  736. $className = $tableName;
  737. foreach ($patterns as $pattern) {
  738. if (preg_match($pattern, $tableName, $matches)) {
  739. $className = $matches[1];
  740. break;
  741. }
  742. }
  743. return $this->classNames[$fullTableName] = Inflector::id2camel($schemaName.$className, '_');
  744. }
  745. /**
  746. * Generates a query class name from the specified model class name.
  747. * @param string $modelClassName model class name
  748. * @return string generated class name
  749. */
  750. protected function generateQueryClassName($modelClassName)
  751. {
  752. $queryClassName = $this->queryClass;
  753. if (empty($queryClassName) || strpos($this->tableName, '*') !== false) {
  754. $queryClassName = $modelClassName . 'Query';
  755. }
  756. return $queryClassName;
  757. }
  758. /**
  759. * @return Connection the DB connection as specified by [[db]].
  760. */
  761. protected function getDbConnection()
  762. {
  763. return Yii::$app->get($this->db, false);
  764. }
  765. /**
  766. * Checks if any of the specified columns is auto incremental.
  767. * @param \yii\db\TableSchema $table the table schema
  768. * @param array $columns columns to check for autoIncrement property
  769. * @return boolean whether any of the specified columns is auto incremental.
  770. */
  771. protected function isColumnAutoIncremental($table, $columns)
  772. {
  773. foreach ($columns as $column) {
  774. if (isset($table->columns[$column]) && $table->columns[$column]->autoIncrement) {
  775. return true;
  776. }
  777. }
  778. return false;
  779. }
  780. }