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 */ 34class 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( 115 Doctrine_Manager::getInstance()->getAttribute(Doctrine_Core::ATTR_MODEL_CLASS_PREFIX) . self::$_fromPrefix, 116 $this->_from 117 ); 118 $to = $this->_generateModels( 119 Doctrine_Manager::getInstance()->getAttribute(Doctrine_Core::ATTR_MODEL_CLASS_PREFIX) . self::$_toPrefix, 120 $this->_to 121 ); 122 123 return $this->_diff($from, $to); 124 } 125 126 /** 127 * Generate a migration class for the changes in this diff instance 128 * 129 * @return array $changes 130 */ 131 public function generateMigrationClasses() 132 { 133 $builder = new Doctrine_Migration_Builder($this->_migration); 134 135 return $builder->generateMigrationsFromDiff($this); 136 } 137 138 /** 139 * Initialize some Doctrine models at a given path. 140 * 141 * @param string $path 142 * @return array $models 143 */ 144 protected function _initializeModels($path) 145 { 146 $manager = Doctrine_Manager::getInstance(); 147 $modelLoading = $manager->getAttribute(Doctrine_Core::ATTR_MODEL_LOADING); 148 if ($modelLoading === Doctrine_Core::MODEL_LOADING_PEAR) { 149 $orig = Doctrine_Core::getModelsDirectory(); 150 Doctrine_Core::setModelsDirectory($path); 151 $models = Doctrine_Core::initializeModels(Doctrine_Core::loadModels($path)); 152 Doctrine_Core::setModelsDirectory($orig); 153 } else { 154 $models = Doctrine_Core::initializeModels(Doctrine_Core::loadModels($path)); 155 } 156 return $models; 157 } 158 159 /** 160 * Generate a diff between the from and to schema information 161 * 162 * @param string $from Path to set of models to migrate from 163 * @param string $to Path to set of models to migrate to 164 * @return array $changes 165 */ 166 protected function _diff($from, $to) 167 { 168 // Load the from and to models 169 $fromModels = $this->_initializeModels($from); 170 $toModels = $this->_initializeModels($to); 171 172 // Build schema information for the models 173 $fromInfo = $this->_buildModelInformation($fromModels); 174 $toInfo = $this->_buildModelInformation($toModels); 175 176 // Build array of changes between the from and to information 177 $changes = $this->_buildChanges($fromInfo, $toInfo); 178 179 $this->_cleanup(); 180 181 return $changes; 182 } 183 184 /** 185 * Build array of changes between the from and to array of schema information 186 * 187 * @param array $from Array of schema information to generate changes from 188 * @param array $to Array of schema information to generate changes for 189 * @return array $changes 190 */ 191 protected function _buildChanges($from, $to) 192 { 193 // Loop over the to schema information and compare it to the from 194 foreach ($to as $className => $info) { 195 // If the from doesn't have this class then it is a new table 196 if ( ! isset($from[$className])) { 197 $names = array('type', 'charset', 'collate', 'indexes', 'foreignKeys', 'primary'); 198 $options = array(); 199 foreach ($names as $name) { 200 if (isset($info['options'][$name]) && $info['options'][$name]) { 201 $options[$name] = $info['options'][$name]; 202 } 203 } 204 205 $table = array('tableName' => $info['tableName'], 206 'columns' => $info['columns'], 207 'options' => $options); 208 $this->_changes['created_tables'][$info['tableName']] = $table; 209 } 210 // Check for new and changed columns 211 foreach ($info['columns'] as $name => $column) { 212 // If column doesn't exist in the from schema information then it is a new column 213 if (isset($from[$className]) && ! isset($from[$className]['columns'][$name])) { 214 $this->_changes['created_columns'][$info['tableName']][$name] = $column; 215 } 216 // If column exists in the from schema information but is not the same then it is a changed column 217 if (isset($from[$className]['columns'][$name]) && $from[$className]['columns'][$name] != $column) { 218 $this->_changes['changed_columns'][$info['tableName']][$name] = $column; 219 } 220 } 221 // Check for new foreign keys 222 foreach ($info['options']['foreignKeys'] as $name => $foreignKey) { 223 $foreignKey['name'] = $name; 224 // If foreign key doesn't exist in the from schema information then we need to add a index and the new fk 225 if ( ! isset($from[$className]['options']['foreignKeys'][$name])) { 226 $this->_changes['created_foreign_keys'][$info['tableName']][$name] = $foreignKey; 227 $indexName = Doctrine_Manager::connection()->generateUniqueIndexName($info['tableName'], $foreignKey['local']); 228 $this->_changes['created_indexes'][$info['tableName']][$indexName] = array('fields' => array($foreignKey['local'])); 229 // If foreign key does exist then lets see if anything has changed with it 230 } else if (isset($from[$className]['options']['foreignKeys'][$name])) { 231 $oldForeignKey = $from[$className]['options']['foreignKeys'][$name]; 232 $oldForeignKey['name'] = $name; 233 // If the foreign key has changed any then we need to drop the foreign key and readd it 234 if ($foreignKey !== $oldForeignKey) { 235 $this->_changes['dropped_foreign_keys'][$info['tableName']][$name] = $oldForeignKey; 236 $this->_changes['created_foreign_keys'][$info['tableName']][$name] = $foreignKey; 237 } 238 } 239 } 240 // Check for new indexes 241 foreach ($info['options']['indexes'] as $name => $index) { 242 // If index doesn't exist in the from schema information 243 if ( ! isset($from[$className]['options']['indexes'][$name])) { 244 $this->_changes['created_indexes'][$info['tableName']][$name] = $index; 245 } 246 } 247 } 248 // Loop over the from schema information and compare it to the to schema information 249 foreach ($from as $className => $info) { 250 // If the class exists in the from but not in the to then it is a dropped table 251 if ( ! isset($to[$className])) { 252 $table = array('tableName' => $info['tableName'], 253 'columns' => $info['columns'], 254 'options' => array('type' => $info['options']['type'], 255 'charset' => $info['options']['charset'], 256 'collate' => $info['options']['collate'], 257 'indexes' => $info['options']['indexes'], 258 'foreignKeys' => $info['options']['foreignKeys'], 259 'primary' => $info['options']['primary'])); 260 $this->_changes['dropped_tables'][$info['tableName']] = $table; 261 } 262 // Check for removed columns 263 foreach ($info['columns'] as $name => $column) { 264 // If column exists in the from but not in the to then we need to remove it 265 if (isset($to[$className]) && ! isset($to[$className]['columns'][$name])) { 266 $this->_changes['dropped_columns'][$info['tableName']][$name] = $column; 267 } 268 } 269 // Check for dropped foreign keys 270 foreach ($info['options']['foreignKeys'] as $name => $foreignKey) { 271 // If the foreign key exists in the from but not in the to then we need to drop it 272 if ( ! isset($to[$className]['options']['foreignKeys'][$name])) { 273 $this->_changes['dropped_foreign_keys'][$info['tableName']][$name] = $foreignKey; 274 } 275 } 276 // Check for removed indexes 277 foreach ($info['options']['indexes'] as $name => $index) { 278 // If the index exists in the from but not the to then we need to remove it 279 if ( ! isset($to[$className]['options']['indexes'][$name])) { 280 $this->_changes['dropped_indexes'][$info['tableName']][$name] = $index; 281 } 282 } 283 } 284 285 return $this->_changes; 286 } 287 288 /** 289 * Build all the model schema information for the passed array of models 290 * 291 * @param array $models Array of models to build the schema information for 292 * @return array $info Array of schema information for all the passed models 293 */ 294 protected function _buildModelInformation(array $models) 295 { 296 $info = array(); 297 foreach ($models as $key => $model) { 298 $table = Doctrine_Core::getTable($model); 299 if ($table->getTableName() !== $this->_migration->getTableName()) { 300 $info[$model] = $table->getExportableFormat(); 301 } 302 } 303 304 $info = $this->_cleanModelInformation($info); 305 306 return $info; 307 } 308 309 /** 310 * Clean the produced model information of any potential prefix text 311 * 312 * @param mixed $info Either array or string to clean of prefixes 313 * @return mixed $info Cleaned value which is either an array or string 314 */ 315 protected function _cleanModelInformation($info) 316 { 317 if (is_array($info)) { 318 foreach ($info as $key => $value) { 319 unset($info[$key]); 320 $key = $this->_cleanModelInformation($key); 321 $info[$key] = $this->_cleanModelInformation($value); 322 } 323 return $info; 324 } else { 325 $find = array( 326 self::$_toPrefix, 327 self::$_fromPrefix, 328 Doctrine_Inflector::tableize(self::$_toPrefix) . '_', 329 Doctrine_Inflector::tableize(self::$_fromPrefix) . '_', 330 Doctrine_Inflector::tableize(self::$_toPrefix), 331 Doctrine_Inflector::tableize(self::$_fromPrefix) 332 ); 333 return str_replace($find, null, $info); 334 } 335 } 336 337 /** 338 * Get the extension of the type of file contained in a directory. 339 * Used to determine if a directory contains YAML or PHP files. 340 * 341 * @param string $item 342 * @return string $extension 343 */ 344 protected function _getItemExtension($item) 345 { 346 if (is_dir($item)) { 347 $files = glob($item . DIRECTORY_SEPARATOR . '*'); 348 } else { 349 $files = array($item); 350 } 351 352 $extension = null; 353 if (isset($files[0])) { 354 if (is_dir($files[0])) { 355 $extension = $this->_getItemExtension($files[0]); 356 } else { 357 $pathInfo = pathinfo($files[0]); 358 $extension = $pathInfo['extension']; 359 } 360 } 361 return $extension; 362 } 363 364 /** 365 * Generate a set of models for the schema information source 366 * 367 * @param string $prefix Prefix to generate the models with 368 * @param mixed $item The item to generate the models from 369 * @return string $path The path where the models were generated 370 * @throws Doctrine_Migration_Exception $e 371 */ 372 protected function _generateModels($prefix, $item) 373 { 374 $path = $this->_tmpPath . DIRECTORY_SEPARATOR . strtolower($prefix) . '_doctrine_tmp_dirs'; 375 $options = array( 376 'classPrefix' => $prefix, 377 'generateBaseClasses' => false 378 ); 379 380 if (is_string($item) && file_exists($item)) { 381 $extension = $this->_getItemExtension($item); 382 383 if ($extension === 'yml') { 384 Doctrine_Core::generateModelsFromYaml($item, $path, $options); 385 386 return $path; 387 } else if ($extension === 'php') { 388 Doctrine_Lib::copyDirectory($item, $path); 389 390 return $path; 391 } else { 392 throw new Doctrine_Migration_Exception('No php or yml files found at path: "' . $item . '"'); 393 } 394 } else { 395 try { 396 Doctrine_Core::generateModelsFromDb($path, (array) $item, $options); 397 return $path; 398 } catch (Exception $e) { 399 throw new Doctrine_Migration_Exception('Could not generate models from connection: ' . $e->getMessage()); 400 } 401 } 402 } 403 404 /** 405 * Cleanup temporary generated models after a diff is performed 406 * 407 * @return void 408 */ 409 protected function _cleanup() 410 { 411 $modelFiles = Doctrine_Core::getLoadedModelFiles(); 412 $filesToClean = array_diff($modelFiles, $this->_startingModelFiles); 413 414 foreach ($filesToClean as $file) { 415 if (file_exists($file)) { 416 unlink($file); 417 } 418 } 419 420 // clean up tmp directories 421 Doctrine_Lib::removeDirectories($this->_tmpPath . DIRECTORY_SEPARATOR . strtolower(self::$_fromPrefix) . '_doctrine_tmp_dirs'); 422 Doctrine_Lib::removeDirectories($this->_tmpPath . DIRECTORY_SEPARATOR . strtolower(self::$_toPrefix) . '_doctrine_tmp_dirs'); 423 } 424} 425