Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 *  $Id: Migration.php 1080 2007-02-10 18:17:08Z jwage $
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
24
 *
25
 * this class represents a database view
26
 *
27
 * @package     Doctrine
28
 * @subpackage  Migration
29
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
30
 * @link        www.doctrine-project.org
31
 * @since       1.0
32
 * @version     $Revision: 1080 $
33
 * @author      Jonathan H. Wage <jwage@mac.com>
34
 */
35
class Doctrine_Migration
36
{
37
    protected $_migrationTableName = 'migration_version',
38
              $_migrationTableCreated = false,
39
              $_connection,
40
              $_migrationClassesDirectory = array(),
41
              $_migrationClasses = array(),
42
              $_reflectionClass,
43
              $_errors = array(),
44
              $_process;
45
 
46
    protected static $_migrationClassesForDirectories = array();
47
 
48
    /**
49
     * Specify the path to the directory with the migration classes.
50
     * The classes will be loaded and the migration table will be created if it
51
     * does not already exist
52
     *
53
     * @param string $directory The path to your migrations directory
54
     * @param mixed $connection The connection name or instance to use for this migration
55
     * @return void
56
     */
57
    public function __construct($directory = null, $connection = null)
58
    {
59
        $this->_reflectionClass = new ReflectionClass('Doctrine_Migration_Base');
60
 
61
        if (is_null($connection)) {
62
            $this->_connection = Doctrine_Manager::connection();
63
        } else {
64
            if (is_string($connection)) {
65
                $this->_connection = Doctrine_Manager::getInstance()
66
                    ->getConnection($connection);
67
            } else {
68
                $this->_connection = $connection;
69
            }
70
        }
71
 
72
        $this->_process = new Doctrine_Migration_Process($this);
73
 
74
        if ($directory != null) {
75
            $this->_migrationClassesDirectory = $directory;
76
 
77
            $this->loadMigrationClassesFromDirectory();
78
        }
79
    }
80
 
81
    public function getConnection()
82
    {
83
        return $this->_connection;
84
    }
85
 
86
    public function setConnection(Doctrine_Connection $conn)
87
    {
88
        $this->_connection = $conn;
89
    }
90
 
91
    /**
92
     * Get the migration classes directory
93
     *
94
     * @return string $migrationClassesDirectory
95
     */
96
    public function getMigrationClassesDirectory()
97
    {
98
        return $this->_migrationClassesDirectory;
99
    }
100
 
101
    /**
102
     * Get the table name for storing the version number for this migration instance
103
     *
104
     * @return string $migrationTableName
105
     */
106
    public function getTableName()
107
    {
108
        return $this->_migrationTableName;
109
    }
110
 
111
    /**
112
     * Set the table name for storing the version number for this migration instance
113
     *
114
     * @param string $tableName
115
     * @return void
116
     */
117
    public function setTableName($tableName)
118
    {
119
        $this->_migrationTableName = $this->_connection
120
                ->formatter->getTableName($tableName);
121
    }
122
 
123
    /**
124
     * Load migration classes from the passed directory. Any file found with a .php
125
     * extension will be passed to the loadMigrationClass()
126
     *
127
     * @param string $directory  Directory to load migration classes from
128
     * @return void
129
     */
130
    public function loadMigrationClassesFromDirectory($directory = null)
131
    {
132
        $directory = $directory ? $directory:$this->_migrationClassesDirectory;
133
 
134
        $classesToLoad = array();
135
        $classes = get_declared_classes();
136
        foreach ((array) $directory as $dir) {
137
            $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),
138
                RecursiveIteratorIterator::LEAVES_ONLY);
139
 
140
            if (isset(self::$_migrationClassesForDirectories[$dir])) {
141
                foreach (self::$_migrationClassesForDirectories[$dir] as $num => $className) {
142
                    $this->_migrationClasses[$num] = $className;
143
                }
144
            }
145
 
146
            foreach ($it as $file) {
147
                $info = pathinfo($file->getFileName());
148
                if (isset($info['extension']) && $info['extension'] == 'php') {
149
                    require_once($file->getPathName());
150
 
151
                    $array = array_diff(get_declared_classes(), $classes);
152
                    $className = end($array);
153
 
154
                    if ($className) {
155
                        $e = explode('_', $file->getFileName());
156
                        $timestamp = $e[0];
157
 
158
                        $classesToLoad[$timestamp] = array('className' => $className, 'path' => $file->getPathName());
159
                    }
160
                }
161
            }
162
        }
