Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
418 lines (368 sloc) 12.7 KB
<?php
/**
* ModelCommand class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008-2009 Yii Software LLC
* @license http://www.yiiframework.com/license/
* @version $Id$
*/
/**
* ModelCommand generates a model class.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @version $Id$
* @package system.cli.commands.shell
* @since 1.0
*/
class ModelCommand extends CConsoleCommand
{
/**
* @var string the template file for the model class.
* Defaults to null, meaning using 'framework/cli/views/shell/model/model.php'.
*/
public $templateFile;
private $_schema;
private $_relations; // where we keep table relations
private $_tables;
private $_classes;
public function getHelp()
{
return <<<EOD
USAGE
model <class-name> [table-name]
DESCRIPTION
This command generates a model class with the specified class name.
PARAMETERS
* class-name: required, model class name. By default, the generated
model class file will be placed under the directory aliased as
'application.models'. To override this default, specify the class
name in terms of a path alias, e.g., 'application.somewhere.ClassName'.
If the model class belongs to a module, it should be specified
as 'ModuleID.models.ClassName'.
If the class name ends with '*', then a model class will be generated
for EVERY table in the database.
If the class name contains a regular expression deliminated by slashes,
then a model class will be generated for those tables whose name
matches the regular expression. If the regular expression contains
sub-patterns, the first sub-pattern will be used to generate the model
class name.
* table-name: optional, the associated database table name. If not given,
it is assumed to be the model class name.
Note, when the class name ends with '*', this parameter will be
ignored.
EXAMPLES
* Generates the Post model:
model Post
* Generates the Post model which is associated with table 'posts':
model Post posts
* Generates the Post model which should belong to module 'admin':
model admin.models.Post
* Generates a model class for every table in the current database:
model *
* Same as above, but the model class files should be generated
under 'protected/models2':
model application.models2.*
* Generates a model class for every table whose name is prefixed
with 'tbl_' in the current database. The model class will not
contain the table prefix.
model /^tbl_(.*)$/
* Same as above, but the model class files should be generated
under 'protected/models2':
model application.models2./^tbl_(.*)$/
EOD;
}
/**
* Checks if the given table is a "many to many" helper 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.
*/
protected function isRelationTable($table)
{
$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
}
/**
* Generate code to put in ActiveRecord class's relations() function.
* @return array indexed by table names, each entry contains array of php code to go in appropriate ActiveRecord class.
* Empty array is returned if database couldn't be connected.
*/
protected function generateRelations()
{
$this->_relations=array();
$this->_classes=array();
foreach($this->_schema->getTables() as $table)
{
$tableName=$table->name;
if ($this->isRelationTable($table))
{
$pks=$table->primaryKey;
$fks=$table->foreignKeys;
$table0=$fks[$pks[1]][0];
$table1=$fks[$pks[0]][0];
$className0=$this->getClassName($table0);
$className1=$this->getClassName($table1);
$relationName=$this->generateRelationName($table0, $table1, true);
$this->_relations[$className0][$relationName]="array(self::MANY_MANY, '$className1', '$tableName($pks[0], $pks[1])')";
$relationName=$this->generateRelationName($table1, $table0, true);
$this->_relations[$className1][$relationName]="array(self::MANY_MANY, '$className0', '$tableName($pks[0], $pks[1])')";
}
else
{
$this->_classes[$tableName]=$className=$this->getClassName($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->getClassName($refTable);
// Add relation for this table
$relationName=$this->generateRelationName($tableName, $fkName, false);
$this->_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, $tableName, $relationType==='HAS_MANY');
$this->_relations[$refClassName][$relationName]="array(self::$relationType, '$className', '$fkName')";
}
}
}
}
protected function getClassName($tableName)
{
return isset($this->_tables[$tableName]) ? $this->_tables[$tableName] : $this->generateClassName($tableName);
}
/**
* Generates model class name based on a table name
* @param string the table name
* @return string the generated model class name
*/
protected function generateClassName($tableName)
{
return str_replace(' ','',
ucwords(
trim(
strtolower(
str_replace(array('-','_'),' ',
preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $tableName))))));
}
/**
* Generates the mapping table between table names and class names.
* @param CDbSchema the database schema
* @param string a regular expression that may be used to filter table names
*/
protected function generateClassNames($schema,$pattern=null)
{
$this->_tables=array();
foreach($schema->getTableNames() as $name)
{
if($pattern===null)
$this->_tables[$name]=$this->generateClassName($name);
else if(preg_match($pattern,$name,$matches))
{
if(count($matches)>1 && !empty($matches[1]))
$className=$this->generateClassName($matches[1]);
else
$className=$this->generateClassName($matches[0]);
$this->_tables[$name]=empty($className) ? $name : $className;
}
}
}
/**
* 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
*/
protected function generateRelationName($tableName, $fkName, $multiple)
{
if(strcasecmp(substr($fkName,-2),'id')===0 && strcasecmp($fkName,'id'))
$relationName=rtrim(substr($fkName, 0, -2),'_');
else
$relationName=$fkName;
$relationName[0]=strtolower($relationName);
$rawName=$relationName;
if($multiple)
$relationName=$this->pluralize($relationName);
$table=$this->_schema->getTable($tableName);
$i=0;
while(isset($table->columns[$relationName]))
$relationName=$rawName.($i++);
return $relationName;
}
/**
* Converts a word to its plural form.
* @param string the word to be pluralized
* @return string the pluralized word
*/
protected function pluralize($name)
{
$rules=array(
'/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
'/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
'/(m)an$/i' => '\1en',
'/(child)$/i' => '\1ren',
'/(r)y$/i' => '\1ies',
'/s$/' => 's',
);
foreach($rules as $rule=>$replacement)
{
if(preg_match($rule,$name))
return preg_replace($rule,$replacement,$name);
}
return $name.'s';
}
/**
* Execute the action.
* @param array command line parameters specific for this command
*/
public function run($args)
{
if(!isset($args[0]))
{
echo "Error: model class name is required.\n";
echo $this->getHelp();
return;
}
$className=$args[0];
if(($db=Yii::app()->getDb())===null)
{
echo "Error: an active 'db' connection is required.\n";
echo "If you already added 'db' component in application configuration,\n";
echo "please quit and re-enter the yiic shell.\n";
return;
}
$db->active=true;
$this->_schema=$db->schema;
if(!preg_match('/^[\w\.\-\*]*(.*?)$/',$className,$matches))
{
echo "Error: model class name is invalid.\n";
return;
}
if(empty($matches[1])) // without regular expression
{
$this->generateClassNames($this->_schema);
if(($pos=strrpos($className,'.'))===false)
$basePath=Yii::getPathOfAlias('application.models');
else
{
$basePath=Yii::getPathOfAlias(substr($className,0,$pos));
$className=substr($className,$pos+1);
}
if($className==='*') // generate all models
$this->generateRelations();
else
{
$tableName=isset($args[1])?$args[1]:$className;
$this->_tables[$tableName]=$className;
$this->generateRelations();
$this->_classes=array($tableName=>$className);
}
}
else // with regular expression
{
$pattern=$matches[1];
$pos=strrpos($className,$pattern);
if($pos>0) // only regexp is given
$basePath=Yii::getPathOfAlias(rtrim(substr($className,0,$pos),'.'));
else
$basePath=Yii::getPathOfAlias('application.models');
$this->generateClassNames($this->_schema,$pattern);
$classes=$this->_tables;
$this->generateRelations();
$this->_classes=$classes;
}
if(count($this->_classes)>1)
{
$entries=array();
$count=0;
foreach($this->_classes as $tableName=>$className)
$entries[]=++$count.". $className ($tableName)";
echo "The following model classes (tables) match your criteria:\n";
echo implode("\n",$entries);
echo "\n\nDo you want to generate the above classes? [Yes|No] ";
if(strncasecmp(trim(fgets(STDIN)),'y',1))
return;
}
$list=array();
foreach ($this->_classes as $tableName=>$className)
{
$files[$className]=$classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';
$templateFile=$this->templateFile===null?YII_PATH.'/cli/views/shell/model/model.php':$this->templateFile;
$list[$className.'.php']=array(
'source'=>$templateFile,
'target'=>$classFile,
'callback'=>array($this,'generateModel'),
'params'=>array($className,$tableName),
);
}
$this->copyFiles($list);
foreach($files as $className=>$file)
{
if(!class_exists($className,false))
include_once($file);
}
$classes=implode(", ", $this->_classes);
echo <<<EOD
The following model classes are successfully generated:
$classes
If you have a 'db' database connection, you can test these models now with:
\$model={$className}::model()->find();
print_r(\$model);
EOD;
}
public function generateModel($source,$params)
{
list($className,$tableName)=$params;
$content=file_get_contents($source);
$rules=array();
$labels=array();
$relations=array();
if(($table=$this->_schema->getTable($tableName))!==null)
{
$required=array();
$integers=array();
$numerical=array();
foreach($table->columns as $column)
{
$label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $column->name)))));
$label=preg_replace('/\s+/',' ',$label);
if(strcasecmp(substr($label,-3),' id')===0)
$label=substr($label,0,-3);
$labels[]="'{$column->name}'=>'$label'";
if($column->isPrimaryKey && $table->sequenceName!==null || $column->isForeignKey)
continue;
if(!$column->allowNull && $column->defaultValue===null)
$required[]=$column->name;
if($column->type==='integer')
$integers[]=$column->name;
else if($column->type==='double')
$numerical[]=$column->name;
else if($column->type==='string' && $column->size>0)
$rules[]="array('{$column->name}','length','max'=>{$column->size})";
}
if($required!==array())
$rules[]="array('".implode(', ',$required)."', 'required')";
if($integers!==array())
$rules[]="array('".implode(', ',$integers)."', 'numerical', 'integerOnly'=>true)";
if($numerical!==array())
$rules[]="array('".implode(', ',$numerical)."', 'numerical')";
if(isset($this->_relations[$className]) && is_array($this->_relations[$className]))
$relations=$this->_relations[$className];
}
else
echo "Warning: the table '$tableName' does not exist in the database.\n";
return $this->renderFile($source,array(
'className'=>$className,
'tableName'=>$tableName,
'columns'=>isset($table) ? $table->columns : array(),
'rules'=>$rules,
'labels'=>$labels,
'relations'=>$relations,
),true);
}
}
Something went wrong with that request. Please try again.