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