PageRenderTime 28ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/yii/framework/cli/commands/shell/ModelCommand.php

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