163
        ksort($classesToLoad, SORT_NUMERIC);
164
        foreach ($classesToLoad as $class) {
165
            $this->loadMigrationClass($class['className'], $class['path']);
166
        }
167
    }
168
 
169
    /**
170
     * Load the specified migration class name in to this migration instances queue of
171
     * migration classes to execute. It must be a child of Doctrine_Migration in order
172
     * to be loaded.
173
     *
174
     * @param string $name
175
     * @return void
176
     */
177
    public function loadMigrationClass($name, $path = null)
178
    {
179
        $class = new ReflectionClass($name);
180
 
181
        while ($class->isSubclassOf($this->_reflectionClass)) {
182
 
183
            $class = $class->getParentClass();
184
            if ($class === false) {
185
                break;
186
            }
187
        }
188
 
189
        if ($class === false) {
190
            return false;
191
        }
192
 
193
        if (empty($this->_migrationClasses)) {
194
            $classMigrationNum = 1;
195
        } else {
196
            $nums = array_keys($this->_migrationClasses);
197
            $num = end($nums);
198
            $classMigrationNum = $num + 1;
199
        }
200
 
201
        $this->_migrationClasses[$classMigrationNum] = $name;
202
 
203
        if ($path) {
204
            $dir = dirname($path);
205
            self::$_migrationClassesForDirectories[$dir][$classMigrationNum] = $name;
206
        }
207
    }
208
 
209
    /**
210
     * Get all the loaded migration classes. Array where key is the number/version
211
     * and the value is the class name.
212
     *
213
     * @return array $migrationClasses
214
     */
215
    public function getMigrationClasses()
216
    {
217
        return $this->_migrationClasses;
218
    }
219
 
220
    /**
221
     * Set the current version of the database
222
     *
223
     * @param integer $number
224
     * @return void
225
     */
226
    public function setCurrentVersion($number)
227
    {
228
        if ($this->hasMigrated()) {
229
            $this->_connection->exec("UPDATE " . $this->_migrationTableName . " SET version = $number");
230
        } else {
231
            $this->_connection->exec("INSERT INTO " . $this->_migrationTableName . " (version) VALUES ($number)");
232
        }
233
    }
234
 
235
    /**
236
     * Get the current version of the database
237
     *
238
     * @return integer $version
239
     */
240
    public function getCurrentVersion()
241
    {
242
        $this->_createMigrationTable();
243
 
244
        $result = $this->_connection->fetchColumn("SELECT version FROM " . $this->_migrationTableName);
245
 
246
        return isset($result[0]) ? $result[0]:0;
247
    }
248
 
249
    /**
250
     * hReturns true/false for whether or not this database has been migrated in the past
251
     *
252
     * @return boolean $migrated
253
     */
254
    public function hasMigrated()
255
    {
256
        $this->_createMigrationTable();
257
 
258
        $result = $this->_connection->fetchColumn("SELECT version FROM " . $this->_migrationTableName);
259
 
260
        return isset($result[0]) ? true:false;
261
    }
262
 
263
    /**
264
     * Gets the latest possible version from the loaded migration classes
265
     *
266
     * @return integer $latestVersion
267
     */
268
    public function getLatestVersion()
269
    {
270
        $versions = array_keys($this->_migrationClasses);
271
        rsort($versions);
272
 
273
        return isset($versions[0]) ? $versions[0]:0;
274
    }
275
 
276
    /**
277
     * Get the next incremented version number based on the latest version number
278
     * using getLatestVersion()
279
     *
280
     * @return integer $nextVersion
281
     */
282
    public function getNextVersion()
