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}