ModelCode.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <?php
  2. class ModelCode extends CCodeModel
  3. {
  4. public $connectionId='db';
  5. public $tablePrefix;
  6. public $tableName;
  7. public $modelClass;
  8. public $modelPath='application.models';
  9. public $baseClass='CActiveRecord';
  10. public $buildRelations=true;
  11. public $commentsAsLabels=false;
  12. /**
  13. * @var array list of candidate relation code. The array are indexed by AR class names and relation names.
  14. * Each element represents the code of the one relation in one AR class.
  15. */
  16. protected $relations;
  17. public function rules()
  18. {
  19. return array_merge(parent::rules(), array(
  20. array('tablePrefix, baseClass, tableName, modelClass, modelPath, connectionId', 'filter', 'filter'=>'trim'),
  21. array('connectionId, tableName, modelPath, baseClass', 'required'),
  22. array('tablePrefix, tableName, modelPath', 'match', 'pattern'=>'/^(\w+[\w\.]*|\*?|\w+\.\*)$/', 'message'=>'{attribute} should only contain word characters, dots, and an optional ending asterisk.'),
  23. array('connectionId', 'validateConnectionId', 'skipOnError'=>true),
  24. array('tableName', 'validateTableName', 'skipOnError'=>true),
  25. array('tablePrefix, modelClass', 'match', 'pattern'=>'/^[a-zA-Z_]\w*$/', 'message'=>'{attribute} should only contain word characters.'),
  26. array('baseClass', 'match', 'pattern'=>'/^[a-zA-Z_\\\\][\w\\\\]*$/', 'message'=>'{attribute} should only contain word characters and backslashes.'),
  27. array('modelPath', 'validateModelPath', 'skipOnError'=>true),
  28. array('baseClass, modelClass', 'validateReservedWord', 'skipOnError'=>true),
  29. array('baseClass', 'validateBaseClass', 'skipOnError'=>true),
  30. array('connectionId, tablePrefix, modelPath, baseClass, buildRelations, commentsAsLabels', 'sticky'),
  31. ));
  32. }
  33. public function attributeLabels()
  34. {
  35. return array_merge(parent::attributeLabels(), array(
  36. 'tablePrefix'=>'Table Prefix',
  37. 'tableName'=>'Table Name',
  38. 'modelPath'=>'Model Path',
  39. 'modelClass'=>'Model Class',
  40. 'baseClass'=>'Base Class',
  41. 'buildRelations'=>'Build Relations',
  42. 'commentsAsLabels'=>'Use Column Comments as Attribute Labels',
  43. 'connectionId'=>'Database Connection',
  44. ));
  45. }
  46. public function requiredTemplates()
  47. {
  48. return array(
  49. 'model.php',
  50. );
  51. }
  52. public function init()
  53. {
  54. if(Yii::app()->{$this->connectionId}===null)
  55. throw new CHttpException(500,'A valid database connection is required to run this generator.');
  56. $this->tablePrefix=Yii::app()->{$this->connectionId}->tablePrefix;
  57. parent::init();
  58. }
  59. public function prepare()
  60. {
  61. if(($pos=strrpos($this->tableName,'.'))!==false)
  62. {
  63. $schema=substr($this->tableName,0,$pos);
  64. $tableName=substr($this->tableName,$pos+1);
  65. }
  66. else
  67. {
  68. $schema='';
  69. $tableName=$this->tableName;
  70. }
  71. if($tableName[strlen($tableName)-1]==='*')
  72. {
  73. $tables=Yii::app()->{$this->connectionId}->schema->getTables($schema);
  74. if($this->tablePrefix!='')
  75. {
  76. foreach($tables as $i=>$table)
  77. {
  78. if(strpos($table->name,$this->tablePrefix)!==0)
  79. unset($tables[$i]);
  80. }
  81. }
  82. }
  83. else
  84. $tables=array($this->getTableSchema($this->tableName));
  85. $this->files=array();
  86. $templatePath=$this->templatePath;
  87. $this->relations=$this->generateRelations();
  88. foreach($tables as $table)
  89. {
  90. $tableName=$this->removePrefix($table->name);
  91. $className=$this->generateClassName($table->name);
  92. $params=array(
  93. 'tableName'=>$schema==='' ? $tableName : $schema.'.'.$tableName,
  94. 'modelClass'=>$className,
  95. 'columns'=>$table->columns,
  96. 'labels'=>$this->generateLabels($table),
  97. 'rules'=>$this->generateRules($table),
  98. 'relations'=>isset($this->relations[$className]) ? $this->relations[$className] : array(),
  99. 'connectionId'=>$this->connectionId,
  100. );
  101. $this->files[]=new CCodeFile(
  102. Yii::getPathOfAlias($this->modelPath).'/'.$className.'.php',
  103. $this->render($templatePath.'/model.php', $params)
  104. );
  105. }
  106. }
  107. public function validateTableName($attribute,$params)
  108. {
  109. if($this->hasErrors())
  110. return;
  111. $invalidTables=array();
  112. $invalidColumns=array();
  113. if($this->tableName[strlen($this->tableName)-1]==='*')
  114. {
  115. if(($pos=strrpos($this->tableName,'.'))!==false)
  116. $schema=substr($this->tableName,0,$pos);
  117. else
  118. $schema='';
  119. $this->modelClass='';
  120. $tables=Yii::app()->{$this->connectionId}->schema->getTables($schema);
  121. foreach($tables as $table)
  122. {
  123. if($this->tablePrefix=='' || strpos($table->name,$this->tablePrefix)===0)
  124. {
  125. if(in_array(strtolower($table->name),self::$keywords))
  126. $invalidTables[]=$table->name;
  127. if(($invalidColumn=$this->checkColumns($table))!==null)
  128. $invalidColumns[]=$invalidColumn;
  129. }
  130. }
  131. }
  132. else
  133. {
  134. if(($table=$this->getTableSchema($this->tableName))===null)
  135. $this->addError('tableName',"Table '{$this->tableName}' does not exist.");
  136. if($this->modelClass==='')
  137. $this->addError('modelClass','Model Class cannot be blank.');
  138. if(!$this->hasErrors($attribute) && ($invalidColumn=$this->checkColumns($table))!==null)
  139. $invalidColumns[]=$invalidColumn;
  140. }
  141. if($invalidTables!=array())
  142. $this->addError('tableName', 'Model class cannot take a reserved PHP keyword! Table name: '.implode(', ', $invalidTables).".");
  143. if($invalidColumns!=array())
  144. $this->addError('tableName', 'Column names that does not follow PHP variable naming convention: '.implode(', ', $invalidColumns).".");
  145. }
  146. /*
  147. * Check that all database field names conform to PHP variable naming rules
  148. * For example mysql allows field name like "2011aa", but PHP does not allow variable like "$model->2011aa"
  149. * @param CDbTableSchema $table the table schema object
  150. * @return string the invalid table column name. Null if no error.
  151. */
  152. public function checkColumns($table)
  153. {
  154. foreach($table->columns as $column)
  155. {
  156. if(!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/',$column->name))
  157. return $table->name.'.'.$column->name;
  158. }
  159. }
  160. public function validateModelPath($attribute,$params)
  161. {
  162. if(Yii::getPathOfAlias($this->modelPath)===false)
  163. $this->addError('modelPath','Model Path must be a valid path alias.');
  164. }
  165. public function validateBaseClass($attribute,$params)
  166. {
  167. $class=@Yii::import($this->baseClass,true);
  168. if(!is_string($class) || !$this->classExists($class))
  169. $this->addError('baseClass', "Class '{$this->baseClass}' does not exist or has syntax error.");
  170. elseif($class!=='CActiveRecord' && !is_subclass_of($class,'CActiveRecord'))
  171. $this->addError('baseClass', "'{$this->baseClass}' must extend from CActiveRecord.");
  172. }
  173. public function getTableSchema($tableName)
  174. {
  175. $connection=Yii::app()->{$this->connectionId};
  176. return $connection->getSchema()->getTable($tableName, $connection->schemaCachingDuration!==0);
  177. }
  178. public function generateLabels($table)
  179. {
  180. $labels=array();
  181. foreach($table->columns as $column)
  182. {
  183. if($this->commentsAsLabels && $column->comment)
  184. $labels[$column->name]=$column->comment;
  185. else
  186. {
  187. $label=ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $column->name)))));
  188. $label=preg_replace('/\s+/',' ',$label);
  189. if(strcasecmp(substr($label,-3),' id')===0)
  190. $label=substr($label,0,-3);
  191. if($label==='Id')
  192. $label='ID';
  193. $label=str_replace("'","\\'",$label);
  194. $labels[$column->name]=$label;
  195. }
  196. }
  197. return $labels;
  198. }
  199. public function generateRules($table)
  200. {
  201. $rules=array();
  202. $required=array();
  203. $integers=array();
  204. $numerical=array();
  205. $length=array();
  206. $safe=array();
  207. foreach($table->columns as $column)
  208. {
  209. if($column->autoIncrement)
  210. continue;
  211. $r=!$column->allowNull && $column->defaultValue===null;
  212. if($r)
  213. $required[]=$column->name;
  214. if($column->type==='integer')
  215. $integers[]=$column->name;
  216. elseif($column->type==='double')
  217. $numerical[]=$column->name;
  218. elseif($column->type==='string' && $column->size>0)
  219. $length[$column->size][]=$column->name;
  220. elseif(!$column->isPrimaryKey && !$r)
  221. $safe[]=$column->name;
  222. }
  223. if($required!==array())
  224. $rules[]="array('".implode(', ',$required)."', 'required')";
  225. if($integers!==array())
  226. $rules[]="array('".implode(', ',$integers)."', 'numerical', 'integerOnly'=>true)";
  227. if($numerical!==array())
  228. $rules[]="array('".implode(', ',$numerical)."', 'numerical')";
  229. if($length!==array())
  230. {
  231. foreach($length as $len=>$cols)
  232. $rules[]="array('".implode(', ',$cols)."', 'length', 'max'=>$len)";
  233. }
  234. if($safe!==array())
  235. $rules[]="array('".implode(', ',$safe)."', 'safe')";
  236. return $rules;
  237. }
  238. public function getRelations($className)
  239. {
  240. return isset($this->relations[$className]) ? $this->relations[$className] : array();
  241. }
  242. protected function removePrefix($tableName,$addBrackets=true)
  243. {
  244. if($addBrackets && Yii::app()->{$this->connectionId}->tablePrefix=='')
  245. return $tableName;
  246. $prefix=$this->tablePrefix!='' ? $this->tablePrefix : Yii::app()->{$this->connectionId}->tablePrefix;
  247. if($prefix!='')
  248. {
  249. if($addBrackets && Yii::app()->{$this->connectionId}->tablePrefix!='')
  250. {
  251. $prefix=Yii::app()->{$this->connectionId}->tablePrefix;
  252. $lb='{{';
  253. $rb='}}';
  254. }
  255. else
  256. $lb=$rb='';
  257. if(($pos=strrpos($tableName,'.'))!==false)
  258. {
  259. $schema=substr($tableName,0,$pos);
  260. $name=substr($tableName,$pos+1);
  261. if(strpos($name,$prefix)===0)
  262. return $schema.'.'.$lb.substr($name,strlen($prefix)).$rb;
  263. }
  264. elseif(strpos($tableName,$prefix)===0)
  265. return $lb.substr($tableName,strlen($prefix)).$rb;
  266. }
  267. return $tableName;
  268. }
  269. protected function generateRelations()
  270. {
  271. if(!$this->buildRelations)
  272. return array();
  273. $schemaName='';
  274. if(($pos=strpos($this->tableName,'.'))!==false)
  275. $schemaName=substr($this->tableName,0,$pos);
  276. $relations=array();
  277. foreach(Yii::app()->{$this->connectionId}->schema->getTables($schemaName) as $table)
  278. {
  279. if($this->tablePrefix!='' && strpos($table->name,$this->tablePrefix)!==0)
  280. continue;
  281. $tableName=$table->name;
  282. if ($this->isRelationTable($table))
  283. {
  284. $pks=$table->primaryKey;
  285. $fks=$table->foreignKeys;
  286. $table0=$fks[$pks[0]][0];
  287. $table1=$fks[$pks[1]][0];
  288. $className0=$this->generateClassName($table0);
  289. $className1=$this->generateClassName($table1);
  290. $unprefixedTableName=$this->removePrefix($tableName);
  291. $relationName=$this->generateRelationName($table0, $table1, true);
  292. $relations[$className0][$relationName]="array(self::MANY_MANY, '$className1', '$unprefixedTableName($pks[0], $pks[1])')";
  293. $relationName=$this->generateRelationName($table1, $table0, true);
  294. $i=1;
  295. $rawName=$relationName;
  296. while(isset($relations[$className1][$relationName]))
  297. $relationName=$rawName.$i++;
  298. $relations[$className1][$relationName]="array(self::MANY_MANY, '$className0', '$unprefixedTableName($pks[1], $pks[0])')";
  299. }
  300. else
  301. {
  302. $className=$this->generateClassName($tableName);
  303. foreach ($table->foreignKeys as $fkName => $fkEntry)
  304. {
  305. // Put table and key name in variables for easier reading
  306. $refTable=$fkEntry[0]; // Table name that current fk references to
  307. $refKey=$fkEntry[1]; // Key in that table being referenced
  308. $refClassName=$this->generateClassName($refTable);
  309. // Add relation for this table
  310. $relationName=$this->generateRelationName($tableName, $fkName, false);
  311. $relations[$className][$relationName]="array(self::BELONGS_TO, '$refClassName', '$fkName')";
  312. // Add relation for the referenced table
  313. $relationType=$table->primaryKey === $fkName ? 'HAS_ONE' : 'HAS_MANY';
  314. $relationName=$this->generateRelationName($refTable, $this->removePrefix($tableName,false), $relationType==='HAS_MANY');
  315. $i=1;
  316. $rawName=$relationName;
  317. while(isset($relations[$refClassName][$relationName]))
  318. $relationName=$rawName.($i++);
  319. $relations[$refClassName][$relationName]="array(self::$relationType, '$className', '$fkName')";
  320. }
  321. }
  322. }
  323. return $relations;
  324. }
  325. /**
  326. * Checks if the given table is a "many to many" pivot table.
  327. * Their PK has 2 fields, and both of those fields are also FK to other separate tables.
  328. * @param CDbTableSchema table to inspect
  329. * @return boolean true if table matches description of helper table.
  330. */
  331. protected function isRelationTable($table)
  332. {
  333. $pk=$table->primaryKey;
  334. return (count($pk) === 2 // we want 2 columns
  335. && isset($table->foreignKeys[$pk[0]]) // pk column 1 is also a foreign key
  336. && isset($table->foreignKeys[$pk[1]]) // pk column 2 is also a foriegn key
  337. && $table->foreignKeys[$pk[0]][0] !== $table->foreignKeys[$pk[1]][0]); // and the foreign keys point different tables
  338. }
  339. protected function generateClassName($tableName)
  340. {
  341. if($this->tableName===$tableName || ($pos=strrpos($this->tableName,'.'))!==false && substr($this->tableName,$pos+1)===$tableName)
  342. return $this->modelClass;
  343. $tableName=$this->removePrefix($tableName,false);
  344. if(($pos=strpos($tableName,'.'))!==false) // remove schema part (e.g. remove 'public2.' from 'public2.post')
  345. $tableName=substr($tableName,$pos+1);
  346. $className='';
  347. foreach(explode('_',$tableName) as $name)
  348. {
  349. if($name!=='')
  350. $className.=ucfirst($name);
  351. }
  352. return $className;
  353. }
  354. /**
  355. * Generate a name for use as a relation name (inside relations() function in a model).
  356. * @param string the name of the table to hold the relation
  357. * @param string the foreign key name
  358. * @param boolean whether the relation would contain multiple objects
  359. * @return string the relation name
  360. */
  361. protected function generateRelationName($tableName, $fkName, $multiple)
  362. {
  363. if(strcasecmp(substr($fkName,-2),'id')===0 && strcasecmp($fkName,'id'))
  364. $relationName=rtrim(substr($fkName, 0, -2),'_');
  365. else
  366. $relationName=$fkName;
  367. $relationName[0]=strtolower($relationName);
  368. if($multiple)
  369. $relationName=$this->pluralize($relationName);
  370. $names=preg_split('/_+/',$relationName,-1,PREG_SPLIT_NO_EMPTY);
  371. if(empty($names)) return $relationName; // unlikely
  372. for($name=$names[0], $i=1;$i<count($names);++$i)
  373. $name.=ucfirst($names[$i]);
  374. $rawName=$name;
  375. $table=Yii::app()->{$this->connectionId}->schema->getTable($tableName);
  376. $i=0;
  377. while(isset($table->columns[$name]))
  378. $name=$rawName.($i++);
  379. return $name;
  380. }
  381. public function validateConnectionId($attribute, $params)
  382. {
  383. if(Yii::app()->hasComponent($this->connectionId)===false || !(Yii::app()->getComponent($this->connectionId) instanceof CDbConnection))
  384. $this->addError('connectionId','A valid database connection is required to run this generator.');
  385. }
  386. }