283
    {
284
        return $this->getLatestVersion() + 1;
285
    }
286
 
287
    /**
288
     * Get the next incremented class version based on the loaded migration classes
289
     *
290
     * @return integer $nextMigrationClassVersion
291
     */
292
    public function getNextMigrationClassVersion()
293
    {
294
        if (empty($this->_migrationClasses)) {
295
            return 1;
296
        } else {
297
            $nums = array_keys($this->_migrationClasses);
298
            $num = end($nums) + 1;
299
            return $num;
300
        }
301
    }
302
 
303
    /**
304
     * Perform a migration process by specifying the migration number/version to
305
     * migrate to. It will automatically know whether you are migrating up or down
306
     * based on the current version of the database.
307
     *
308
     * @param  integer $to       Version to migrate to
309
     * @param  boolean $dryRun   Whether or not to run the migrate process as a dry run
310
     * @return integer $to       Version number migrated to
311
     * @throws Doctrine_Exception
312
     */
313
    public function migrate($to = null, $dryRun = false)
314
    {
315
        $this->clearErrors();
316
 
317
        $this->_createMigrationTable();
318
 
319
        $this->_connection->beginTransaction();
320
 
321
        try {
322
            // If nothing specified then lets assume we are migrating from
323
            // the current version to the latest version
324
            if ($to === null) {
325
                $to = $this->getLatestVersion();
326
            }
327
 
328
            $this->_doMigrate($to);
329
        } catch (Exception $e) {
330
            $this->addError($e);
331
        }
332
 
333
        if ($this->hasErrors()) {
334
            $this->_connection->rollback();
335
 
336
            if ($dryRun) {
337
                return false;
338
            } else {
339
                $this->_throwErrorsException();
340
            }
341
        } else {
342
            if ($dryRun) {
343
                $this->_connection->rollback();
344
                if ($this->hasErrors()) {
345
                    return false;
346
                } else {
347
                    return $to;
348
                }
349
            } else {
350
                $this->_connection->commit();
351
                $this->setCurrentVersion($to);
352
                return $to;
353
            }
354
        }
355
        return false;
356
    }
357
 
358
    /**
359
     * Run the migration process but rollback at the very end. Returns true or
360
     * false for whether or not the migration can be ran
361
     *
362
     * @param  string  $to
363
     * @return boolean $success
364
     */
365
    public function migrateDryRun($to = null)
366
    {
367
        return $this->migrate($to, true);
368
    }
369
 
370
    /**
371
     * Get the number of errors
372
     *
373
     * @return integer $numErrors
374
     */
375
    public function getNumErrors()
376
    {
377
        return count($this->_errors);
378
    }
379
 
380
    /**
381
     * Get all the error exceptions
382
     *
383
     * @return array $errors
384
     */
385
    public function getErrors()
386
    {
387
        return $this->_errors;
388
    }
389
 
390
    /**
391
     * Clears the error exceptions
392
     *
393
     * @return void
394
     */
395
    public function clearErrors()
396
    {
397
        $this->_errors = array();
398
    }
399
 
400
    /**
401
     * Add an error to the stack. Excepts some type of Exception
402
     *
403
     * @param Exception $e
404
     * @return void
405
     */
406
    public function addError(Exception $e)
407
    {
408
        $this->_errors[] = $e;
409
    }
410
 
411
    /**
412
     * Whether or not the migration instance has errors
413
     *
414
     * @return boolean
415
     */
416
    public function hasErrors()
417
    {
418
        return $this->getNumErrors() > 0 ? true:false;
419
    }
420
 
421
    /**
422
     * Get instance of migration class for number/version specified
423
     *
424
     * @param integer $num
425
     * @throws Doctrine_Migration_Exception $e
426
     */
427
    public function getMigrationClass($num)
428
    {
429
        if (isset($this->_migrationClasses[$num])) {
430
            $className = $this->_migrationClasses[$num];
431
            return new $className();
432
        }
433
 
434
        throw new Doctrine_Migration_Exception('Could not find migration class for migration step: '.$num);
435
    }
