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: Diff.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_Diff - class used for generating differences and migration
24
 * classes from 'from' and 'to' schema information.
25
 *
26
 * @package     Doctrine
27
 * @subpackage  Migration
28
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
29
 * @link        www.doctrine-project.org
30
 * @since       1.0
31
 * @version     $Revision: 1080 $
32
 * @author      Jonathan H. Wage <jonwage@gmail.com>
33
 */
34
class Doctrine_Migration_Diff
35
{
36
    protected $_from,
37
              $_to,
38
              $_changes = array('created_tables'      =>  array(),
39
                                'dropped_tables'      =>  array(),
40
                                'created_foreign_keys'=>  array(),
41
                                'dropped_foreign_keys'=>  array(),
42
                                'created_columns'     =>  array(),
43
                                'dropped_columns'     =>  array(),
44
                                'changed_columns'     =>  array(),
45
                                'created_indexes'     =>  array(),
46
                                'dropped_indexes'     =>  array()),
47
              $_migration,
48
              $_startingModelFiles = array(),
49
              $_tmpPath;
50
 
51
    protected static $_toPrefix   = 'ToPrfx',
52
                     $_fromPrefix = 'FromPrfx';
53
 
54
    /**
55
     * Instantiate new Doctrine_Migration_Diff instance
56
     *
57
     * <code>
58
     * $diff = new Doctrine_Migration_Diff('/path/to/old_models', '/path/to/new_models', '/path/to/migrations');
59
     * $diff->generateMigrationClasses();
60
     * </code>
61
     *
62
     * @param string $from      The from schema information source
63
     * @param string $to        The to schema information source
64
     * @param mixed  $migration Instance of Doctrine_Migration or path to migration classes
65
     * @return void
66
     */
67
    public function __construct($from, $to, $migration)
68
    {
69
        $this->_from = $from;
70
        $this->_to = $to;
71
        $this->_startingModelFiles = Doctrine_Core::getLoadedModelFiles();
72
        $this->setTmpPath(sys_get_temp_dir() . DIRECTORY_SEPARATOR . getmypid());
73
 
74
        if ($migration instanceof Doctrine_Migration) {
75
            $this->_migration = $migration;
76
        } else if (is_dir($migration)) {
77
            $this->_migration = new Doctrine_Migration($migration);
78
        }
79
    }
80
 
81
    /**
82
     * Set the temporary path to store the generated models for generating diffs
83
     *
84
     * @param string $tmpPath
85
     * @return void
86
     */
87
    public function setTmpPath($tmpPath)
88
    {
89
        if ( ! is_dir($tmpPath)) {
90
            mkdir($tmpPath, 0777, true);
91
        }
92
        $this->_tmpPath = $tmpPath;
93
    }
94
 
95
    /**
96
     * Get unique hash id for this migration instance
97
     *
98
     * @return string $uniqueId
99
     */
100
    protected function getUniqueId()
101
    {
102
        return md5($this->_from . $this->_to);
103
    }
104
 
105
    /**
106
     * Generate an array of changes found between the from and to schema information.
107
     *
108
     * @return array $changes
109
     */
110
    public function generateChanges()
111
    {
112
        $this->_cleanup();
113
 
114
        $from = $this->_generateModels(self::$_fromPrefix, $this->_from);
115
        $to = $this->_generateModels(
116
            Doctrine_Manager::getInstance()->getAttribute(Doctrine_Core::ATTR_MODEL_CLASS_PREFIX) . self::$_toPrefix,
117
            $this->_to
118
        );
119
 
120
        return $this->_diff($from, $to);
121
    }
122
 
123
    /**
124
     * Generate a migration class for the changes in this diff instance
125
     *
126
     * @return array $changes
127
     */
128
    public function generateMigrationClasses()
129
    {
130
        $builder = new Doctrine_Migration_Builder($this->_migration);
131
 
132
        return $builder->generateMigrationsFromDiff($this);
133
    }
134
 
135
    /**
136
     * Initialize some Doctrine models at a given path.
137
     *
138
     * @param string $path
139
     * @return array $models
140
     */
141
    protected function _initializeModels($path)
142
    {
143
        $manager = Doctrine_Manager::getInstance();
144
        $modelLoading = $manager->getAttribute(Doctrine_Core::ATTR_MODEL_LOADING);
145
        if ($modelLoading === Doctrine_Core::MODEL_LOADING_PEAR) {
146
            $orig = Doctrine_Core::getModelsDirectory();
147
            Doctrine_Core::setModelsDirectory($path);
148
            $models = Doctrine_Core::initializeModels(Doctrine_Core::loadModels($path));
149
            Doctrine_Core::setModelsDirectory($orig);
150
        } else {
151
            $models = Doctrine_Core::initializeModels(Doctrine_Core::loadModels($path));
152
        }
153
        return $models;
154
    }
155
 
156
    /**
157
     * Generate a diff between the from and to schema information
158
     *
159
     * @param  string $from     Path to set of models to migrate from
160
     * @param  string $to       Path to set of models to migrate to
161
     * @return array  $changes
162
     */
163
    protected function _diff($from, $to)
164
    {
165
        // Load the from and to models
166
        $fromModels = $this->_initializeModels($from);
167
        $toModels = $this->_initializeModels($to);
168
 
169
        // Build schema information for the models
170
        $fromInfo = $this->_buildModelInformation($fromModels);
171
        $toInfo = $this->_buildModelInformation($toModels);
172
 
173
        // Build array of changes between the from and to information
174
        $changes = $this->_buildChanges($fromInfo, $toInfo);
175
 
176
        $this->_cleanup();
177
 
178
        return $changes;
179
    }
180
 
181
    /**
182
     * Build array of changes between the from and to array of schema information
183
     *
184
     * @param array $from  Array of schema information to generate changes from
185
     * @param array $to    Array of schema information to generate changes for
186
     * @return array $changes
187
     */
188
    protected function _buildChanges($from, $to)
189
    {
190
        // Loop over the to schema information and compare it to the from
191
        foreach ($to as $className => $info) {
192
            // If the from doesn't have this class then it is a new table
193
            if ( ! isset($from[$className])) {
194
                $names = array('type', 'charset', 'collate', 'indexes', 'foreignKeys', 'primary');
195
                $options = array();
196
                foreach ($names as $name) {
197
                    if (isset($info['options'][$name]) && $info['options'][$name]) {
198
                        $options[$name] = $info['options'][$name];
199
                    }
200
                }
201
 
202
                $table = array('tableName' => $info['tableName'],
203
                               'columns'   => $info['columns'],
204
                               'options'   => $options);
205
                $this->_changes['created_tables'][$info['tableName']] = $table;
206
            }
207
            // Check for new and changed columns
208
            foreach ($info['columns'] as $name => $column) {
209
                // If column doesn't exist in the from schema information then it is a new column
210
                if (isset($from[$className]) && ! isset($from[$className]['columns'][$name])) {
211
                    $this->_changes['created_columns'][$info['tableName']][$name] = $column;
212
                }
213
                // If column exists in the from schema information but is not the same then it is a changed column
214
                if (isset($from[$className]['columns'][$name]) && $from[$className]['columns'][$name] != $column) {
215
                    $this->_changes['changed_columns'][$info['tableName']][$name] = $column;
216
                }
217
            }
218
            // Check for new foreign keys
219
            foreach ($info['options']['foreignKeys'] as $name => $foreignKey) {
220
                $foreignKey['name'] = $name;
221
                // If foreign key doesn't exist in the from schema information then we need to add a index and the new fk
222
                if ( ! isset($from[$className]['options']['foreignKeys'][$name])) {
223
                    $this->_changes['created_foreign_keys'][$info['tableName']][$name] = $foreignKey;
224
                    $indexName = Doctrine_Manager::connection()->generateUniqueIndexName($info['tableName'], $foreignKey['local']);
225
                    $this->_changes['created_indexes'][$info['tableName']][$indexName] = array('fields' => array($foreignKey['local']));
226
                // If foreign key does exist then lets see if anything has changed with it
227
                } else if (isset($from[$className]['options']['foreignKeys'][$name])) {
228
                    $oldForeignKey = $from[$className]['options']['foreignKeys'][$name];
229
                    $oldForeignKey['name'] = $name;
230
                    // If the foreign key has changed any then we need to drop the foreign key and readd it
231
                    if ($foreignKey !== $oldForeignKey) {
232
                        $this->_changes['dropped_foreign_keys'][$info['tableName']][$name] = $oldForeignKey;
233
                        $this->_changes['created_foreign_keys'][$info['tableName']][$name] = $foreignKey;
234
                    }
235
                }
236
            }
237
            // Check for new indexes
238
            foreach ($info['options']['indexes'] as $name => $index) {
239
                // If index doesn't exist in the from schema information
240
                if ( ! isset($from[$className]['options']['indexes'][$name])) {
241
                    $this->_changes['created_indexes'][$info['tableName']][$name] = $index;
242
                }
243
            }
244
        }
245
        // Loop over the from schema information and compare it to the to schema information
246
        foreach ($from as $className => $info) {
247
            // If the class exists in the from but not in the to then it is a dropped table
248
            if ( ! isset($to[$className])) {
249
                $table = array('tableName' => $info['tableName'],
250
                               'columns'   => $info['columns'],
251
                               'options'   => array('type'        => $info['options']['type'],
252
                                                    'charset'     => $info['options']['charset'],
253
                                                    'collate'     => $info['options']['collate'],
254
                                                    'indexes'     => $info['options']['indexes'],
255
                                                    'foreignKeys' => $info['options']['foreignKeys'],
256
                                                    'primary'     => $info['options']['primary']));
257
                $this->_changes['dropped_tables'][$info['tableName']] = $table;
258
            }
259
            // Check for removed columns
260
            foreach ($info['columns'] as $name => $column) {
261
                // If column exists in the from but not in the to then we need to remove it
262
                if (isset($to[$className]) && ! isset($to[$className]['columns'][$name])) {
263
                    $this->_changes['dropped_columns'][$info['tableName']][$name] = $column;
264
                }
265
            }
266
            // Check for dropped foreign keys
267
            foreach ($info['options']['foreignKeys'] as $name => $foreignKey) {
268
                // If the foreign key exists in the from but not in the to then we need to drop it
269
                if ( ! isset($to[$className]['options']['foreignKeys'][$name])) {
270
                    $this->_changes['dropped_foreign_keys'][$info['tableName']][$name] = $foreignKey;
271
                }
272
            }
273
            // Check for removed indexes
274
            foreach ($info['options']['indexes'] as $name => $index) {
275
                // If the index exists in the from but not the to then we need to remove it
276
                if ( ! isset($to[$className]['options']['indexes'][$name])) {
277
                    $this->_changes['dropped_indexes'][$info['tableName']][$name] = $index;
278
                }
279
            }
280
        }
281
 
282
        return $this->_changes;
283
    }
284
 
285
    /**
286
     * Build all the model schema information for the passed array of models
287
     *
288
     * @param  array $models Array of models to build the schema information for
289
     * @return array $info   Array of schema information for all the passed models
290
     */
291
    protected function _buildModelInformation(array $models)
292
    {
293
        $info = array();
294
        foreach ($models as $key => $model) {
295
            $table = Doctrine_Core::getTable($model);
296
            if ($table->getTableName() !== $this->_migration->getTableName()) {
297
                $info[$model] = $table->getExportableFormat();
298
            }
299
        }
300
 
301
        $info = $this->_cleanModelInformation($info);
302
 
303
        return $info;
304
    }
305
 
306
    /**
307
     * Clean the produced model information of any potential prefix text
308
     *
309
     * @param  mixed $info  Either array or string to clean of prefixes
310
     * @return mixed $info  Cleaned value which is either an array or string
311
     */
312
    protected function _cleanModelInformation($info)
313
    {
314
        if (is_array($info)) {
315
            foreach ($info as $key => $value) {
316
                unset($info[$key]);
317
                $key = $this->_cleanModelInformation($key);
318
                $info[$key] = $this->_cleanModelInformation($value);
319
            }
320
            return $info;
321
        } else {
322
            $find = array(
323
                self::$_toPrefix,
324
                self::$_fromPrefix,
325
                Doctrine_Inflector::tableize(self::$_toPrefix) . '_',
326
                Doctrine_Inflector::tableize(self::$_fromPrefix) . '_',
327
                Doctrine_Inflector::tableize(self::$_toPrefix),
328
                Doctrine_Inflector::tableize(self::$_fromPrefix)
329
            );
330
            return str_replace($find, null, $info);
331
        }
332
    }
333
 
334
    /**
335
     * Get the extension of the type of file contained in a directory.
336
     * Used to determine if a directory contains YAML or PHP files.
337
     *
338
     * @param string $item
339
     * @return string $extension
340
     */
341
    protected function _getItemExtension($item)
342
    {
343
        if (is_dir($item)) {
344
            $files = glob($item . DIRECTORY_SEPARATOR . '*');
345
        } else {
346
            $files = array($item);
347
        }
348
 
349
        $extension = null;
350
        if (isset($files[0])) {
351
            if (is_dir($files[0])) {
352
                $extension = $this->_getItemExtension($files[0]);
353
            } else {
354
                $pathInfo = pathinfo($files[0]);
355
                $extension = $pathInfo['extension'];
356
            }
357
        }
358
        return $extension;
359
    }
360
 
361
    /**
362
     * Generate a set of models for the schema information source
363
     *
364
     * @param  string $prefix  Prefix to generate the models with
365
     * @param  mixed  $item    The item to generate the models from
366
     * @return string $path    The path where the models were generated
367
     * @throws Doctrine_Migration_Exception $e
368
     */
369
    protected function _generateModels($prefix, $item)
370
    {
371
        $path = $this->_tmpPath . DIRECTORY_SEPARATOR . strtolower($prefix) . '_doctrine_tmp_dirs';
372
        $options = array(
373
            'classPrefix' => $prefix,
374
            'generateBaseClasses' => false
375
        );
376
 
377
        if (is_string($item) && file_exists($item)) {
378
            $extension = $this->_getItemExtension($item);
379
 
380
            if ($extension === 'yml') {
381
                Doctrine_Core::generateModelsFromYaml($item, $path, $options);
382
 
383
                return $path;
384
            } else if ($extension === 'php') {
385
                Doctrine_Lib::copyDirectory($item, $path);
386
 
387
                return $path;
388
            } else {
389
                throw new Doctrine_Migration_Exception('No php or yml files found at path: "' . $item . '"');
390
            }
391
        } else {
392
            try {
393
                Doctrine_Core::generateModelsFromDb($path, (array) $item, $options);
394
                return $path;
395
            } catch (Exception $e) {
396
                throw new Doctrine_Migration_Exception('Could not generate models from connection: ' . $e->getMessage());
397
            }
398
        }
399
    }
400
 
401
    /**
402
     * Cleanup temporary generated models after a diff is performed
403
     *
404
     * @return void
405
     */
406
    protected function _cleanup()
407
    {
408
        $modelFiles = Doctrine_Core::getLoadedModelFiles();
409
        $filesToClean = array_diff($modelFiles, $this->_startingModelFiles);
410
 
411
        foreach ($filesToClean as $file) {
412
            if (file_exists($file)) {
413
                unlink($file);
414
            }
415
        }
416
 
417
        // clean up tmp directories
418
        Doctrine_Lib::removeDirectories($this->_tmpPath . DIRECTORY_SEPARATOR . strtolower(self::$_fromPrefix) . '_doctrine_tmp_dirs');
419
        Doctrine_Lib::removeDirectories($this->_tmpPath . DIRECTORY_SEPARATOR . strtolower(self::$_toPrefix) . '_doctrine_tmp_dirs');
420
    }
421
}