Commit 7150524e by Qiang Xue

Finished model generator.

parent b968402e
...@@ -138,7 +138,7 @@ abstract class Schema extends Object ...@@ -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 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, * @param boolean $refresh whether to fetch the latest available table schemas. If this is false,
* cached data may be returned if available. * 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. * Each array element is an instance of [[TableSchema]] or its child class.
*/ */
public function getTableSchemas($schema = '', $refresh = false) public function getTableSchemas($schema = '', $refresh = false)
...@@ -161,7 +161,7 @@ abstract class Schema extends Object ...@@ -161,7 +161,7 @@ abstract class Schema extends Object
* If not empty, the returned table names will be prefixed with the schema name. * 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, * @param boolean $refresh whether to fetch the latest available table names. If this is false,
* table names fetched previously (if available) will be returned. * 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) public function getTableNames($schema = '', $refresh = false)
{ {
......
...@@ -15,6 +15,7 @@ use yii\gii\CodeFile; ...@@ -15,6 +15,7 @@ use yii\gii\CodeFile;
use yii\helpers\Inflector; use yii\helpers\Inflector;
/** /**
* This generator will generate one or multiple ActiveRecord classes for the specified database table.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
...@@ -30,16 +31,25 @@ class Generator extends \yii\gii\Generator ...@@ -30,16 +31,25 @@ class Generator extends \yii\gii\Generator
public $generateLabelsFromComments = false; public $generateLabelsFromComments = false;
/**
* @inheritdoc
*/
public function getName() public function getName()
{ {
return 'Model Generator'; return 'Model Generator';
} }
/**
* @inheritdoc
*/
public function getDescription() public function getDescription()
{ {
return 'This generator generates an ActiveRecord class for the specified database table.'; return 'This generator generates an ActiveRecord class for the specified database table.';
} }
/**
* @inheritdoc
*/
public function rules() public function rules()
{ {
return array_merge(parent::rules(), array( return array_merge(parent::rules(), array(
...@@ -57,6 +67,9 @@ class Generator extends \yii\gii\Generator ...@@ -57,6 +67,9 @@ class Generator extends \yii\gii\Generator
)); ));
} }
/**
* @inheritdoc
*/
public function attributeLabels() public function attributeLabels()
{ {
return array( return array(
...@@ -70,6 +83,9 @@ class Generator extends \yii\gii\Generator ...@@ -70,6 +83,9 @@ class Generator extends \yii\gii\Generator
); );
} }
/**
* @inheritdoc
*/
public function hints() public function hints()
{ {
return array( return array(
...@@ -91,6 +107,9 @@ class Generator extends \yii\gii\Generator ...@@ -91,6 +107,9 @@ class Generator extends \yii\gii\Generator
); );
} }
/**
* @inheritdoc
*/
public function requiredTemplates() public function requiredTemplates()
{ {
return array( return array(
...@@ -98,31 +117,32 @@ class Generator extends \yii\gii\Generator ...@@ -98,31 +117,32 @@ class Generator extends \yii\gii\Generator
); );
} }
/**
* @inheritdoc
*/
public function stickyAttributes() public function stickyAttributes()
{ {
return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments'); return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments');
} }
/** /**
* @return Connection * @inheritdoc
*/ */
public function getDbConnection()
{
return Yii::$app->{$this->db};
}
public function generate() public function generate()
{ {
$files = array(); $files = array();
$relations = $this->generateRelations();
$db = $this->getDbConnection();
foreach ($this->getTableNames() as $tableName) { foreach ($this->getTableNames() as $tableName) {
$className = $this->generateClassName($tableName); $className = $this->generateClassName($tableName);
$tableSchema = $this->getTableSchema($tableName); $tableSchema = $db->getTableSchema($tableName);
$params = array( $params = array(
'tableName' => $tableName, 'tableName' => $tableName,
'className' => $className, 'className' => $className,
'tableSchema' => $tableSchema, 'tableSchema' => $tableSchema,
'labels' => $this->generateLabels($tableSchema), 'labels' => $this->generateLabels($tableSchema),
'rules' => $this->generateRules($tableSchema), 'rules' => $this->generateRules($tableSchema),
'relations' => isset($relations[$className]) ? $relations[$className] : array(),
); );
$files[] = new CodeFile( $files[] = new CodeFile(
Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php', Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
...@@ -133,11 +153,11 @@ class Generator extends \yii\gii\Generator ...@@ -133,11 +153,11 @@ class Generator extends \yii\gii\Generator
return $files; return $files;
} }
public function getTableSchema($tableName) /**
{ * Generates the attribute labels for the specified table.
return $this->getDbConnection()->getTableSchema($tableName, true); * @param \yii\db\TableSchema $table the table schema
} * @return array the generated attribute labels (name => label)
*/
public function generateLabels($table) public function generateLabels($table)
{ {
$labels = array(); $labels = array();
...@@ -158,8 +178,9 @@ class Generator extends \yii\gii\Generator ...@@ -158,8 +178,9 @@ class Generator extends \yii\gii\Generator
} }
/** /**
* @param \yii\db\TableSchema $table * Generates validation rules for the specified table.
* @return array * @param \yii\db\TableSchema $table the table schema
* @return array the generated validation rules
*/ */
public function generateRules($table) public function generateRules($table)
{ {
...@@ -212,159 +233,165 @@ class Generator extends \yii\gii\Generator ...@@ -212,159 +233,165 @@ class Generator extends \yii\gii\Generator
return $rules; return $rules;
} }
public function getRelations($className) /**
{ * @return array the generated relation declarations
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;
}
protected function generateRelations() protected function generateRelations()
{ {
if (!$this->generateRelations) { if (!$this->generateRelations) {
return array(); return array();
} }
$schemaName = ''; $db = $this->getDbConnection();
if (($pos = strpos($this->tableName, '.')) !== false) { if (($pos = strpos($this->tableName, '.')) !== false) {
$schemaName = substr($this->tableName, 0, $pos); $schemaName = substr($this->tableName, 0, $pos);
} else {
$schemaName = '';
} }
$relations = array(); $relations = array();
foreach (Yii::$app->{$this->connectionId}->schema->getTables($schemaName) as $table) { foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) {
if ($this->tablePrefix != '' && strpos($table->name, $this->tablePrefix) !== 0) {
continue;
}
$tableName = $table->name; $tableName = $table->name;
$className = $this->generateClassName($tableName);
if ($this->isRelationTable($table)) { foreach ($table->foreignKeys as $refs) {
$pks = $table->primaryKey; $refTable = $refs[0];
$fks = $table->foreignKeys; unset($refs[0]);
$fks = array_keys($refs);
$table0 = $fks[$pks[0]][0]; $refClassName = $this->generateClassName($refTable);
$table1 = $fks[$pks[1]][0];
$className0 = $this->generateClassName($table0); // Add relation for this table
$className1 = $this->generateClassName($table1); $link = $this->generateRelationLink(array_flip($refs));
$relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false);
$unprefixedTableName = $this->removePrefix($tableName); $relations[$className][$relationName] = array(
"return \$this->hasOne('$refClassName', $link);",
$relationName = $this->generateRelationName($table0, $table1, true); $refClassName,
$relations[$className0][$relationName] = "array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')"; false,
);
$relationName = $this->generateRelationName($table1, $table0, true);
// Add relation for the referenced table
$i = 1; $hasMany = false;
$rawName = $relationName; foreach ($fks as $key) {
while (isset($relations[$className1][$relationName])) { if (!in_array($key, $table->primaryKey, true)) {
$relationName = $rawName . $i++; $hasMany = true;
} break;
$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++);
} }
$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; return $relations;
} }
/** /**
* Checks if the given table is a "many to many" pivot table. * Generates the link parameter to be used in generating the relation declaration.
* Their PK has 2 fields, and both of those fields are also FK to other separate tables. * @param array $refs reference constraint
* @param CDbTableSchema table to inspect * @return string the generated link parameter.
* @return boolean true if table matches description of helpter table.
*/ */
protected function isRelationTable($table) protected function generateRelationLink($refs)
{ {
$pk = $table->primaryKey; $pairs = array();
return (count($pk) === 2 // we want 2 columns foreach ($refs as $a => $b) {
&& isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key $pairs[] = "'$a' => '$b'";
&& 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 return 'array(' . implode(', ', $pairs) . ')';
} }
/** /**
* Generate a name for use as a relation name (inside relations() function in a model). * Checks if the given table is a pivot table.
* @param string the name of the table to hold the relation * For simplicity, this method only deals with the case where the pivot contains two PK columns,
* @param string the foreign key name * each referencing a column in a different table.
* @param boolean whether the relation would contain multiple objects * @param \yii\db\TableSchema the table being checked
* @return string the relation name * @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')) { $pk = $table->primaryKey;
$relationName = rtrim(substr($fkName, 0, -2), '_'); 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 { } 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), '_');
} }
if ($multiple) {
$names = preg_split('/_+/', $relationName, -1, PREG_SPLIT_NO_EMPTY); $key = Inflector::pluralize($key);
if (empty($names)) {
return $relationName;
} // unlikely
for ($name = $names[0], $i = 1; $i < count($names); ++$i) {
$name .= ucfirst($names[$i]);
} }
$name = $rawName = Inflector::id2camel($key, '_');
$rawName = $name;
$table = Yii::$app->{$this->connectionId}->schema->getTable($tableName);
$i = 0; $i = 0;
while (isset($table->columns[$name])) { while (isset($table->columns[$name])) {
$name = $rawName . ($i++); $name = $rawName . ($i++);
} }
while (isset($relations[$className][$name])) {
$name = $rawName . ($i++);
}
return $name; return $name;
} }
/**
* Validates the [[db]] attribute.
*/
public function validateDb() public function validateDb()
{ {
if (Yii::$app->hasComponent($this->db) === false) { if (Yii::$app->hasComponent($this->db) === false) {
...@@ -374,6 +401,9 @@ class Generator extends \yii\gii\Generator ...@@ -374,6 +401,9 @@ class Generator extends \yii\gii\Generator
} }
} }
/**
* Validates the [[ns]] attribute.
*/
public function validateNamespace() public function validateNamespace()
{ {
$this->ns = ltrim($this->ns, '\\'); $this->ns = ltrim($this->ns, '\\');
...@@ -383,6 +413,9 @@ class Generator extends \yii\gii\Generator ...@@ -383,6 +413,9 @@ class Generator extends \yii\gii\Generator
} }
} }
/**
* Validates the [[modelClass]] attribute.
*/
public function validateModelClass() public function validateModelClass()
{ {
if ($this->isReservedKeyword($this->modelClass)) { if ($this->isReservedKeyword($this->modelClass)) {
...@@ -393,6 +426,9 @@ class Generator extends \yii\gii\Generator ...@@ -393,6 +426,9 @@ class Generator extends \yii\gii\Generator
} }
} }
/**
* Validates the [[tableName]] attribute.
*/
public function validateTableName() public function validateTableName()
{ {
if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) { if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) {
...@@ -416,6 +452,9 @@ class Generator extends \yii\gii\Generator ...@@ -416,6 +452,9 @@ class Generator extends \yii\gii\Generator
private $_tableNames; private $_tableNames;
private $_classNames; private $_classNames;
/**
* @return array the table names that match the pattern specified by [[tableName]].
*/
protected function getTableNames() protected function getTableNames()
{ {
if ($this->_tableNames !== null) { if ($this->_tableNames !== null) {
...@@ -444,6 +483,11 @@ class Generator extends \yii\gii\Generator ...@@ -444,6 +483,11 @@ class Generator extends \yii\gii\Generator
return $this->_tableNames = $tableNames; 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) protected function generateClassName($tableName)
{ {
if (isset($this->_classNames[$tableName])) { if (isset($this->_classNames[$tableName])) {
...@@ -477,4 +521,12 @@ class Generator extends \yii\gii\Generator ...@@ -477,4 +521,12 @@ class Generator extends \yii\gii\Generator
} }
return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); return $this->_classNames[$tableName] = Inflector::id2camel($className, '_');
} }
/**
* @return Connection the DB connection as specified by [[db]].
*/
protected function getDbConnection()
{
return Yii::$app->{$this->db};
}
} }
...@@ -4,18 +4,12 @@ ...@@ -4,18 +4,12 @@
* *
* @var yii\base\View $this * @var yii\base\View $this
* @var yii\gii\generators\model\Generator $generator * @var yii\gii\generators\model\Generator $generator
* @var string $tableName * @var string $tableName full table name
* @var string $className * @var string $className class name
* @var yii\db\TableSchema $tableSchema * @var yii\db\TableSchema $tableSchema
* @var string[] $labels * @var string[] $labels list of attribute labels (name=>label)
* @var string[] $rules * @var string[] $rules list of validation rules
* * @var array $relations list of relations (name=>relation declaration)
* - $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)
*/ */
echo "<?php\n"; echo "<?php\n";
...@@ -26,18 +20,22 @@ namespace <?php echo $generator->ns; ?>; ...@@ -26,18 +20,22 @@ namespace <?php echo $generator->ns; ?>;
/** /**
* This is the model class for table "<?php echo $tableName; ?>". * This is the model class for table "<?php echo $tableName; ?>".
* *
* Attributes:
*
<?php foreach ($tableSchema->columns as $column): ?> <?php foreach ($tableSchema->columns as $column): ?>
* @property <?php echo "{$column->phpType} \${$column->name}\n"; ?> * @property <?php echo "{$column->phpType} \${$column->name}\n"; ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (!empty($relations)): ?>
*
<?php foreach ($relations as $name => $relation): ?>
* @property <?php echo $relation[1] . ($relation[2] ? '[]' : '') . ' $' . lcfirst($name) . "\n"; ?>
<?php endforeach; ?>
<?php endif; ?>
*/ */
class <?php echo $className; ?> extends <?php echo '\\' . ltrim($generator->baseClass, '\\') . "\n"; ?> class <?php echo $className; ?> extends <?php echo '\\' . ltrim($generator->baseClass, '\\') . "\n"; ?>
{ {
/** /**
* @inheritdoc * @inheritdoc
*/ */
public function tableName() public static function tableName()
{ {
return '<?php echo $tableName; ?>'; return '<?php echo $tableName; ?>';
} }
...@@ -61,4 +59,14 @@ class <?php echo $className; ?> extends <?php echo '\\' . ltrim($generator->base ...@@ -61,4 +59,14 @@ class <?php echo $className; ?> extends <?php echo '\\' . ltrim($generator->base
<?php endforeach; ?> <?php endforeach; ?>
); );
} }
<?php foreach ($relations as $name => $relation): ?>
/**
* @return \yii\db\ActiveRelation
*/
public function get<?php echo $name; ?>()
{
<?php echo $relation[0] . "\n"; ?>
}
<?php endforeach; ?>
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment