Migration.php 15.5 KB
Newer Older
Jonathan.Wage's avatar
Jonathan.Wage committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php
/*
 *  $Id: Migration.php 1080 2007-02-10 18:17:08Z jwage $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21

Jonathan.Wage's avatar
Jonathan.Wage committed
22 23 24 25 26 27
/**
 * Doctrine_Migration
 *
 * this class represents a database view
 *
 * @package     Doctrine
28
 * @subpackage  Migration
Jonathan.Wage's avatar
Jonathan.Wage committed
29 30 31 32
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision: 1080 $
33
 * @author      Jonathan H. Wage <jwage@mac.com>
Jonathan.Wage's avatar
Jonathan.Wage committed
34 35 36
 */
class Doctrine_Migration
{
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
    protected $_changes = array('created_tables'      =>  array(),
                                'renamed_tables'      =>  array(),
                                'created_constraints' =>  array(),
                                'dropped_fks'         =>  array(),
                                'created_fks'         =>  array(),
                                'dropped_constraints' =>  array(),
                                'removed_indexes'     =>  array(),
                                'dropped_tables'      =>  array(),
                                'added_columns'       =>  array(),
                                'renamed_columns'     =>  array(),
                                'changed_columns'     =>  array(),
                                'removed_columns'     =>  array(),
                                'added_indexes'       =>  array()),
              $_migrationTableName = 'migration_version',
              $_migrationClassesDirectory = array(),
              $_migrationClasses = array(),
              $_loadedMigrations = array();
54

55 56 57 58 59 60 61 62 63
    /**
     * construct
     *
     * Specify the path to the directory with the migration classes.
     * The classes will be loaded and the migration table will be created if it does not already exist
     *
     * @param string $directory 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
64 65 66
    public function __construct($directory = null)
    {
        if ($directory != null) {
67
            $this->_migrationClassesDirectory = $directory;
Jonathan.Wage's avatar
Jonathan.Wage committed
68 69
            
            $this->loadMigrationClasses();
70 71
            
            $this->createMigrationTable();
Jonathan.Wage's avatar
Jonathan.Wage committed
72 73
        }
    }
74

75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    /**
     * getTableName
     *
     * @return void
     */
    public function getTableName()
    {
        return $this->_migrationTableName;
    }

