1<?php 2/* 3 * $Id: Builder.php 2939 2007-10-19 14:23:42Z Jonathan.Wage $ 4 * 5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 * 17 * This software consists of voluntary contributions made by many individuals 18 * and is licensed under the LGPL. For more information, see 19 * <http://www.doctrine-project.org>. 20 */ 21 22/** 23 * Doctrine_Migration_Builder 24 * 25 * @package Doctrine 26 * @subpackage Migration 27 * @author Konsta Vesterinen <kvesteri@cc.hut.fi> 28 * @author Jonathan H. Wage <jwage@mac.com> 29 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL 30 * @link www.doctrine-project.org 31 * @since 1.0 32 * @version $Revision: 2939 $ 33 */ 34class Doctrine_Migration_Builder extends Doctrine_Builder 35{ 36 /** 37 * The path to your migration classes directory 38 * 39 * @var string 40 */ 41 private $migrationsPath = ''; 42 43 /** 44 * File suffix to use when writing class definitions 45 * 46 * @var string $suffix 47 */ 48 private $suffix = '.php'; 49 50 /** 51 * Instance of the migration class for the migration classes directory 52 * 53 * @var Doctrine_Migration $migration 54 */ 55 private $migration; 56 57 /** 58 * Class template used for writing classes 59 * 60 * @var $tpl 61 */ 62 private static $tpl; 63 64 /** 65 * Instantiate new instance of the Doctrine_Migration_Builder class 66 * 67 * <code> 68 * $builder = new Doctrine_Migration_Builder('/path/to/migrations'); 69 * </code> 70 * 71 * @return void 72 */ 73 public function __construct($migrationsPath = null) 74 { 75 if ($migrationsPath instanceof Doctrine_Migration) { 76 $this->setMigrationsPath($migrationsPath->getMigrationClassesDirectory()); 77 $this->migration = $migrationsPath; 78 } else if (is_dir($migrationsPath)) { 79 $this->setMigrationsPath($migrationsPath); 80 $this->migration = new Doctrine_Migration($migrationsPath); 81 } 82 83 $this->loadTemplate(); 84 } 85 86 /** 87 * Set the path to write the generated migration classes 88 * 89 * @param string path the path where migration classes are stored and being generated 90 * @return void 91 */ 92 public function setMigrationsPath($path) 93 { 94 Doctrine_Lib::makeDirectories($path); 95 96 $this->migrationsPath = $path; 97 } 98 99 /** 100 * Get the path where generated migration classes are written to 101 * 102 * @return string the path where migration classes are stored and being generated 103 */ 104 public function getMigrationsPath() 105 { 106 return $this->migrationsPath; 107 } 108 109 /** 110 * Loads the class template used for generating classes 111 * 112 * @return void 113 */ 114 protected function loadTemplate() 115 { 116 if (isset(self::$tpl)) { 117 return; 118 } 119 120 self::$tpl =<<<END 121/** 122 * This class has been auto-generated by the Doctrine ORM Framework 123 */ 124class %s extends %s 125{ 126 public function up() 127 { 128%s 129 } 130 131 public function down() 132 { 133%s 134 } 135} 136END; 137 } 138 139 /** 140 * Generate migrations from a Doctrine_Migration_Diff instance 141 * 142 * @param Doctrine_Migration_Diff $diff Instance to generate changes from 143 * @return array $changes Array of changes produced from the diff 144 */ 145 public function generateMigrationsFromDiff(Doctrine_Migration_Diff $diff) 146 { 147 $changes = $diff->generateChanges(); 148 149 $up = array(); 150 $down = array(); 151 152 if ( ! empty($changes['dropped_tables'])) { 153 foreach ($changes['dropped_tables'] as $tableName => $table) { 154 $up[] = $this->buildDropTable($table); 155 $down[] = $this->buildCreateTable($table); 156 } 157 } 158 159 if ( ! empty($changes['created_tables'])) { 160 foreach ($changes['created_tables'] as $tableName => $table) { 161 $up[] = $this->buildCreateTable($table); 162 $down[] = $this->buildDropTable($table); 163 } 164 } 165 166 if ( ! empty($changes['dropped_columns'])) { 167 foreach ($changes['dropped_columns'] as $tableName => $removedColumns) { 168 foreach ($removedColumns as $name => $column) { 169 $up[] = $this->buildRemoveColumn($tableName, $name, $column); 170 $down[] = $this->buildAddColumn($tableName, $name, $column); 171 } 172 } 173 } 174 175 if ( ! empty($changes['created_columns'])) { 176 foreach ($changes['created_columns'] as $tableName => $addedColumns) { 177 foreach ($addedColumns as $name => $column) { 178 $up[] = $this->buildAddColumn($tableName, $name, $column); 179 $down[] = $this->buildRemoveColumn($tableName, $name, $column); 180 } 181 } 182 } 183 184 if ( ! empty($changes['changed_columns'])) { 185 foreach ($changes['changed_columns'] as $tableName => $changedColumns) { 186 foreach ($changedColumns as $name => $column) { 187 $up[] = $this->buildChangeColumn($tableName, $name, $column); 188 } 189 } 190 } 191 192 if ( ! empty($up) || ! empty($down)) { 193 $up = implode("\n", $up); 194 $down = implode("\n", $down); 195 $className = 'Version' . $this->migration->getNextMigrationClassVersion(); 196 $this->generateMigrationClass($className, array(), $up, $down); 197 } 198 199 $up = array(); 200 $down = array(); 201 if ( ! empty($changes['dropped_foreign_keys'])) { 202 foreach ($changes['dropped_foreign_keys'] as $tableName => $droppedFks) { 203 if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 204 continue; 205 } 206 207 foreach ($droppedFks as $name => $foreignKey) { 208 $up[] = $this->buildDropForeignKey($tableName, $foreignKey); 209 $down[] = $this->buildCreateForeignKey($tableName, $foreignKey); 210 } 211 } 212 } 213 214 if ( ! empty($changes['dropped_indexes'])) { 215 foreach ($changes['dropped_indexes'] as $tableName => $removedIndexes) { 216 if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 217 continue; 218 } 219 220 foreach ($removedIndexes as $name => $index) { 221 $up[] = $this->buildRemoveIndex($tableName, $name, $index); 222 $down[] = $this->buildAddIndex($tableName, $name, $index); 223 } 224 } 225 } 226 227 if ( ! empty($changes['created_foreign_keys'])) { 228 foreach ($changes['created_foreign_keys'] as $tableName => $createdFks) { 229 if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 230 continue; 231 } 232 233 foreach ($createdFks as $name => $foreignKey) { 234 $up[] = $this->buildCreateForeignKey($tableName, $foreignKey); 235 $down[] = $this->buildDropForeignKey($tableName, $foreignKey); 236 } 237 } 238 } 239 240 if ( ! empty($changes['created_indexes'])) { 241 foreach ($changes['created_indexes'] as $tableName => $addedIndexes) { 242 if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 243 continue; 244 } 245 246 foreach ($addedIndexes as $name => $index) { 247 if (isset($changes['created_tables'][$tableName]['options']['indexes'][$name])) { 248 continue; 249 } 250 $up[] = $this->buildAddIndex($tableName, $name, $index); 251 $down[] = $this->buildRemoveIndex($tableName, $name, $index); 252 } 253 } 254 } 255 256 if ( ! empty($up) || ! empty($down)) { 257 $up = implode("\n", $up); 258 $down = implode("\n", $down); 259 $className = 'Version' . $this->migration->getNextMigrationClassVersion(); 260 $this->generateMigrationClass($className, array(), $up, $down); 261 } 262 return $changes; 263 } 264 265 /** 266 * Generate a set of migration classes from the existing databases 267 * 268 * @return void 269 */ 270 public function generateMigrationsFromDb() 271 { 272 $directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'tmp_doctrine_models'; 273 274 Doctrine_Core::generateModelsFromDb($directory); 275 276 $result = $this->generateMigrationsFromModels($directory, Doctrine_Core::MODEL_LOADING_CONSERVATIVE); 277 278 Doctrine_Lib::removeDirectories($directory); 279 280 return $result; 281 } 282 283 /** 284 * Generate a set of migrations from a set of models 285 * 286 * @param string $modelsPath Path to models 287 * @param string $modelLoading What type of model loading to use when loading the models 288 * @return boolean 289 */ 290 public function generateMigrationsFromModels($modelsPath = null, $modelLoading = null) 291 { 292 if ($modelsPath !== null) { 293 $models = Doctrine_Core::filterInvalidModels(Doctrine_Core::loadModels($modelsPath, $modelLoading)); 294 } else { 295 $models = Doctrine_Core::getLoadedModels(); 296 } 297 298 $models = Doctrine_Core::initializeModels($models); 299 300 $foreignKeys = array(); 301 302 foreach ($models as $model) { 303 $table = Doctrine_Core::getTable($model); 304 if ($table->getTableName() !== $this->migration->getTableName()) { 305 $export = $table->getExportableFormat(); 306 307 $foreignKeys[$export['tableName']] = $export['options']['foreignKeys']; 308 309 $up = $this->buildCreateTable($export); 310 $down = $this->buildDropTable($export); 311 312 $className = 'Add' . Doctrine_Inflector::classify($export['tableName']); 313 314 $this->generateMigrationClass($className, array(), $up, $down); 315 } 316 } 317 318 if ( ! empty($foreignKeys)) { 319 $className = 'AddFks'; 320 321 $up = array(); 322 $down = array(); 323 foreach ($foreignKeys as $tableName => $definitions) { 324 $tableForeignKeyNames[$tableName] = array(); 325 326 foreach ($definitions as $definition) { 327 $up[] = $this->buildCreateForeignKey($tableName, $definition); 328 $down[] = $this->buildDropForeignKey($tableName, $definition); 329 } 330 } 331 332 $up = implode("\n", $up); 333 $down = implode("\n", $down); 334 if ($up || $down) { 335 $this->generateMigrationClass($className, array(), $up, $down); 336 } 337 } 338 339 return true; 340 } 341 342 /** 343 * Build the code for creating foreign keys 344 * 345 * @param string $tableName 346 * @param array $definition 347 * @return string $code 348 */ 349 public function buildCreateForeignKey($tableName, $definition) 350 { 351 return " \$this->createForeignKey('" . $tableName . "', '" . $definition['name'] . "', " . $this->varExport($definition, true) . ");"; 352 } 353 354 /** 355 * Build the code for dropping foreign keys 356 * 357 * @param string $tableName 358 * @param array $definition 359 * @return string $code 360 */ 361 public function buildDropForeignKey($tableName, $definition) 362 { 363 return " \$this->dropForeignKey('" . $tableName . "', '" . $definition['name'] . "');"; 364 } 365 366 /** 367 * Build the code for creating tables 368 * 369 * @param string $tableData 370 * @return string $code 371 */ 372 public function buildCreateTable($tableData) 373 { 374 $code = " \$this->createTable('" . $tableData['tableName'] . "', "; 375 376 $code .= $this->varExport($tableData['columns'], true) . ", "; 377 378 $optionsWeNeed = array('type', 'indexes', 'primary', 'collate', 'charset'); 379 380 $options = array(); 381 foreach ($optionsWeNeed as $option) { 382 if (isset($tableData['options'][$option])) { 383 $options[$option] = $tableData['options'][$option]; 384 } 385 } 386 387 $code .= $this->varExport($options, true); 388 389 $code .= ");"; 390 391 return $code; 392 } 393 394 /** 395 * Build the code for dropping tables 396 * 397 * @param string $tableData 398 * @return string $code 399 */ 400 public function buildDropTable($tableData) 401 { 402 return " \$this->dropTable('" . $tableData['tableName'] . "');"; 403 } 404 405 /** 406 * Build the code for adding columns 407 * 408 * @param string $tableName 409 * @param string $columnName 410 * @param string $column 411 * @return string $code 412 */ 413 public function buildAddColumn($tableName, $columnName, $column) 414 { 415 $length = $column['length']; 416 $type = $column['type']; 417 unset($column['length'], $column['type']); 418 return " \$this->addColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");"; 419 } 420 421 /** 422 * Build the code for removing columns 423 * 424 * @param string $tableName 425 * @param string $columnName 426 * @param string $column 427 * @return string $code 428 */ 429 public function buildRemoveColumn($tableName, $columnName, $column) 430 { 431 return " \$this->removeColumn('" . $tableName . "', '" . $columnName. "');"; 432 } 433 434 /** 435 * Build the code for changing columns 436 * 437 * @param string $tableName 438 * @param string $columnName 439 * @param string $column 440 * @return string $code 441 */ 442 public function buildChangeColumn($tableName, $columnName, $column) 443 { 444 $length = $column['length']; 445 $type = $column['type']; 446 unset($column['length'], $column['type']); 447 return " \$this->changeColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");"; 448 } 449 450 /** 451 * Build the code for adding indexes 452 * 453 * @param string $tableName 454 * @param string $indexName 455 * @param string $index 456 * @return sgtring $code 457 */ 458 public function buildAddIndex($tableName, $indexName, $index) 459 { 460 return " \$this->addIndex('$tableName', '$indexName', " . $this->varExport($index) . ");"; 461 } 462 463 /** 464 * Build the code for removing indexes 465 * 466 * @param string $tableName 467 * @param string $indexName 468 * @param string $index 469 * @return string $code 470 */ 471 public function buildRemoveIndex($tableName, $indexName, $index) 472 { 473 return " \$this->removeIndex('$tableName', '$indexName', " . $this->varExport($index) . ");"; 474 } 475 476 /** 477 * Generate a migration class 478 * 479 * @param string $className Class name to generate 480 * @param array $options Options for the migration class 481 * @param string $up The code for the up function 482 * @param string $down The code for the down function 483 * @param boolean $return Whether or not to return the code. 484 * If true return and false it writes the class to disk. 485 * @return mixed 486 */ 487 public function generateMigrationClass($className, $options = array(), $up = null, $down = null, $return = false) 488 { 489 $className = Doctrine_Inflector::urlize($className); 490 $className = str_replace('-', '_', $className); 491 $className = Doctrine_Inflector::classify($className); 492 493 if ($return || ! $this->getMigrationsPath()) { 494 return $this->buildMigrationClass($className, null, $options, $up, $down); 495 } else { 496 if ( ! $this->getMigrationsPath()) { 497 throw new Doctrine_Migration_Exception('You must specify the path to your migrations.'); 498 } 499 500 $next = time() + $this->migration->getNextMigrationClassVersion(); 501 $fileName = $next . '_' . Doctrine_Inflector::tableize($className) . $this->suffix; 502 503 $class = $this->buildMigrationClass($className, $fileName, $options, $up, $down); 504 505 $path = $this->getMigrationsPath() . DIRECTORY_SEPARATOR . $fileName; 506 if (class_exists($className) || file_exists($path)) { 507 $this->migration->loadMigrationClass($className); 508 return false; 509 } 510 511 file_put_contents($path, $class); 512 require_once($path); 513 $this->migration->loadMigrationClass($className); 514 515 return true; 516 } 517 } 518 519 /** 520 * Build the code for a migration class 521 * 522 * @param string $className Class name to generate 523 * @param string $fileName File name to write the class to 524 * @param array $options Options for the migration class 525 * @param string $up The code for the up function 526 * @param string $down The code for the down function 527 * @return string $content The code for the generated class 528 */ 529 public function buildMigrationClass($className, $fileName = null, $options = array(), $up = null, $down = null) 530 { 531 $extends = isset($options['extends']) ? $options['extends']:'Doctrine_Migration_Base'; 532 533 $content = '<?php' . PHP_EOL; 534 535 $content .= sprintf(self::$tpl, $className, 536 $extends, 537 $up, 538 $down); 539 540 return $content; 541 } 542}