ModelCommand.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <?php
  2. /**
  3. * ModelCommand class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * ModelCommand generates a model class.
  12. *
  13. * @author Qiang Xue <qiang.xue@gmail.com>
  14. * @package system.cli.commands.shell
  15. * @since 1.0
  16. */
  17. class ModelCommand extends CConsoleCommand
  18. {
  19. /**
  20. * @var string the directory that contains templates for the model command.
  21. * Defaults to null, meaning using 'framework/cli/views/shell/model'.
  22. * If you set this path and some views are missing in the directory,
  23. * the default views will be used.
  24. */
  25. public $templatePath;
  26. /**
  27. * @var string the directory that contains test fixtures.
  28. * Defaults to null, meaning using 'protected/tests/fixtures'.
  29. * If this is false, it means fixture file should NOT be generated.
  30. */
  31. public $fixturePath;
  32. /**
  33. * @var string the directory that contains unit test classes.
  34. * Defaults to null, meaning using 'protected/tests/unit'.
  35. * If this is false, it means unit test file should NOT be generated.
  36. */
  37. public $unitTestPath;
  38. private $_schema;
  39. private $_relations; // where we keep table relations
  40. private $_tables;
  41. private $_classes;
  42. public function getHelp()
  43. {
  44. return <<<EOD
  45. USAGE
  46. model <class-name> [table-name]
  47. DESCRIPTION
  48. This command generates a model class with the specified class name.
  49. PARAMETERS
  50. * class-name: required, model class name. By default, the generated
  51. model class file will be placed under the directory aliased as
  52. 'application.models'. To override this default, specify the class
  53. name in terms of a path alias, e.g., 'application.somewhere.ClassName'.
  54. If the model class belongs to a module, it should be specified
  55. as 'ModuleID.models.ClassName'.
  56. If the class name ends with '*', then a model class will be generated
  57. for EVERY table in the database.
  58. If the class name contains a regular expression deliminated by slashes,
  59. then a model class will be generated for those tables whose name
  60. matches the regular expression. If the regular expression contains
  61. sub-patterns, the first sub-pattern will be used to generate the model
  62. class name.
  63. * table-name: optional, the associated database table name. If not given,
  64. it is assumed to be the model class name.
  65. Note, when the class name ends with '*', this parameter will be
  66. ignored.
  67. EXAMPLES
  68. * Generates the Post model:
  69. model Post
  70. * Generates the Post model which is associated with table 'posts':
  71. model Post posts
  72. * Generates the Post model which should belong to module 'admin':
  73. model admin.models.Post
  74. * Generates a model class for every table in the current database:
  75. model *
  76. * Same as above, but the model class files should be generated
  77. under 'protected/models2':
  78. model application.models2.*
  79. * Generates a model class for every table whose name is prefixed
  80. with 'tbl_' in the current database. The model class will not
  81. contain the table prefix.
  82. model /^tbl_(.*)$/
  83. * Same as above, but the model class files should be generated
  84. under 'protected/models2':
  85. model application.models2./^tbl_(.*)$/
  86. EOD;
  87. }
  88. /**
  89. * Checks if the given table is a "many to many" helper table.
  90. * Their PK has 2 fields, and both of those fields are also FK to other separate tables.
  91. * @param CDbTableSchema $table table to inspect
  92. * @return boolean true if table matches description of helper table.
  93. */
  94. protected function isRelationTable($table)
  95. {
  96. $pk=$table->primaryKey;
  97. return (count($pk) === 2 // we want 2 columns
  98. && isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key
  99. && isset($table->foreignKeys[$pk[1]]) // pk column 2 is also a foreign key
  100. && $table->foreignKeys[$pk[0]][0] !== $table->foreignKeys[$pk[1]][0]); // and the foreign keys point different tables
  101. }
  102. /**
  103. * Generate code to put in ActiveRecord class's relations() function.
  104. * @return array indexed by table names, each entry contains array of php code to go in appropriate ActiveRecord class.
  105. * Empty array is returned if database couldn't be connected.
  106. */
  107. protected function generateRelations()
  108. {
  109. $this->_relations=array();
  110. $this->_classes=array();
  111. foreach($this->_schema->getTables() as $table)
  112. {
  113. $tableName=$table->name;
  114. if ($this->isRelationTable($table))
  115. {
  116. $pks=$table->primaryKey;
  117. $fks=$table->foreignKeys;
  118. $table0=$fks[$pks[1]][0];
  119. $table1=$fks[$pks[0]][0];
  120. $className0=$this->getClassName($table0);
  121. $className1=$this->getClassName($table1);
  122. $unprefixedTableName=$this->removePrefix($tableName,true);
  123. $relationName=$this->generateRelationName($table0, $table1, true);
  124. $this->_relations[$className0][$relationName]="array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')";
  125. $relationName=$this->generateRelationName($table1, $table0, true);
  126. $this->_relations[$className1][$relationName]="array(self::MANY_MANY, '$className0', '$unprefixedTableName($pks[0], $pks[1])')";
  127. }
  128. else
  129. {
  130. $this->_classes[$tableName]=$className=$this->getClassName($tableName);
  131. foreach ($table->foreignKeys as $fkName => $fkEntry)
  132. {
  133. // Put table and key name in variables for easier reading
  134. $refTable=$fkEntry[0]; // Table name that current fk references to
  135. $refKey=$fkEntry[1]; // Key in that table being referenced
  136. $refClassName=$this->getClassName($refTable);
  137. // Add relation for this table
  138. $relationName=$this->generateRelationName($tableName, $fkName, false);
  139. $this->_relations[$className][$relationName]="array(self::BELONGS_TO, '$refClassName', '$fkName')";
  140. // Add relation for the referenced table
  141. $relationType=$table->primaryKey === $fkName ? 'HAS_ONE' : 'HAS_MANY';
  142. $relationName=$this->generateRelationName($refTable, $this->removePrefix($tableName), $relationType==='HAS_MANY');
  143. $this->_relations[$refClassName][$relationName]="array(self::$relationType, '$className', '$fkName')";
  144. }
  145. }
  146. }
  147. }
  148. protected function getClassName($tableName)
  149. {
  150. return isset($this->_tables[$tableName]) ? $this->_tables[$tableName] : $this->generateClassName($tableName);
  151. }
  152. /**
  153. * Generates model class name based on a table name
  154. * @param string $tableName the table name
  155. * @return string the generated model class name
  156. */
  157. protected function generateClassName($tableName)
  158. {
  159. return str_replace(' ','',
  160. ucwords(
  161. trim(
  162. strtolower(
  163. str_replace(array('-','_'),' ',
  164. preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $tableName))))));
  165. }
  166. /**
  167. * Generates the mapping table between table names and class names.
  168. * @param CDbSchema $schema the database schema
  169. * @param string $pattern a regular expression that may be used to filter table names
  170. */
  171. protected function generateClassNames($schema,$pattern=null)
  172. {
  173. $this->_tables=array();
  174. foreach($schema->getTableNames() as $name)
  175. {
  176. if($pattern===null)
  177. $this->_tables[$name]=$this->generateClassName($this->removePrefix($name));
  178. elseif(preg_match($pattern,$name,$matches))
  179. {
  180. if(count($matches)>1 && !empty($matches[1]))
  181. $className=$this->generateClassName($matches[1]);
  182. else
  183. $className=$this->generateClassName($matches[0]);
  184. $this->_tables[$name]=empty($className) ? $name : $className;
  185. }
  186. }
  187. }
  188. /**
  189. * Generate a name for use as a relation name (inside relations() function in a model).
  190. * @param string $tableName the name of the table to hold the relation
  191. * @param string $fkName the foreign key name
  192. * @param boolean $multiple whether the relation would contain multiple objects
  193. * @return string the generated relation name
  194. */
  195. protected function generateRelationName($tableName, $fkName, $multiple)
  196. {
  197. if(strcasecmp(substr($fkName,-2),'id')===0 && strcasecmp($fkName,'id'))
  198. $relationName=rtrim(substr($fkName, 0, -2),'_');
  199. else
  200. $relationName=$fkName;
  201. $relationName[0]=strtolower($relationName);
  202. $rawName=$relationName;
  203. if($multiple)
  204. $relationName=$this->pluralize($relationName);
  205. $table=$this->_schema->getTable($tableName);
  206. $i=0;
  207. while(isset($table->columns[$relationName]))
  208. $relationName=$rawName.($i++);
  209. return $relationName;
  210. }
  211. /**
  212. * Execute the action.
  213. * @param array $args command line parameters specific for this command
  214. * @return integer|null non zero application exit code for help or null on success
  215. */
  216. public function run($args)
  217. {
  218. if(!isset($args[0]))
  219. {
  220. echo "Error: model class name is required.\n";
  221. echo $this->getHelp();
  222. return 1;
  223. }
  224. $className=$args[0];
  225. if(($db=Yii::app()->getDb())===null)
  226. {
  227. echo "Error: an active 'db' connection is required.\n";
  228. echo "If you already added 'db' component in application configuration,\n";
  229. echo "please quit and re-enter the yiic shell.\n";
  230. return 1;
  231. }
  232. $db->active=true;
  233. $this->_schema=$db->schema;
  234. if(!preg_match('/^[\w\.\-\*]*(.*?)$/',$className,$matches))
  235. {
  236. echo "Error: model class name is invalid.\n";
  237. return 1;
  238. }
  239. if(empty($matches[1])) // without regular expression
  240. {
  241. $this->generateClassNames($this->_schema);
  242. if(($pos=strrpos($className,'.'))===false)
  243. $basePath=Yii::getPathOfAlias('application.models');
  244. else
  245. {
  246. $basePath=Yii::getPathOfAlias(substr($className,0,$pos));
  247. $className=substr($className,$pos+1);
  248. }
  249. if($className==='*') // generate all models
  250. $this->generateRelations();
  251. else
  252. {
  253. $tableName=isset($args[1])?$args[1]:$className;
  254. $tableName=$this->addPrefix($tableName);
  255. $this->_tables[$tableName]=$className;
  256. $this->generateRelations();
  257. $this->_classes=array($tableName=>$className);
  258. }
  259. }
  260. else // with regular expression
  261. {
  262. $pattern=$matches[1];
  263. $pos=strrpos($className,$pattern);
  264. if($pos>0) // only regexp is given
  265. $basePath=Yii::getPathOfAlias(rtrim(substr($className,0,$pos),'.'));
  266. else
  267. $basePath=Yii::getPathOfAlias('application.models');
  268. $this->generateClassNames($this->_schema,$pattern);
  269. $classes=$this->_tables;
  270. $this->generateRelations();
  271. $this->_classes=$classes;
  272. }
  273. if(count($this->_classes)>1)
  274. {
  275. $entries=array();
  276. $count=0;
  277. foreach($this->_classes as $tableName=>$className)
  278. $entries[]=++$count.". $className ($tableName)";
  279. echo "The following model classes (tables) match your criteria:\n";
  280. echo implode("\n",$entries)."\n\n";
  281. if(!$this->confirm("Do you want to generate the above classes?"))
  282. return;
  283. }
  284. $templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/model':$this->templatePath;
  285. $fixturePath=$this->fixturePath===null?Yii::getPathOfAlias('application.tests.fixtures'):$this->fixturePath;
  286. $unitTestPath=$this->unitTestPath===null?Yii::getPathOfAlias('application.tests.unit'):$this->unitTestPath;
  287. $list=array();
  288. $files=array();
  289. foreach ($this->_classes as $tableName=>$className)
  290. {
  291. $files[$className]=$classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';
  292. $list['models/'.$className.'.php']=array(
  293. 'source'=>$templatePath.DIRECTORY_SEPARATOR.'model.php',
  294. 'target'=>$classFile,
  295. 'callback'=>array($this,'generateModel'),
  296. 'params'=>array($className,$tableName),
  297. );
  298. if($fixturePath!==false)
  299. {
  300. $list['fixtures/'.$tableName.'.php']=array(
  301. 'source'=>$templatePath.DIRECTORY_SEPARATOR.'fixture.php',
  302. 'target'=>$fixturePath.DIRECTORY_SEPARATOR.$tableName.'.php',
  303. 'callback'=>array($this,'generateFixture'),
  304. 'params'=>$this->_schema->getTable($tableName),
  305. );
  306. }
  307. if($unitTestPath!==false)
  308. {
  309. $fixtureName=$this->pluralize($className);
  310. $fixtureName[0]=strtolower($fixtureName);
  311. $list['unit/'.$className.'Test.php']=array(
  312. 'source'=>$templatePath.DIRECTORY_SEPARATOR.'test.php',
  313. 'target'=>$unitTestPath.DIRECTORY_SEPARATOR.$className.'Test.php',
  314. 'callback'=>array($this,'generateTest'),
  315. 'params'=>array($className,$fixtureName),
  316. );
  317. }
  318. }
  319. $this->copyFiles($list);
  320. foreach($files as $className=>$file)
  321. {
  322. if(!class_exists($className,false))
  323. include_once($file);
  324. }
  325. $classes=implode(", ", $this->_classes);
  326. echo <<<EOD
  327. The following model classes are successfully generated:
  328. $classes
  329. If you have a 'db' database connection, you can test these models now with:
  330. \$model={$className}::model()->find();
  331. print_r(\$model);
  332. EOD;
  333. }
  334. public function generateModel($source,$params)
  335. {
  336. list($className,$tableName)=$params;
  337. $rules=array();
  338. $labels=array();
  339. $relations=array();
  340. if(($table=$this->_schema->getTable($tableName))!==null)
  341. {
  342. $required=array();
  343. $integers=array();
  344. $numerical=array();
  345. $length=array();
  346. $safe=array();
  347. foreach($table->columns as $column)
  348. {
  349. $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $column->name)))));
  350. $label=preg_replace('/\s+/',' ',$label);
  351. if(strcasecmp(substr($label,-3),' id')===0)
  352. $label=substr($label,0,-3);
  353. $labels[$column->name]=$label;
  354. if($column->isPrimaryKey && $table->sequenceName!==null)
  355. continue;
  356. $r=!$column->allowNull && $column->defaultValue===null;
  357. if($r)
  358. $required[]=$column->name;
  359. if($column->type==='integer')
  360. $integers[]=$column->name;
  361. elseif($column->type==='double')
  362. $numerical[]=$column->name;
  363. elseif($column->type==='string' && $column->size>0)
  364. $length[$column->size][]=$column->name;
  365. elseif(!$column->isPrimaryKey && !$r)
  366. $safe[]=$column->name;
  367. }
  368. if($required!==array())
  369. $rules[]="array('".implode(', ',$required)."', 'required')";
  370. if($integers!==array())
  371. $rules[]="array('".implode(', ',$integers)."', 'numerical', 'integerOnly'=>true)";
  372. if($numerical!==array())
  373. $rules[]="array('".implode(', ',$numerical)."', 'numerical')";
  374. if($length!==array())
  375. {
  376. foreach($length as $len=>$cols)
  377. $rules[]="array('".implode(', ',$cols)."', 'length', 'max'=>$len)";
  378. }
  379. if($safe!==array())
  380. $rules[]="array('".implode(', ',$safe)."', 'safe')";
  381. if(isset($this->_relations[$className]) && is_array($this->_relations[$className]))
  382. $relations=$this->_relations[$className];
  383. }
  384. else
  385. echo "Warning: the table '$tableName' does not exist in the database.\n";
  386. if(!is_file($source)) // fall back to default ones
  387. $source=YII_PATH.'/cli/views/shell/model/'.basename($source);
  388. return $this->renderFile($source,array(
  389. 'className'=>$className,
  390. 'tableName'=>$this->removePrefix($tableName,true),
  391. 'columns'=>isset($table) ? $table->columns : array(),
  392. 'rules'=>$rules,
  393. 'labels'=>$labels,
  394. 'relations'=>$relations,
  395. ),true);
  396. }
  397. public function generateFixture($source,$table)
  398. {
  399. if(!is_file($source)) // fall back to default ones
  400. $source=YII_PATH.'/cli/views/shell/model/'.basename($source);
  401. return $this->renderFile($source, array(
  402. 'table'=>$table,
  403. ),true);
  404. }
  405. public function generateTest($source,$params)
  406. {
  407. list($className,$fixtureName)=$params;
  408. if(!is_file($source)) // fall back to default ones
  409. $source=YII_PATH.'/cli/views/shell/model/'.basename($source);
  410. return $this->renderFile($source, array(
  411. 'className'=>$className,
  412. 'fixtureName'=>$fixtureName,
  413. ),true);
  414. }
  415. protected function removePrefix($tableName,$addBrackets=false)
  416. {
  417. $tablePrefix=Yii::app()->getDb()->tablePrefix;
  418. if($tablePrefix!='' && !strncmp($tableName,$tablePrefix,strlen($tablePrefix)))
  419. {
  420. $tableName=substr($tableName,strlen($tablePrefix));
  421. if($addBrackets)
  422. $tableName='{{'.$tableName.'}}';
  423. }
  424. return $tableName;
  425. }
  426. protected function addPrefix($tableName)
  427. {
  428. $tablePrefix=Yii::app()->getDb()->tablePrefix;
  429. if($tablePrefix!='' && strncmp($tableName,$tablePrefix,strlen($tablePrefix)))
  430. $tableName=$tablePrefix.$tableName;
  431. return $tableName;
  432. }
  433. }