    /**
     * setTableName
     *
     * @param string $tableName 
     * @return void
     */
    public function setTableName($tableName)
    {
        $this->_migrationTableName = $tableName;
    }

96 97 98 99 100 101 102 103 104 105 106 107
    /**
     * createMigrationTable
     * 
     * Creates the migration table used to store the current version
     *
     * @return void
     */
    protected function createMigrationTable()
    {
        $conn = Doctrine_Manager::connection();
        
        try {
108
            $conn->export->createTable($this->_migrationTableName, array('version' => array('type' => 'integer', 'size' => 11)));
109 110 111 112 113 114
            
            return true;
        } catch(Exception $e) {
            return false;
        }
    }
115

116 117 118 119 120 121 122
    /**
     * loadMigrationClasses
     *
     * Loads the migration classes for the directory specified by the constructor
     *
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
123 124
    protected function loadMigrationClasses()
    {
125 126
        if ($this->_migrationClasses) {
            return $this->_migrationClasses;
127 128
        }
        
Jonathan.Wage's avatar
Jonathan.Wage committed
129
        $classes = get_declared_classes();
130
        
131 132
        if ($this->_migrationClassesDirectory !== null) {
            foreach ((array) $this->_migrationClassesDirectory as $dir) {
Jonathan.Wage's avatar
Jonathan.Wage committed
133 134 135 136 137 138
                $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),
                                                        RecursiveIteratorIterator::LEAVES_ONLY);

                foreach ($it as $file) {
                    $e = explode('.', $file->getFileName());
                    if (end($e) === 'php' && strpos($file->getFileName(), '.inc') === false) {
139
                        if ( ! in_array($file->getFileName(), $this->_loadedMigrations)) {
140 141 142 143 144 145
                            require_once($file->getPathName());
                            
                            $requiredClass = array_diff(get_declared_classes(), $classes);
                            $requiredClass = end($requiredClass);
                            
                            if ($requiredClass) {
146
                                $this->_loadedMigrations[$requiredClass] = $file->getFileName();
147 148
                            }
                        }
Jonathan.Wage's avatar
Jonathan.Wage committed
149 150 151 152 153 154 155
                    }
                }
            }
        }
        
        $parent = new ReflectionClass('Doctrine_Migration');
        
156
        foreach ($this->_loadedMigrations as $name => $fileName) {
Jonathan.Wage's avatar
Jonathan.Wage committed
157 158 159 160 161 162 163 164 165 166 167 168 169 170
            $class = new ReflectionClass($name);
            
            while ($class->isSubclassOf($parent)) {

                $class = $class->getParentClass();
                if ($class === false) {
                    break;
                }
            }
            
            if ($class === false) {
                continue;
            }
            
171 172 173
            $e = explode('_', $fileName);
            $classMigrationNum = (int) $e[0];
            
174
            $this->_migrationClasses[$classMigrationNum] = array('className' => $name, 'fileName' => $fileName);
Jonathan.Wage's avatar
Jonathan.Wage committed
175 176
        }
        
177
        return $this->_migrationClasses;
178
    }
179

180 181 182 183 184 185 186
    /**
     * getMigrationClasses
     *
     * @return void
     */
    public function getMigrationClasses()
    {
187
        return $this->_migrationClasses;
Jonathan.Wage's avatar
Jonathan.Wage committed
188
    }
189

190 191 192 193 194 195 196 197 198
    /**
     * setCurrentVersion
     *
     * Sets the current version in the migration table
     *
     * @param string $number 
     * @return void
     */
    protected function setCurrentVersion($number)
199 200 201
    {
        $conn = Doctrine_Manager::connection();
        
202
        if ($this->hasMigrated()) {
203
            $conn->exec("UPDATE " . $this->_migrationTableName . " SET version = $number");
204
        } else {
205
            $conn->exec("INSERT INTO " . $this->_migrationTableName . " (version) VALUES ($number)");
206 207
        }
    }
208

209 210 211 212 213 214 215
    /**
     * getCurrentVersion
     *
     * Get the current version of the database
     *
     * @return void
     */
216
    public function getCurrentVersion()
217 218 219
    {
        $conn = Doctrine_Manager::connection();
        
220
        $result = $conn->fetchColumn("SELECT version FROM " . $this->_migrationTableName);
221
        
222
        return isset($result[0]) ? $result[0]:0;
223
    }
224

225 226 227 228 229 230 231
    /**
     * hasMigrated
     *
     * Returns true/false for whether or not this database has been migrated in the past
     *
     * @return void
     */
232
    public function hasMigrated()
Jonathan.Wage's avatar
Jonathan.Wage committed
233
    {
234
        $conn = Doctrine_Manager::connection();
Jonathan.Wage's avatar
Jonathan.Wage committed
235
        
236
        $result = $conn->fetchColumn("SELECT version FROM " . $this->_migrationTableName);
Jonathan.Wage's avatar
Jonathan.Wage committed
237
        
238 239
        return isset($result[0]) ? true:false; 
    }
240

241 242 243 244 245 246 247
    /**
     * getLatestVersion
     *
     * Gets the latest possible version from the loaded migration classes
     *
     * @return void
     */
248
    public function getLatestVersion()
249 250 251 252
    {
        $this->loadMigrationClasses();
        
        $versions = array();
253
        foreach (array_keys($this->_migrationClasses) as $classMigrationNum) {
254
            $versions[$classMigrationNum] = $classMigrationNum;
Jonathan.Wage's avatar
Jonathan.Wage committed
255
        }
256
        
257 258
        rsort($versions);
        
259 260
        return isset($versions[0]) ? $versions[0]:0;
    }
261 262 263 264 265 266

