PageRenderTime 70ms CodeModel.GetById 39ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/gii/generators/model/ModelCode.php

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