436
 
437
    /**
438
     * Throw an exception with all the errors trigged during the migration
439
     *
440
     * @return void
441
     * @throws Doctrine_Migration_Exception $e
442
     */
443
    protected function _throwErrorsException()
444
    {
445
        $messages = array();
446
        $num = 0;
447
        foreach ($this->getErrors() as $error) {
448
            $num++;
449
            $messages[] = ' Error #' . $num . ' - ' .$error->getMessage() . "\n" . $error->getTraceAsString() . "\n";
450
        }
451
 
452
        $title = $this->getNumErrors() . ' error(s) encountered during migration';
453
        $message  = $title . "\n";
454
        $message .= str_repeat('=', strlen($title)) . "\n";
455
        $message .= implode("\n", $messages);
456
 
457
        throw new Doctrine_Migration_Exception($message);
458
    }
459
 
460
    /**
461
     * Do the actual migration process
462
     *
463
     * @param  integer $to
464
     * @return integer $to
465
     * @throws Doctrine_Exception
466
     */
467
    protected function _doMigrate($to)
468
    {
469
        $from = $this->getCurrentVersion();
470
 
471
        if ($from == $to) {
472
            throw new Doctrine_Migration_Exception('Already at version # ' . $to);
473
        }
474
 
475
        $direction = $from > $to ? 'down':'up';
476
 
477
        if ($direction === 'up') {
478
            for ($i = $from + 1; $i <= $to; $i++) {
479
                $this->_doMigrateStep($direction, $i);
480
            }
481
        } else {
482
            for ($i = $from; $i > $to; $i--) {
483
                $this->_doMigrateStep($direction, $i);
484
            }
485
        }
486
 
487
        return $to;
488
    }
489
 
490
    /**
491
     * Perform a single migration step. Executes a single migration class and
492
     * processes the changes
493
     *
494
     * @param string $direction Direction to go, 'up' or 'down'
495
     * @param integer $num
496
     * @return void
497
     */
498
    protected function _doMigrateStep($direction, $num)
499
    {
500
        try {
501
            $migration = $this->getMigrationClass($num);
502
 
503
            $method = 'pre' . $direction;
504
            $migration->$method();
505
 
506
            if (method_exists($migration, $direction)) {
507
                $migration->$direction();
508
            } else if (method_exists($migration, 'migrate')) {
509
                $migration->migrate($direction);
510
            }
511
 
512
            if ($migration->getNumChanges() > 0) {
513
                $changes = $migration->getChanges();
514
                if ($direction == 'down' && method_exists($migration, 'migrate')) {
515
                    $changes = array_reverse($changes);
516
                }
517
                foreach ($changes as $value) {
518
                    list($type, $change) = $value;
519
                    $funcName = 'process' . Doctrine_Inflector::classify($type);
520
                    if (method_exists($this->_process, $funcName)) {
521
                        try {
522
                            $this->_process->$funcName($change);
523
                        } catch (Exception $e) {
524
                            $this->addError($e);
525
                        }
526
                    } else {
527
                        throw new Doctrine_Migration_Exception(sprintf('Invalid migration change type: %s', $type));
528
                    }
529
                }
530
            }
531
 
532
            $method = 'post' . $direction;
533
            $migration->$method();
534
        } catch (Exception $e) {
535
            $this->addError($e);
536
        }
537
    }
538
 
539
    /**
540
     * Create the migration table and return true. If it already exists it will
541
     * silence the exception and return false
542
     *
543
     * @return boolean $created Whether or not the table was created. Exceptions
544
     *                          are silenced when table already exists
545
     */
546
    protected function _createMigrationTable()
547
    {
548
        if ($this->_migrationTableCreated) {
549
            return true;
550
        }
551
 
552
        $this->_migrationTableCreated = true;
553
 
554
        try {
555
            $this->_connection->export->createTable($this->_migrationTableName, array('version' => array('type' => 'integer', 'size' => 11)));
556
 
557
            return true;
558
        } catch(Exception $e) {
559
            return false;
560
        }
561
    }
562
}