    /**
     * getNextVersion
     *
     * @return integer $nextVersion
     */
267 268 269
    public function getNextVersion()
    {
        return $this->getLatestVersion() + 1;
Jonathan.Wage's avatar
Jonathan.Wage committed
270
    }
271

272 273 274 275 276 277 278 279
    /**
     * getMigrationClass
     *
     * Get instance of migration class for $num
     *
     * @param string $num 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
280
    protected function getMigrationClass($num)
Jonathan.Wage's avatar
Jonathan.Wage committed
281
    {
282
        foreach ($this->_migrationClasses as $classMigrationNum => $info) {
Jonathan.Wage's avatar
Jonathan.Wage committed
283 284 285
            $className = $info['className'];
            
            if ($classMigrationNum == $num) {
Jonathan.Wage's avatar
Jonathan.Wage committed
286 287
                return new $className();
            }
288 289
        }
        
Jonathan.Wage's avatar
Jonathan.Wage committed
290
        throw new Doctrine_Migration_Exception('Could not find migration class for migration step: '.$num);
291
    }
292

293 294 295 296 297 298 299 300 301 302
    /**
     * doMigrateStep
     *
     * Perform migration directory for the specified version. Loads migration classes and performs the migration then processes the changes
     *
     * @param string $direction 
     * @param string $num 
     * @return void
     */
    protected function doMigrateStep($direction, $num)
303
    {
Jonathan.Wage's avatar
Jonathan.Wage committed
304
        $migrate = $this->getMigrationClass($num);
305
        
Jonathan.Wage's avatar
Jonathan.Wage committed
306
        $migrate->doMigrate($direction);
Jonathan.Wage's avatar
Jonathan.Wage committed
307
    }
308

309 310 311 312 313 314 315 316 317
    /**
     * doMigrate
     * 
     * Perform migration for a migration class. Executes the up or down method then processes the changes
     *
     * @param string $direction 
     * @return void
     */
    protected function doMigrate($direction)
Jonathan.Wage's avatar
Jonathan.Wage committed
318 319 320 321
    {
        if (method_exists($this, $direction)) {
            $this->$direction();

322
            foreach ($this->_changes as $type => $changes) {
323 324 325
                $process = new Doctrine_Migration_Process();
                $funcName = 'process' . Doctrine::classify($type);

326
                if ( ! empty($changes)) {
327 328 329
                    $process->$funcName($changes); 
                }
            }
Jonathan.Wage's avatar
Jonathan.Wage committed
330 331
        }
    }
332

333 334 335 336 337 338 339 340 341 342
    /**
     * migrate
     *
     * Perform a migration chain by specifying the $from and $to.
     * If you do not specify a $from or $to then it will attempt to migrate from the current version to the latest version
     *
     * @param string $from 
     * @param string $to 
     * @return void
     */
343
    public function migrate($to = null)
Jonathan.Wage's avatar
Jonathan.Wage committed
344
    {
345 346
        $from = $this->getCurrentVersion();
        
347
        // If nothing specified then lets assume we are migrating from the current version to the latest version
348
        if ($to === null) {
349 350 351
            $to = $this->getLatestVersion();
        }
        
352
        if ($from == $to) {
353
            throw new Doctrine_Migration_Exception('Already at version # ' . $to);
Jonathan.Wage's avatar
Jonathan.Wage committed
354
        }
355 356 357 358 359 360 361 362 363 364 365 366 367 368
        
        $direction = $from > $to ? 'down':'up';
        
        if ($direction === 'up') {
            for ($i = $from + 1; $i <= $to; $i++) {
                $this->doMigrateStep($direction, $i);
            }
        } else {
            for ($i = $from; $i > $to; $i--) {
                $this->doMigrateStep($direction, $i);
            }
        }
        
        $this->setCurrentVersion($to);
369
        
370
        return $to;
Jonathan.Wage's avatar
Jonathan.Wage committed
371
    }
372

373 374 375 376 377 378 379 380
    /**
     * addChange
     *
     * @param string $type 
     * @param string $array 
     * @return void
     */
    protected function addChange($type, array $change = array())
Jonathan.Wage's avatar
Jonathan.Wage committed
381
    {
382
        $this->_changes[$type][] = $change;
Jonathan.Wage's avatar
Jonathan.Wage committed
383
    }
384

385 386 387 388 389 390 391 392
    /**
     * createTable
     *
     * @param string $tableName 
     * @param string $array 
     * @param string $array 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
393 394 395 396 397 398
    public function createTable($tableName, array $fields = array(), array $options = array())
    {
        $options = get_defined_vars();
        
        $this->addChange('created_tables', $options);
    }
399

400 401 402 403 404 405
    /**
     * dropTable
     *
     * @param string $tableName 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
406 407 408 409 410 411
    public function dropTable($tableName)
    {
        $options = get_defined_vars();
        
        $this->addChange('dropped_tables', $options);
    }
412

413 414 415 416 417 418 419
    /**
     * renameTable
     *
     * @param string $oldTableName 
     * @param string $newTableName 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
420 421 422 423 424 425
    public function renameTable($oldTableName, $newTableName)
    {
        $options = get_defined_vars();
        
        $this->addChange('renamed_tables', $options);
    }
426

427 428 429 430 431 432 433 434 435 436 437 438 439
    /**
     * createConstraint
     *
     * @param string $tableName
     * @param string $constraintName
     * @return void
     */
    public function createConstraint($tableName, $constraintName, array $definition)
    {
        $options = get_defined_vars();
        
        $this->addChange('created_constraints', $options);
    }
440

441
    /**
442
     * dropConstraint
443 444 445 446 447
     *
     * @param string $tableName
     * @param string $constraintName
     * @return void
     */
romanb's avatar
romanb committed
448
    public function dropConstraint($tableName, $constraintName, $primary = false)
449 450 451 452 453
    {
        $options = get_defined_vars();
        
        $this->addChange('dropped_constraints', $options);
    }
454

455 456 457 458 459 460 461 462 463 464 465 466 467
    /**
     * createForeignKey
     *
     * @param string $tableName
     * @param string $constraintName
     * @return void
     */
    public function createForeignKey($tableName, array $definition)
    {
        $options = get_defined_vars();
        
        $this->addChange('created_fks', $options);
    }
468

469 470 471 472 473 474 475 476 477 478 479 480 481
    /**
     * dropForeignKey
     *
     * @param string $tableName
     * @param string $constraintName
     * @return void
     */
    public function dropForeignKey($tableName, $fkName)
    {
        $options = get_defined_vars();
        
        $this->addChange('dropped_fks', $options);
    }
482

483 484 485 486 487 488 489 490 491
    /**
     * addColumn
     *
     * @param string $tableName 
     * @param string $columnName 
     * @param string $type 
     * @param string $array 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
492
    public function addColumn($tableName, $columnName, $type, array $options = array())
Jonathan.Wage's avatar
Jonathan.Wage committed
493 494 495 496 497
    {
        $options = get_defined_vars();
        
        $this->addChange('added_columns', $options);
    }
498

499 500 501 502 503 504 505 506
    /**
     * renameColumn
     *
     * @param string $tableName 
     * @param string $oldColumnName 
     * @param string $newColumnName 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
507 508 509 510 511 512
    public function renameColumn($tableName, $oldColumnName, $newColumnName)
    {
        $options = get_defined_vars();
        
        $this->addChange('renamed_columns', $options);
    }
513

514 515 516 517 518 519 520 521 522
    /**
     * renameColumn
     *
     * @param string $tableName 
     * @param string $columnName 
     * @param string $type 
     * @param string $array 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
523 524 525 526 527 528
    public function changeColumn($tableName, $columnName, $type, array $options = array())
    {
        $options = get_defined_vars();
        
        $this->addChange('changed_columns', $options);
    }
529

530 531 532 533 534 535 536
    /**
     * removeColumn
     *
     * @param string $tableName 
     * @param string $columnName 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
537 538 539 540 541 542
    public function removeColumn($tableName, $columnName)
    {
        $options = get_defined_vars();
        
        $this->addChange('removed_columns', $options);
    }
543

544 545 546 547 548 549 550 551
    /**
     * addIndex
     *
     * @param string $tableName 
     * @param string $indexName 
     * @param string $array 
     * @return void
     */
romanb's avatar
romanb committed
552
    public function addIndex($tableName, $indexName, array $definition)
Jonathan.Wage's avatar
Jonathan.Wage committed
553 554 555 556 557
    {
        $options = get_defined_vars();
        
        $this->addChange('added_indexes', $options);
    }
558

559 560 561 562 563 564 565
    /**
     * removeIndex
     *
     * @param string $tableName 
     * @param string $indexName 
     * @return void
     */
Jonathan.Wage's avatar
Jonathan.Wage committed
566 567 568 569 570 571 572
    public function removeIndex($tableName, $indexName)
    {
        $options = get_defined_vars();
        
        $this->addChange('removed_indexes', $options);
    }
}