mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-20 02:16:41 +08:00
536 lines
16 KiB
PHP
536 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* @link http://www.yiiframework.com/
|
|
* @copyright Copyright (c) 2008 Yii Software LLC
|
|
* @license http://www.yiiframework.com/license/
|
|
*/
|
|
|
|
namespace yii\gii\generators\model;
|
|
|
|
use Yii;
|
|
use yii\db\ActiveRecord;
|
|
use yii\db\Connection;
|
|
use yii\db\Schema;
|
|
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 <qiang.xue@gmail.com>
|
|
* @since 2.0
|
|
*/
|
|
class Generator extends \yii\gii\Generator
|
|
{
|
|
public $db = 'db';
|
|
public $ns = 'app\models';
|
|
public $tableName;
|
|
public $modelClass;
|
|
public $baseClass = 'yii\db\ActiveRecord';
|
|
public $generateRelations = true;
|
|
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(
|
|
array('db, ns, tableName, modelClass, baseClass', 'filter', 'filter' => 'trim'),
|
|
array('db, ns, tableName, baseClass', 'required'),
|
|
array('db, modelClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'),
|
|
array('ns, baseClass', 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'),
|
|
array('tableName', 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'),
|
|
array('db', 'validateDb'),
|
|
array('ns', 'validateNamespace'),
|
|
array('tableName', 'validateTableName'),
|
|
array('modelClass', 'validateModelClass'),
|
|
array('baseClass', 'validateClass', 'params' => array('extends' => ActiveRecord::className())),
|
|
array('generateRelations, generateLabelsFromComments', 'boolean'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function attributeLabels()
|
|
{
|
|
return array(
|
|
'ns' => 'Namespace',
|
|
'db' => 'Database Connection ID',
|
|
'tableName' => 'Table Name',
|
|
'modelClass' => 'Model Class',
|
|
'baseClass' => 'Base Class',
|
|
'generateRelations' => 'Generate Relations',
|
|
'generateLabelsFromComments' => 'Generate Labels from DB Comments',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function hints()
|
|
{
|
|
return array(
|
|
'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>',
|
|
'db' => 'This is the ID of the DB application component.',
|
|
'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. <code>tbl_post</code>.
|
|
The table name may consist of the DB schema part if needed, e.g. <code>public.tbl_post</code>.
|
|
The table name may contain an asterisk to match multiple table names, e.g. <code>tbl_*</code>
|
|
will match tables who name starts with <code>tbl_</code>. In this case, multiple ActiveRecord classes
|
|
will be generated, one for each matching table name; and the class names will be generated from
|
|
the matching characters. For example, table <code>tbl_post</code> will generate <code>Post</code>
|
|
class.',
|
|
'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
|
|
the namespace part as it is specified in "Namespace". You do not need to specify the class name
|
|
if "Table Name" contains an asterisk at the end, in which case multiple ActiveRecord classes will be generated.',
|
|
'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
|
|
'generateRelations' => 'This indicates whether the generator should generate relations based on
|
|
foreign key constraints it detects in the database. Note that if your database contains too many tables,
|
|
you may want to uncheck this option to accelerate the code generation proc ess.',
|
|
'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels
|
|
by using the comments of the corresponding DB columns.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function requiredTemplates()
|
|
{
|
|
return array(
|
|
'model.php',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function stickyAttributes()
|
|
{
|
|
return array('ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments');
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function generate()
|
|
{
|
|
$files = array();
|
|
$relations = $this->generateRelations();
|
|
$db = $this->getDbConnection();
|
|
foreach ($this->getTableNames() as $tableName) {
|
|
$className = $this->generateClassName($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',
|
|
$this->render('model.php', $params)
|
|
);
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
foreach ($table->columns as $column) {
|
|
if ($this->generateLabelsFromComments && !empty($column->comment)) {
|
|
$labels[$column->name] = $column->comment;
|
|
} elseif (!strcasecmp($column->name, 'id')) {
|
|
$labels[$column->name] = 'ID';
|
|
} else {
|
|
$label = Inflector::camel2words($column->name);
|
|
if (strcasecmp(substr($label, -3), ' id') === 0) {
|
|
$label = substr($label, 0, -3) . ' ID';
|
|
}
|
|
$labels[$column->name] = $label;
|
|
}
|
|
}
|
|
return $labels;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
$types = array();
|
|
$lengths = array();
|
|
foreach ($table->columns as $column) {
|
|
if ($column->autoIncrement) {
|
|
continue;
|
|
}
|
|
if (!$column->allowNull && $column->defaultValue === null) {
|
|
$types['required'][] = $column->name;
|
|
}
|
|
switch ($column->type) {
|
|
case Schema::TYPE_SMALLINT:
|
|
case Schema::TYPE_INTEGER:
|
|
case Schema::TYPE_BIGINT:
|
|
$types['integer'][] = $column->name;
|
|
break;
|
|
case Schema::TYPE_BOOLEAN:
|
|
$types['boolean'][] = $column->name;
|
|
break;
|
|
case Schema::TYPE_FLOAT:
|
|
case Schema::TYPE_DECIMAL:
|
|
case Schema::TYPE_MONEY:
|
|
$types['number'][] = $column->name;
|
|
break;
|
|
case Schema::TYPE_DATE:
|
|
case Schema::TYPE_TIME:
|
|
case Schema::TYPE_DATETIME:
|
|
case Schema::TYPE_TIMESTAMP:
|
|
$types['safe'][] = $column->name;
|
|
break;
|
|
default: // strings
|
|
if ($column->size > 0) {
|
|
$lengths[$column->size][] = $column->name;
|
|
} else {
|
|
$types['string'][] = $column->name;
|
|
}
|
|
}
|
|
}
|
|
|
|
$rules = array();
|
|
foreach ($types as $type => $columns) {
|
|
$rules[] = "array('" . implode(', ', $columns) . "', '$type')";
|
|
}
|
|
foreach ($lengths as $length => $columns) {
|
|
$rules[] = "array('" . implode(', ', $columns) . "', 'string', 'max' => $length)";
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* @return array the generated relation declarations
|
|
*/
|
|
protected function generateRelations()
|
|
{
|
|
if (!$this->generateRelations) {
|
|
return array();
|
|
}
|
|
|
|
$db = $this->getDbConnection();
|
|
|
|
if (($pos = strpos($this->tableName, '.')) !== false) {
|
|
$schemaName = substr($this->tableName, 0, $pos);
|
|
} else {
|
|
$schemaName = '';
|
|
}
|
|
|
|
$relations = array();
|
|
foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) {
|
|
$tableName = $table->name;
|
|
$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;
|
|
}
|
|
}
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* 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 generateRelationLink($refs)
|
|
{
|
|
$pairs = array();
|
|
foreach ($refs as $a => $b) {
|
|
$pairs[] = "'$a' => '$b'";
|
|
}
|
|
return 'array(' . implode(', ', $pairs) . ')';
|
|
}
|
|
|
|
/**
|
|
* 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 checkPivotTable($table)
|
|
{
|
|
$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 {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
$key = Inflector::pluralize($key);
|
|
}
|
|
$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) {
|
|
$this->addError('db', 'There is no application component named "db".');
|
|
} elseif (!Yii::$app->getComponent($this->db) instanceof Connection) {
|
|
$this->addError('db', 'The "db" application component must be a DB connection instance.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the [[ns]] attribute.
|
|
*/
|
|
public function validateNamespace()
|
|
{
|
|
$this->ns = ltrim($this->ns, '\\');
|
|
$path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
|
|
if ($path === false) {
|
|
$this->addError('ns', 'Namespace must be associated with an existing directory.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the [[modelClass]] attribute.
|
|
*/
|
|
public function validateModelClass()
|
|
{
|
|
if ($this->isReservedKeyword($this->modelClass)) {
|
|
$this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
|
|
}
|
|
if (strpos($this->tableName, '*') === false && $this->modelClass == '') {
|
|
$this->addError('modelClass', 'Model Class cannot be blank.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the [[tableName]] attribute.
|
|
*/
|
|
public function validateTableName()
|
|
{
|
|
if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) {
|
|
$this->addError('tableName', 'At most one asterisk is allowed.');
|
|
return;
|
|
}
|
|
$tables = $this->getTableNames();
|
|
if (empty($tables)) {
|
|
$this->addError('tableName', "Table '{$this->tableName}' does not exist.");
|
|
} else {
|
|
foreach ($tables as $table) {
|
|
$class = $this->generateClassName($table);
|
|
if ($this->isReservedKeyword($class)) {
|
|
$this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private $_tableNames;
|
|
private $_classNames;
|
|
|
|
/**
|
|
* @return array the table names that match the pattern specified by [[tableName]].
|
|
*/
|
|
protected function getTableNames()
|
|
{
|
|
if ($this->_tableNames !== null) {
|
|
return $this->_tableNames;
|
|
}
|
|
$db = $this->getDbConnection();
|
|
$tableNames = array();
|
|
if (strpos($this->tableName, '*') !== false) {
|
|
if (($pos = strrpos($this->tableName, '.')) !== false) {
|
|
$schema = substr($this->tableName, 0, $pos);
|
|
$pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/';
|
|
} else {
|
|
$schema = '';
|
|
$pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/';
|
|
}
|
|
|
|
foreach ($db->schema->getTableNames($schema) as $table) {
|
|
if (preg_match($pattern, $table)) {
|
|
$tableNames[] = $schema === '' ? $table : ($schema . '.' . $table);
|
|
}
|
|
}
|
|
} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
|
|
$tableNames[] = $this->tableName;
|
|
$this->_classNames[$this->tableName] = $this->modelClass;
|
|
}
|
|
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])) {
|
|
return $this->_classNames[$tableName];
|
|
}
|
|
|
|
if (($pos = strrpos($tableName, '.')) !== false) {
|
|
$tableName = substr($tableName, $pos + 1);
|
|
}
|
|
|
|
$db = $this->getDbConnection();
|
|
$patterns = array();
|
|
if (strpos($this->tableName, '*') !== false) {
|
|
$pattern = $this->tableName;
|
|
if (($pos = strrpos($pattern, '.')) !== false) {
|
|
$pattern = substr($pattern, $pos + 1);
|
|
}
|
|
$patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
|
|
}
|
|
if (!empty($db->tablePrefix)) {
|
|
$patterns[] = "/^{$db->tablePrefix}(.*?)|(.*?){$db->tablePrefix}$/";
|
|
} else {
|
|
$patterns[] = "/^tbl_(.*?)$/";
|
|
}
|
|
|
|
$className = $tableName;
|
|
foreach ($patterns as $pattern) {
|
|
if (preg_match($pattern, $tableName, $matches)) {
|
|
$className = $matches[1];
|
|
}
|
|
}
|
|
return $this->_classNames[$tableName] = Inflector::id2camel($className, '_');
|
|
}
|
|
|
|
/**
|
|
* @return Connection the DB connection as specified by [[db]].
|
|
*/
|
|
protected function getDbConnection()
|
|
{
|
|
return Yii::$app->{$this->db};
|
|
}
|
|
}
|