diff --git a/framework/yii/db/Schema.php b/framework/yii/db/Schema.php index 1aef764..8804f77 100644 --- a/framework/yii/db/Schema.php +++ b/framework/yii/db/Schema.php @@ -138,7 +138,7 @@ abstract class Schema extends Object * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. * @param boolean $refresh whether to fetch the latest available table schemas. If this is false, * cached data may be returned if available. - * @return array the metadata for all tables in the database. + * @return TableSchema[] the metadata for all tables in the database. * Each array element is an instance of [[TableSchema]] or its child class. */ public function getTableSchemas($schema = '', $refresh = false) @@ -161,7 +161,7 @@ abstract class Schema extends Object * If not empty, the returned table names will be prefixed with the schema name. * @param boolean $refresh whether to fetch the latest available table names. If this is false, * table names fetched previously (if available) will be returned. - * @return array all table names in the database. + * @return string[] all table names in the database. */ public function getTableNames($schema = '', $refresh = false) { diff --git a/framework/yii/gii/generators/model/Generator.php b/framework/yii/gii/generators/model/Generator.php index 6ce68a6..7aa601a 100644 --- a/framework/yii/gii/generators/model/Generator.php +++ b/framework/yii/gii/generators/model/Generator.php @@ -15,6 +15,7 @@ use yii\gii\CodeFile; use yii\helpers\Inflector; /** + * This generator will generate one or multiple ActiveRecord classes for the specified database table. * * @author Qiang Xue * @since 2.0 @@ -30,16 +31,25 @@ class Generator extends \yii\gii\Generator public $generateLabelsFromComments = false; + /** + * @inheritdoc + */ public function getName() { return 'Model Generator'; } + /** + * @inheritdoc + */ public function getDescription() { return 'This generator generates an ActiveRecord class for the specified database table.'; } + /** + * @inheritdoc + */ public function rules() { return array_merge(parent::rules(), array( @@ -57,6 +67,9 @@ class Generator extends \yii\gii\Generator )); } + /** + * @inheritdoc + */ public function attributeLabels() { return array( @@ -70,6 +83,9 @@ class Generator extends \yii\gii\Generator ); } + /** + * @inheritdoc + */ public function hints() { return array( @@ -91,6 +107,9 @@ class Generator extends \yii\gii\Generator ); } + /** + * @inheritdoc + */ public function requiredTemplates() { return array( @@ -98,31 +117,32 @@ class Generator extends \yii\gii\Generator ); } + /** + * @inheritdoc + */ public function stickyAttributes() { return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments'); } /** - * @return Connection + * @inheritdoc */ - public function getDbConnection() - { - return Yii::$app->{$this->db}; - } - public function generate() { $files = array(); + $relations = $this->generateRelations(); + $db = $this->getDbConnection(); foreach ($this->getTableNames() as $tableName) { $className = $this->generateClassName($tableName); - $tableSchema = $this->getTableSchema($tableName); + $tableSchema = $db->getTableSchema($tableName); $params = array( 'tableName' => $tableName, 'className' => $className, 'tableSchema' => $tableSchema, 'labels' => $this->generateLabels($tableSchema), 'rules' => $this->generateRules($tableSchema), + 'relations' => isset($relations[$className]) ? $relations[$className] : array(), ); $files[] = new CodeFile( Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php', @@ -133,11 +153,11 @@ class Generator extends \yii\gii\Generator return $files; } - public function getTableSchema($tableName) - { - return $this->getDbConnection()->getTableSchema($tableName, true); - } - + /** + * Generates the attribute labels for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated attribute labels (name => label) + */ public function generateLabels($table) { $labels = array(); @@ -158,8 +178,9 @@ class Generator extends \yii\gii\Generator } /** - * @param \yii\db\TableSchema $table - * @return array + * Generates validation rules for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated validation rules */ public function generateRules($table) { @@ -212,159 +233,165 @@ class Generator extends \yii\gii\Generator return $rules; } - public function getRelations($className) - { - return isset($this->relations[$className]) ? $this->relations[$className] : array(); - } - - protected function removePrefix($tableName, $addBrackets = true) - { - if ($addBrackets && Yii::$app->{$this->connectionId}->tablePrefix == '') { - return $tableName; - } - $prefix = $this->tablePrefix != '' ? $this->tablePrefix : Yii::$app->{$this->connectionId}->tablePrefix; - if ($prefix != '') { - if ($addBrackets && Yii::$app->{$this->connectionId}->tablePrefix != '') { - $prefix = Yii::$app->{$this->connectionId}->tablePrefix; - $lb = '{{'; - $rb = '}}'; - } else { - $lb = $rb = ''; - } - if (($pos = strrpos($tableName, '.')) !== false) { - $schema = substr($tableName, 0, $pos); - $name = substr($tableName, $pos + 1); - if (strpos($name, $prefix) === 0) { - return $schema . '.' . $lb . substr($name, strlen($prefix)) . $rb; - } - } elseif (strpos($tableName, $prefix) === 0) { - return $lb . substr($tableName, strlen($prefix)) . $rb; - } - } - return $tableName; - } - + /** + * @return array the generated relation declarations + */ protected function generateRelations() { if (!$this->generateRelations) { return array(); } - $schemaName = ''; + $db = $this->getDbConnection(); + if (($pos = strpos($this->tableName, '.')) !== false) { $schemaName = substr($this->tableName, 0, $pos); + } else { + $schemaName = ''; } $relations = array(); - foreach (Yii::$app->{$this->connectionId}->schema->getTables($schemaName) as $table) { - if ($this->tablePrefix != '' && strpos($table->name, $this->tablePrefix) !== 0) { - continue; - } + foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) { $tableName = $table->name; - - if ($this->isRelationTable($table)) { - $pks = $table->primaryKey; - $fks = $table->foreignKeys; - - $table0 = $fks[$pks[0]][0]; - $table1 = $fks[$pks[1]][0]; - $className0 = $this->generateClassName($table0); - $className1 = $this->generateClassName($table1); - - $unprefixedTableName = $this->removePrefix($tableName); - - $relationName = $this->generateRelationName($table0, $table1, true); - $relations[$className0][$relationName] = "array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')"; - - $relationName = $this->generateRelationName($table1, $table0, true); - - $i = 1; - $rawName = $relationName; - while (isset($relations[$className1][$relationName])) { - $relationName = $rawName . $i++; - } - - $relations[$className1][$relationName] = "array(self::MANY_MANY, '$className0', '$unprefixedTableName($pks[1], $pks[0])')"; - } else { - $className = $this->generateClassName($tableName); - foreach ($table->foreignKeys as $fkName => $fkEntry) { - // Put table and key name in variables for easier reading - $refTable = $fkEntry[0]; // Table name that current fk references to - $refKey = $fkEntry[1]; // Key in that table being referenced - $refClassName = $this->generateClassName($refTable); - - // Add relation for this table - $relationName = $this->generateRelationName($tableName, $fkName, false); - $relations[$className][$relationName] = "array(self::BELONGS_TO, '$refClassName', '$fkName')"; - - // Add relation for the referenced table - $relationType = $table->primaryKey === $fkName ? 'HAS_ONE' : 'HAS_MANY'; - $relationName = $this->generateRelationName($refTable, $this->removePrefix($tableName, false), $relationType === 'HAS_MANY'); - $i = 1; - $rawName = $relationName; - while (isset($relations[$refClassName][$relationName])) { - $relationName = $rawName . ($i++); + $className = $this->generateClassName($tableName); + foreach ($table->foreignKeys as $refs) { + $refTable = $refs[0]; + unset($refs[0]); + $fks = array_keys($refs); + $refClassName = $this->generateClassName($refTable); + + // Add relation for this table + $link = $this->generateRelationLink(array_flip($refs)); + $relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false); + $relations[$className][$relationName] = array( + "return \$this->hasOne('$refClassName', $link);", + $refClassName, + false, + ); + + // Add relation for the referenced table + $hasMany = false; + foreach ($fks as $key) { + if (!in_array($key, $table->primaryKey, true)) { + $hasMany = true; + break; } - $relations[$refClassName][$relationName] = "array(self::$relationType, '$className', '$fkName')"; } + $link = $this->generateRelationLink($refs); + $relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany); + $relations[$refClassName][$relationName] = array( + "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "('$className', $link);", + $className, + $hasMany, + ); + } + + if (($fks = $this->checkPivotTable($table)) === false) { + continue; } + $table0 = $fks[$table->primaryKey[0]][0]; + $table1 = $fks[$table->primaryKey[1]][0]; + $className0 = $this->generateClassName($table0); + $className1 = $this->generateClassName($table1); + + $link = $this->generateRelationLink(array($fks[$table->primaryKey[1]][1] => $table->primaryKey[1])); + $viaLink = $this->generateRelationLink(array($table->primaryKey[0] => $fks[$table->primaryKey[0]][1])); + $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true); + $relations[$className0][$relationName] = array( + "return \$this->hasMany('$className1', $link)->viaTable('{$table->name}', $viaLink);", + $className0, + true, + ); + + $link = $this->generateRelationLink(array($fks[$table->primaryKey[0]][1] => $table->primaryKey[0])); + $viaLink = $this->generateRelationLink(array($table->primaryKey[1] => $fks[$table->primaryKey[1]][1])); + $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true); + $relations[$className1][$relationName] = array( + "return \$this->hasMany('$className0', $link)->viaTable('{$table->name}', $viaLink);", + $className1, + true, + ); } return $relations; } /** - * Checks if the given table is a "many to many" pivot table. - * Their PK has 2 fields, and both of those fields are also FK to other separate tables. - * @param CDbTableSchema table to inspect - * @return boolean true if table matches description of helpter table. + * Generates the link parameter to be used in generating the relation declaration. + * @param array $refs reference constraint + * @return string the generated link parameter. */ - protected function isRelationTable($table) + protected function generateRelationLink($refs) { - $pk = $table->primaryKey; - return (count($pk) === 2 // we want 2 columns - && isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key - && isset($table->foreignKeys[$pk[1]]) // pk column 2 is also a foriegn key - && $table->foreignKeys[$pk[0]][0] !== $table->foreignKeys[$pk[1]][0]); // and the foreign keys point different tables + $pairs = array(); + foreach ($refs as $a => $b) { + $pairs[] = "'$a' => '$b'"; + } + return 'array(' . implode(', ', $pairs) . ')'; } /** - * Generate a name for use as a relation name (inside relations() function in a model). - * @param string the name of the table to hold the relation - * @param string the foreign key name - * @param boolean whether the relation would contain multiple objects - * @return string the relation name + * Checks if the given table is a pivot table. + * For simplicity, this method only deals with the case where the pivot contains two PK columns, + * each referencing a column in a different table. + * @param \yii\db\TableSchema the table being checked + * @return array|boolean the relevant foreign key constraint information if the table is a pivot table, + * or false if the table is not a pivot table. */ - protected function generateRelationName($tableName, $fkName, $multiple) + protected function checkPivotTable($table) { - if (strcasecmp(substr($fkName, -2), 'id') === 0 && strcasecmp($fkName, 'id')) { - $relationName = rtrim(substr($fkName, 0, -2), '_'); + $pk = $table->primaryKey; + if (count($pk) !== 2) { + return false; + } + $fks = array(); + foreach ($table->foreignKeys as $refs) { + if (count($refs) === 2) { + if (isset($refs[$pk[0]])) { + $fks[$pk[0]] = array($refs[0], $refs[$pk[0]]); + } elseif (isset($refs[$pk[1]])) { + $fks[$pk[1]] = array($refs[0], $refs[$pk[1]]); + } + } + } + if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) { + return $fks; } else { - $relationName = $fkName; + return false; } - $relationName[0] = strtolower($relationName); + } - if ($multiple) { - $relationName = $this->pluralize($relationName); + /** + * Generate a relation name for the specified table and a base name. + * @param array $relations the relations being generated currently. + * @param string $className the class name that will contain the relation declarations + * @param \yii\db\TableSchema $table the table schema + * @param string $key a base name that the relation name may be generated from + * @param boolean $multiple whether this is a has-many relation + * @return string the relation name + */ + protected function generateRelationName($relations, $className, $table, $key, $multiple) + { + if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) { + $key = rtrim(substr($key, 0, -2), '_'); } - - $names = preg_split('/_+/', $relationName, -1, PREG_SPLIT_NO_EMPTY); - if (empty($names)) { - return $relationName; - } // unlikely - for ($name = $names[0], $i = 1; $i < count($names); ++$i) { - $name .= ucfirst($names[$i]); + if ($multiple) { + $key = Inflector::pluralize($key); } - - $rawName = $name; - $table = Yii::$app->{$this->connectionId}->schema->getTable($tableName); + $name = $rawName = Inflector::id2camel($key, '_'); $i = 0; while (isset($table->columns[$name])) { $name = $rawName . ($i++); } + while (isset($relations[$className][$name])) { + $name = $rawName . ($i++); + } return $name; } + /** + * Validates the [[db]] attribute. + */ public function validateDb() { if (Yii::$app->hasComponent($this->db) === false) { @@ -374,6 +401,9 @@ class Generator extends \yii\gii\Generator } } + /** + * Validates the [[ns]] attribute. + */ public function validateNamespace() { $this->ns = ltrim($this->ns, '\\'); @@ -383,6 +413,9 @@ class Generator extends \yii\gii\Generator } } + /** + * Validates the [[modelClass]] attribute. + */ public function validateModelClass() { if ($this->isReservedKeyword($this->modelClass)) { @@ -393,6 +426,9 @@ class Generator extends \yii\gii\Generator } } + /** + * Validates the [[tableName]] attribute. + */ public function validateTableName() { if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) { @@ -416,6 +452,9 @@ class Generator extends \yii\gii\Generator private $_tableNames; private $_classNames; + /** + * @return array the table names that match the pattern specified by [[tableName]]. + */ protected function getTableNames() { if ($this->_tableNames !== null) { @@ -444,6 +483,11 @@ class Generator extends \yii\gii\Generator return $this->_tableNames = $tableNames; } + /** + * Generates a class name from the specified table name. + * @param string $tableName the table name (which may contain schema prefix) + * @return string the generated class name + */ protected function generateClassName($tableName) { if (isset($this->_classNames[$tableName])) { @@ -477,4 +521,12 @@ class Generator extends \yii\gii\Generator } return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); } + + /** + * @return Connection the DB connection as specified by [[db]]. + */ + protected function getDbConnection() + { + return Yii::$app->{$this->db}; + } } diff --git a/framework/yii/gii/generators/model/templates/model.php b/framework/yii/gii/generators/model/templates/model.php index 3e3091a..b194294 100644 --- a/framework/yii/gii/generators/model/templates/model.php +++ b/framework/yii/gii/generators/model/templates/model.php @@ -4,18 +4,12 @@ * * @var yii\base\View $this * @var yii\gii\generators\model\Generator $generator - * @var string $tableName - * @var string $className + * @var string $tableName full table name + * @var string $className class name * @var yii\db\TableSchema $tableSchema - * @var string[] $labels - * @var string[] $rules - * - * - $tableName: the table name for this class (prefix is already removed if necessary) - * - $modelClass: the model class name - * - $tableSchema: list of table columns (name=>CDbColumnSchema) - * - $labels: list of attribute labels (name=>label) - * - $rules: list of validation rules - * - $relations: list of relations (name=>relation declaration) + * @var string[] $labels list of attribute labels (name=>label) + * @var string[] $rules list of validation rules + * @var array $relations list of relations (name=>relation declaration) */ echo "ns; ?>; /** * This is the model class for table "". * - * Attributes: - * columns as $column): ?> * @property phpType} \${$column->name}\n"; ?> + + * + $relation): ?> + * @property + + */ class extends baseClass, '\\') . "\n"; ?> { /** * @inheritdoc */ - public function tableName() + public static function tableName() { return ''; } @@ -61,4 +59,14 @@ class extends base ); } + $relation): ?> + + /** + * @return \yii\db\ActiveRelation + */ + public function get() + { + + } + }