diff options
Diffstat (limited to 'framework/cli/commands/shell/ModelCommand.php')
| -rw-r--r-- | framework/cli/commands/shell/ModelCommand.php | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/framework/cli/commands/shell/ModelCommand.php b/framework/cli/commands/shell/ModelCommand.php new file mode 100644 index 0000000..cfd8c21 --- /dev/null +++ b/framework/cli/commands/shell/ModelCommand.php @@ -0,0 +1,488 @@ +<?php +/** + * ModelCommand class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @version $Id: ModelCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $ + */ + +/** + * ModelCommand generates a model class. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: ModelCommand.php 3477 2011-12-06 22:33:37Z alexander.makarow $ + * @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 <<<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); + + $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 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($this->removePrefix($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; + } + + /** + * 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; + $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 <<<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; + $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('/(?<![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) + continue; + $r=!$column->allowNull && $column->defaultValue===null; + if($r) + $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) + $length[$column->size][]=$column->name; + else if(!$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; + } +}
\ No newline at end of file |
