* @link http://www.yiiframework.com/ * @copyright 2008-2013 Yii Software LLC * @license http://www.yiiframework.com/license/ */ /** * ModelCommand generates a model class. * * @author Qiang Xue * @package system.cli.commands.shell * @since 1.0 */ class ModelCommand extends CConsoleCommand { /** * @var string the directory that contains templates for the model command. * Defaults to null, meaning using 'framework/cli/views/shell/model'. * If you set this path and some views are missing in the directory, * the default views will be used. */ public $templatePath; /** * @var string the directory that contains test fixtures. * Defaults to null, meaning using 'protected/tests/fixtures'. * If this is false, it means fixture file should NOT be generated. */ public $fixturePath; /** * @var string the directory that contains unit test classes. * Defaults to null, meaning using 'protected/tests/unit'. * If this is false, it means unit test file should NOT be generated. */ public $unitTestPath; private $_schema; private $_relations; // where we keep table relations private $_tables; private $_classes; public function getHelp() { return << [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 table to inspect * @return boolean true if table matches description of helper 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 foreign 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); $unprefixedTableName=$this->removePrefix($tableName,true); $relationName=$this->generateRelationName($table0, $table1, true); $this->_relations[$className0][$relationName]="array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')"; $relationName=$this->generateRelationName($table1, $table0, true); $this->_relations[$className1][$relationName]="array(self::MANY_MANY, '$className0', '$unprefixedTableName($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, $this->removePrefix($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 $tableName 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('/(?_tables=array(); foreach($schema->getTableNames() as $name) { if($pattern===null) $this->_tables[$name]=$this->generateClassName($this->removePrefix($name)); elseif(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 $tableName the name of the table to hold the relation * @param string $fkName the foreign key name * @param boolean $multiple whether the relation would contain multiple objects * @return string the generated relation name */ 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; } /** * Execute the action. * @param array $args command line parameters specific for this command * @return integer|null non zero application exit code for help or null on success */ public function run($args) { if(!isset($args[0])) { echo "Error: model class name is required.\n"; echo $this->getHelp(); return 1; } $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 1; } $db->active=true; $this->_schema=$db->schema; if(!preg_match('/^[\w\.\-\*]*(.*?)$/',$className,$matches)) { echo "Error: model class name is invalid.\n"; return 1; } 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; $tableName=$this->addPrefix($tableName); $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)."\n\n"; if(!$this->confirm("Do you want to generate the above classes?")) return; } $templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/model':$this->templatePath; $fixturePath=$this->fixturePath===null?Yii::getPathOfAlias('application.tests.fixtures'):$this->fixturePath; $unitTestPath=$this->unitTestPath===null?Yii::getPathOfAlias('application.tests.unit'):$this->unitTestPath; $list=array(); $files=array(); foreach ($this->_classes as $tableName=>$className) { $files[$className]=$classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php'; $list['models/'.$className.'.php']=array( 'source'=>$templatePath.DIRECTORY_SEPARATOR.'model.php', 'target'=>$classFile, 'callback'=>array($this,'generateModel'), 'params'=>array($className,$tableName), ); if($fixturePath!==false) { $list['fixtures/'.$tableName.'.php']=array( 'source'=>$templatePath.DIRECTORY_SEPARATOR.'fixture.php', 'target'=>$fixturePath.DIRECTORY_SEPARATOR.$tableName.'.php', 'callback'=>array($this,'generateFixture'), 'params'=>$this->_schema->getTable($tableName), ); } if($unitTestPath!==false) { $fixtureName=$this->pluralize($className); $fixtureName[0]=strtolower($fixtureName); $list['unit/'.$className.'Test.php']=array( 'source'=>$templatePath.DIRECTORY_SEPARATOR.'test.php', 'target'=>$unitTestPath.DIRECTORY_SEPARATOR.$className.'Test.php', 'callback'=>array($this,'generateTest'), 'params'=>array($className,$fixtureName), ); } } $this->copyFiles($list); foreach($files as $className=>$file) { if(!class_exists($className,false)) include_once($file); } $classes=implode(", ", $this->_classes); echo <<find(); print_r(\$model); EOD; } public function generateModel($source,$params) { list($className,$tableName)=$params; $rules=array(); $labels=array(); $relations=array(); if(($table=$this->_schema->getTable($tableName))!==null) { $required=array(); $integers=array(); $numerical=array(); $length=array(); $safe=array(); foreach($table->columns as $column) { $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?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) continue; $r=!$column->allowNull && $column->defaultValue===null; if($r) $required[]=$column->name; if($column->type==='integer') $integers[]=$column->name; elseif($column->type==='double') $numerical[]=$column->name; elseif($column->type==='string' && $column->size>0) $length[$column->size][]=$column->name; elseif(!$column->isPrimaryKey && !$r) $safe[]=$column->name; } 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($length!==array()) { foreach($length as $len=>$cols) $rules[]="array('".implode(', ',$cols)."', 'length', 'max'=>$len)"; } if($safe!==array()) $rules[]="array('".implode(', ',$safe)."', 'safe')"; 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"; if(!is_file($source)) // fall back to default ones $source=YII_PATH.'/cli/views/shell/model/'.basename($source); return $this->renderFile($source,array( 'className'=>$className, 'tableName'=>$this->removePrefix($tableName,true), 'columns'=>isset($table) ? $table->columns : array(), 'rules'=>$rules, 'labels'=>$labels, 'relations'=>$relations, ),true); } public function generateFixture($source,$table) { if(!is_file($source)) // fall back to default ones $source=YII_PATH.'/cli/views/shell/model/'.basename($source); return $this->renderFile($source, array( 'table'=>$table, ),true); } public function generateTest($source,$params) { list($className,$fixtureName)=$params; if(!is_file($source)) // fall back to default ones $source=YII_PATH.'/cli/views/shell/model/'.basename($source); return $this->renderFile($source, array( 'className'=>$className, 'fixtureName'=>$fixtureName, ),true); } protected function removePrefix($tableName,$addBrackets=false) { $tablePrefix=Yii::app()->getDb()->tablePrefix; if($tablePrefix!='' && !strncmp($tableName,$tablePrefix,strlen($tablePrefix))) { $tableName=substr($tableName,strlen($tablePrefix)); if($addBrackets) $tableName='{{'.$tableName.'}}'; } return $tableName; } protected function addPrefix($tableName) { $tablePrefix=Yii::app()->getDb()->tablePrefix; if($tablePrefix!='' && strncmp($tableName,$tablePrefix,strlen($tablePrefix))) $tableName=$tablePrefix.$tableName; return $tableName; } }