PageRenderTime 34ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/sfMigrator.class.php

https://github.com/jcoby/sfPropelMigrationsPlugin
PHP | 582 lines | 344 code | 81 blank | 157 comment | 26 complexity | f9b3b3e1253f9f1c68fcd62d6e5f3c76 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the sfPropelMigrationsLightPlugin package.
  4. * (c) 2006-2008 Martin Kreidenweis <sf@kreidenweis.com>
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * Manage all calls to the sfMigration class instances.
  11. *
  12. * @package symfony
  13. * @subpackage plugin
  14. * @author Martin Kreidenweis <sf@kreidenweis.com>
  15. * @version SVN: $Id: sfMigrator.class.php 26873 2010-01-19 12:09:31Z Stefan.Koopmanschap $
  16. */
  17. class sfMigrator
  18. {
  19. /**
  20. * Migration filenames.
  21. *
  22. * @var array $migrations
  23. */
  24. protected $migrations = array();
  25. /**
  26. * Perform an update on the database.
  27. *
  28. * @param string $sql
  29. *
  30. * @return integer
  31. */
  32. static public function executeUpdate($sql)
  33. {
  34. $con = Propel::getConnection();
  35. return $con instanceof PropelPDO ? $con->exec($sql) : $con->executeUpdate($sql);
  36. }
  37. /**
  38. * Perform a query on the database.
  39. *
  40. * @param string $sql
  41. * @param string $fetchmode
  42. *
  43. * @return mixed
  44. */
  45. static public function executeQuery($sql, $fetchmode = null)
  46. {
  47. $con = Propel::getConnection();
  48. if ($con instanceof PropelPDO)
  49. {
  50. $stmt = $con->prepare($sql);
  51. $stmt->execute();
  52. return $stmt;
  53. }
  54. else
  55. {
  56. return $con->executeQuery($sql, $fetchmode);
  57. }
  58. }
  59. /**
  60. * Constructor.
  61. */
  62. public function __construct()
  63. {
  64. $this->loadMigrations();
  65. }
  66. /**
  67. * Execute migrations.
  68. *
  69. * @param integer $destVersion Version number to migrate to, defaults to
  70. * the max existing
  71. *
  72. * @return integer Number of executed migrations
  73. */
  74. public function migrate($destVersion = null)
  75. {
  76. $maxVersion = $this->getMaxVersion();
  77. if ($destVersion === null)
  78. {
  79. $destVersion = $maxVersion;
  80. }
  81. else
  82. {
  83. $destVersion = $destVersion;
  84. if (($destVersion > $maxVersion) || ($destVersion < 0))
  85. {
  86. throw new sfException(sprintf('Migration %d does not exist.', $destVersion));
  87. }
  88. }
  89. $sourceVersion = $this->getCurrentVersion();
  90. // do appropriate stuff according to migration direction
  91. if (strnatcmp($destVersion, $sourceVersion) == -1)
  92. {
  93. $res = $this->migrateDown($sourceVersion, $destVersion);
  94. }
  95. else
  96. {
  97. $res = $this->migrateUp($sourceVersion, $destVersion);
  98. }
  99. return $res;
  100. }
  101. public function rollback($steps=1)
  102. {
  103. $current_version = $this->getCurrentVersion();
  104. $result = $this->executeQuery("SELECT version FROM schema_migration WHERE version != '$current_version' ORDER BY lpad(version, 14, '0') desc limit 1");
  105. if($result instanceof PDOStatement)
  106. {
  107. $prev_version = $result->fetchColumn(0);
  108. }
  109. else
  110. {
  111. if($result->next())
  112. {
  113. $prev_version = $result->getString('version');
  114. }
  115. else
  116. {
  117. throw new sfDatabaseException('Unable to retrieve current schema version.');
  118. }
  119. }
  120. return $this->migrate($prev_version);
  121. }
  122. /**
  123. * Generate a new migration stub
  124. *
  125. * @param string $name Name of the new migration
  126. *
  127. * @return string Filename of the new migration file
  128. */
  129. public function generateMigration($name)
  130. {
  131. // calculate version number for new migration
  132. $maxVersion = $this->getMaxVersion();
  133. $newVersion = date("YmdHis");
  134. // sanitize name
  135. $name = preg_replace('/[^a-zA-Z0-9]/', '_', $name);
  136. $upLogic = '';
  137. $downLogic = '';
  138. if($maxVersion == 0)
  139. {
  140. $this->generateFirstMigrationLogic($name, $newVersion, $upLogic, $downLogic);
  141. }
  142. $newClass = <<<EOF
  143. <?php
  144. /**
  145. * Migrations between versions $maxVersion and $newVersion.
  146. */
  147. class Migration$newVersion extends sfMigration
  148. {
  149. /**
  150. * Migrate up to version $newVersion.
  151. */
  152. public function up()
  153. {
  154. $upLogic
  155. }
  156. /**
  157. * Migrate down to version $maxVersion.
  158. */
  159. public function down()
  160. {
  161. $downLogic
  162. }
  163. }
  164. EOF;
  165. // write new migration stub
  166. $newFileName = $this->getMigrationsDir().DIRECTORY_SEPARATOR.$newVersion.'_'.$name.'.php';
  167. file_put_contents($newFileName, $newClass);
  168. return $newFileName;
  169. }
  170. /**
  171. * Get the list of migration filenames.
  172. *
  173. * @return array
  174. */
  175. public function getMigrations()
  176. {
  177. return $this->migrations;
  178. }
  179. /**
  180. * @return integer The lowest migration that exists
  181. */
  182. public function getMinVersion()
  183. {
  184. return $this->migrations ? $this->getMigrationNumberFromFile($this->migrations[0]) : 0;
  185. }
  186. /**
  187. * @return integer The highest existing migration that exists
  188. */
  189. public function getMaxVersion()
  190. {
  191. $count = count($this->migrations);
  192. $versions = array_keys($this->migrations);
  193. return $count ? $this->getMigrationNumberFromFile($versions[$count - 1]) : 0;
  194. }
  195. /**
  196. * Get the current schema version from the database.
  197. *
  198. * If no schema version is currently stored in the database, one is created.
  199. *
  200. * @return integer
  201. */
  202. public function getCurrentVersion()
  203. {
  204. try
  205. {
  206. $result = $this->executeQuery("SELECT max(lpad(version, 14, '0')) as version FROM schema_migration");
  207. if($result instanceof PDOStatement)
  208. {
  209. $currentVersion = $result->fetchColumn(0);
  210. }
  211. else
  212. {
  213. if($result->next())
  214. {
  215. $currentVersion = $result->getString('version');
  216. }
  217. else
  218. {
  219. throw new sfDatabaseException('Unable to retrieve current schema version.');
  220. }
  221. }
  222. }
  223. catch (Exception $e)
  224. {
  225. // assume no schema_info table exists yet so we create it
  226. $this->executeUpdate('CREATE TABLE `schema_migration` (
  227. `version` varchar(255) NOT NULL,
  228. UNIQUE KEY `unique_schema_migrations` (`version`)
  229. )');
  230. // attempt to migrate the schema_version table
  231. $currentVersion = $this->migrateSchemaVersion();
  232. }
  233. return $this->cleanVersion($currentVersion);
  234. }
  235. public function migrateSchemaVersion()
  236. {
  237. try {
  238. $result = $this->executeQuery("SELECT version FROM schema_info");
  239. if ($result instanceof PDOStatement)
  240. {
  241. $currentVersion = $result->fetchColumn(0);
  242. }
  243. else
  244. {
  245. if($result->next())
  246. {
  247. $currentVersion = $result->getInt('version');
  248. }
  249. else
  250. {
  251. throw new sfDatabaseException('Unable to retrieve current schema version.');
  252. }
  253. }
  254. for($i = 0; $i <= $currentVersion; $i++)
  255. $this->executeUpdate("INSERT INTO schema_migration(version) VALUES ($i)");
  256. return $currentVersion;
  257. } catch(Exception $e) {
  258. // ignore it; assume that the old table doesn't exist
  259. }
  260. return 0;
  261. }
  262. /**
  263. * Get the number encoded in the given migration file name.
  264. *
  265. * @param string $file The filename to look at
  266. *
  267. * @return integer
  268. */
  269. public function getMigrationNumberFromFile($file)
  270. {
  271. $number = current(explode("_", basename($file), 2));
  272. if (!ctype_digit($number))
  273. {
  274. throw new sfParseException('Migration filename could not be parsed.');
  275. }
  276. return $number;
  277. }
  278. /**
  279. * Get the directory where migration classes are saved.
  280. *
  281. * @return string
  282. */
  283. public function getMigrationsDir()
  284. {
  285. return sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'migrations';
  286. }
  287. /**
  288. * Get the directory where migration fixtures are saved.
  289. *
  290. * @return string
  291. */
  292. public function getMigrationsFixturesDir()
  293. {
  294. return $this->getMigrationsDir().DIRECTORY_SEPARATOR.'fixtures';
  295. }
  296. /**
  297. * Write the given version as current version to the database.
  298. *
  299. * @param integer $version New current version
  300. */
  301. protected function recordMigration($version)
  302. {
  303. $version = $this->cleanVersion($version);
  304. $this->executeUpdate("INSERT INTO schema_migration(version) VALUES ($version)");
  305. pake_echo_action('migrations', "migrated version $version");
  306. }
  307. protected function unrecordMigration($version)
  308. {
  309. $version = $this->cleanVersion($version);
  310. $this->executeUpdate("DELETE FROM schema_migration WHERE version='$version'");
  311. pake_echo_action('migrations', "rollback version $version");
  312. }
  313. /**
  314. * Migrate down, from version $from to version $to.
  315. *
  316. * @param integer $from
  317. * @param integer $to
  318. *
  319. * @return integer Number of executed migrations
  320. */
  321. protected function migrateDown($from, $to)
  322. {
  323. $counter = 0;
  324. // look for any unapplied migrations with versions between the from:to range.
  325. foreach($this->migrations as $version => $migration)
  326. {
  327. if(strnatcmp($this->cleanVersion($version), $to) > 0)
  328. {
  329. $counter += $this->doMigrateDown($version);
  330. }
  331. }
  332. return $counter;
  333. }
  334. public function cleanVersion($version)
  335. {
  336. return preg_replace("/^0*/", "", $version);
  337. }
  338. public function doMigrateDown($version)
  339. {
  340. $con = Propel::getConnection();
  341. if(!$this->isMigrationApplied($version))
  342. return 0;
  343. try
  344. {
  345. $con instanceof PropelPDO ? $con->beginTransaction() : $con->begin();
  346. $migration = $this->getMigrationObject($version);
  347. $migration->down();
  348. $this->unrecordMigration($version);
  349. $con->commit();
  350. }
  351. catch (Exception $e)
  352. {
  353. $con->rollback();
  354. throw $e;
  355. }
  356. return 1;
  357. }
  358. /**
  359. * Migrate up, from version $from to version $to.
  360. *
  361. * @param integer $from
  362. * @param integer $to
  363. * @return integer Number of executed migrations
  364. */
  365. protected function migrateUp($from, $to)
  366. {
  367. $counter = 0;
  368. // look for any unapplied migrations with versions between the from:to range.
  369. foreach($this->migrations as $version => $migration)
  370. {
  371. if(strnatcmp($version, $to) <= 0)
  372. {
  373. $counter += $this->doMigrateUp($version);
  374. }
  375. }
  376. return $counter;
  377. }
  378. public function doMigrateUp($version)
  379. {
  380. if($this->isMigrationApplied($version))
  381. return 0;
  382. $con = Propel::getConnection();
  383. try
  384. {
  385. $con instanceof PropelPDO ? $con->beginTransaction() : $con->begin();
  386. $migration = $this->getMigrationObject($version);
  387. $migration->up();
  388. $this->recordMigration($version);
  389. $con->commit();
  390. }
  391. catch (Exception $e)
  392. {
  393. $con->rollback();
  394. throw $e;
  395. }
  396. return 1;
  397. }
  398. /**
  399. * Get the migration object for the given version.
  400. *
  401. * @param integer $version
  402. *
  403. * @return sfMigration
  404. */
  405. protected function getMigrationObject($version)
  406. {
  407. $file = $this->getMigrationFileName($version);
  408. // load the migration class
  409. require_once $file;
  410. $migrationClass = 'Migration'.$this->getMigrationNumberFromFile($file);
  411. return new $migrationClass($this, $version);
  412. }
  413. /**
  414. * Version to filename.
  415. *
  416. * @param integer $version
  417. *
  418. * @return string Filename
  419. */
  420. protected function getMigrationFileName($version)
  421. {
  422. return $this->migrations[$version];
  423. }
  424. /**
  425. * Load all migration file names.
  426. */
  427. protected function loadMigrations()
  428. {
  429. $migrations = sfFinder::type('file')->name('/^\d{3}.*\.php$/')->maxdepth(0)->in($this->getMigrationsDir());
  430. sort($migrations);
  431. foreach($migrations as $migration)
  432. {
  433. $this->migrations[current(explode('_', basename($migration), 2))] = $migration;
  434. }
  435. // grab
  436. }
  437. public function isMigrationApplied($version)
  438. {
  439. $version = $this->cleanVersion($version);
  440. $result = $this->executeQuery("SELECT count(*) as c FROM schema_migration WHERE version='$version'");
  441. if($result instanceof PDOStatement)
  442. {
  443. $count = $result->fetchColumn(0);
  444. }
  445. else
  446. {
  447. if($result->next())
  448. {
  449. $count = $result->getString('c');
  450. }
  451. else
  452. {
  453. throw new sfDatabaseException('Unable to retrieve version info.');
  454. }
  455. }
  456. return $count > 0;
  457. }
  458. /**
  459. * Auto generate logic for the first migration.
  460. *
  461. * @param string $name
  462. * @param string $newVersion
  463. * @param string $upLogic
  464. * @param string $downLogic
  465. */
  466. protected function generateFirstMigrationLogic($name, $newVersion, &$upLogic, &$downLogic)
  467. {
  468. $sqlFiles = sfFinder::type('file')->name('*.sql')->in(sfConfig::get('sf_root_dir').'/data/sql');
  469. if ($sqlFiles)
  470. {
  471. // use propel sql files for the up logic
  472. $sql = '';
  473. foreach ($sqlFiles as $sqlFile)
  474. {
  475. $sql .= file_get_contents($sqlFile);
  476. }
  477. file_put_contents($this->getMigrationsDir().DIRECTORY_SEPARATOR.$newVersion.'_'.$name.'.sql', $sql);
  478. $upLogic .= sprintf('$this->loadSql(dirname(__FILE__).\'/%s_%s.sql\');', $newVersion, $name);
  479. // drop tables for down logic
  480. $downLines = array();
  481. // disable mysql foreign key checks
  482. if (false !== $fkChecks = strpos($sql, 'FOREIGN_KEY_CHECKS'))
  483. {
  484. $downLines[] = '$this->executeSQL(\'SET FOREIGN_KEY_CHECKS=0\');';
  485. $downLines[] = '';
  486. }
  487. preg_match_all('/DROP TABLE IF EXISTS `(\w+)`;/', $sql, $matches);
  488. foreach ($matches[1] as $match)
  489. {
  490. $downLines[] = sprintf('$this->executeSQL(\'DROP TABLE %s\');', $match);
  491. }
  492. // enable mysql foreign key checks
  493. if (false !== $fkChecks)
  494. {
  495. $downLines[] = '';
  496. $downLines[] = '$this->executeSQL(\'SET FOREIGN_KEY_CHECKS=1\');';
  497. }
  498. $downLogic .= join("\n ", $downLines);
  499. }
  500. }
  501. }