1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23:
24: class Horde_Db_Migration_Migrator
25: {
26: 27: 28:
29: protected $_direction = null;
30:
31: 32: 33:
34: protected $_migrationsPath = null;
35:
36: 37: 38:
39: protected $_targetVersion = null;
40:
41: 42: 43:
44: protected $_schemaTableName = 'schema_info';
45:
46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58:
59: public function __construct(Horde_Db_Adapter $connection,
60: Horde_Log_Logger $logger = null,
61: array $options = array())
62: {
63: if (!$connection->supportsMigrations()) {
64: throw new Horde_Db_Migration_Exception('This database does not yet support migrations');
65: }
66:
67: $this->_connection = $connection;
68: $this->_logger = $logger ? $logger : new Horde_Support_Stub();
69: $this->_inflector = new Horde_Support_Inflector();
70: if (isset($options['migrationsPath'])) {
71: $this->_migrationsPath = $options['migrationsPath'];
72: }
73: if (isset($options['schemaTableName'])) {
74: $this->_schemaTableName = $options['schemaTableName'];
75: }
76:
77: $this->_initializeSchemaInformation();
78: }
79:
80: 81: 82:
83: public function migrate($targetVersion = null)
84: {
85: $currentVersion = $this->getCurrentVersion();
86:
87: if ($targetVersion == null || $currentVersion < $targetVersion) {
88: $this->up($targetVersion);
89: } elseif ($currentVersion > $targetVersion) {
90:
91: $this->down($targetVersion);
92: }
93: }
94:
95: 96: 97:
98: public function up($targetVersion = null)
99: {
100: $this->_targetVersion = $targetVersion;
101: $this->_direction = 'up';
102: $this->_doMigrate();
103: }
104:
105: 106: 107:
108: public function down($targetVersion = null)
109: {
110: $this->_targetVersion = $targetVersion;
111: $this->_direction = 'down';
112: $this->_doMigrate();
113: }
114:
115: 116: 117:
118: public function getCurrentVersion()
119: {
120: return in_array($this->_schemaTableName, $this->_connection->tables())
121: ? $this->_connection->selectValue('SELECT version FROM ' . $this->_schemaTableName)
122: : 0;
123: }
124:
125: 126: 127:
128: public function getTargetVersion()
129: {
130: $migrations = array();
131: foreach ($this->_getMigrationFiles() as $migrationFile) {
132: list($version, $name) = $this->_getMigrationVersionAndName($migrationFile);
133: $this->_assertUniqueMigrationVersion($migrations, $version);
134: $migrations[$version] = $name;
135: }
136:
137:
138: uksort($migrations, 'strnatcmp');
139:
140: return key(array_reverse($migrations, true));
141: }
142:
143: 144: 145:
146: public function setMigrationsPath($migrationsPath)
147: {
148: $this->_migrationsPath = $migrationsPath;
149: }
150:
151: 152: 153:
154: public function setLogger(Horde_Log_Logger $logger)
155: {
156: $this->_logger = $logger;
157: }
158:
159: 160: 161:
162: public function setInflector(Horde_Support_Inflector $inflector)
163: {
164: $this->_inflector = $inflector;
165: }
166:
167: 168: 169:
170: protected function _doMigrate()
171: {
172: foreach ($this->_getMigrationClasses() as $migration) {
173: if ($this->_hasReachedTargetVersion($migration->version)) {
174: $this->_logger->info('Reached target version: ' . $this->_targetVersion);
175: return;
176: }
177: if ($this->_isIrrelevantMigration($migration->version)) {
178: continue;
179: }
180:
181: $this->_logger->info('Migrating ' . ($this->_direction == 'up' ? 'to ' : 'from ') . get_class($migration) . ' (' . $migration->version . ')');
182: $migration->migrate($this->_direction);
183: $this->_setSchemaVersion($migration->version);
184: }
185: }
186:
187: 188: 189:
190: protected function _getMigrationClasses()
191: {
192: $migrations = array();
193: foreach ($this->_getMigrationFiles() as $migrationFile) {
194: require_once $migrationFile;
195: list($version, $name) = $this->_getMigrationVersionAndName($migrationFile);
196: $this->_assertUniqueMigrationVersion($migrations, $version);
197: $migrations[$version] = $this->_getMigrationClass($name, $version);
198: }
199:
200:
201: uksort($migrations, 'strnatcmp');
202: $sorted = array_values($migrations);
203:
204: return $this->_isDown() ? array_reverse($sorted) : $sorted;
205: }
206:
207: 208: 209: 210: 211: 212:
213: protected function _assertUniqueMigrationVersion($migrations, $version)
214: {
215: if (isset($migrations[$version])) {
216: throw new Horde_Db_Migration_Exception('Multiple migrations have the version number ' . $version);
217: }
218: }
219:
220: 221: 222: 223: 224:
225: protected function _getMigrationFiles()
226: {
227: return array_keys(
228: iterator_to_array(
229: new RegexIterator(
230: new RecursiveIteratorIterator(
231: new RecursiveDirectoryIterator(
232: $this->_migrationsPath
233: )
234: ),
235: '/' . preg_quote(DIRECTORY_SEPARATOR, '/') . '\d+_.*\.php$/',
236: RecursiveRegexIterator::MATCH,
237: RegexIterator::USE_KEY
238: )
239: )
240: );
241: }
242:
243: 244: 245: 246: 247: 248: 249: 250:
251: protected function _getMigrationClass($migrationName, $version)
252: {
253: $className = $this->_inflector->camelize($migrationName);
254: $class = new $className($this->_connection, $version);
255: $class->setLogger($this->_logger);
256:
257: return $class;
258: }
259:
260: 261: 262: 263: 264:
265: protected function _getMigrationVersionAndName($migrationFile)
266: {
267: preg_match_all('/([0-9]+)_([_a-z0-9]*).php/', $migrationFile, $matches);
268: return array($matches[1][0], $matches[2][0]);
269: }
270:
271: 272: 273:
274: protected function _initializeSchemaInformation()
275: {
276: if (in_array($this->_schemaTableName, $this->_connection->tables())) {
277: return;
278: }
279: $schemaTable = $this->_connection->createTable($this->_schemaTableName, array('autoincrementKey' => false));
280: $schemaTable->column('version', 'integer');
281: $schemaTable->end();
282: $this->_connection->insert('INSERT INTO ' . $this->_schemaTableName . ' (version) VALUES (0)');
283: }
284:
285: 286: 287:
288: protected function _setSchemaVersion($version)
289: {
290: $version = $this->_isDown() ? $version - 1 : $version;
291: if ($version) {
292: $sql = 'UPDATE ' . $this->_schemaTableName . ' SET version = ' . (int)$version;
293: $this->_connection->update($sql);
294: } else {
295: $this->_connection->dropTable($this->_schemaTableName);
296: }
297: }
298:
299: 300: 301:
302: protected function _isUp()
303: {
304: return $this->_direction == 'up';
305: }
306:
307: 308: 309:
310: protected function _isDown()
311: {
312: return $this->_direction == 'down';
313: }
314:
315: 316: 317:
318: protected function _hasReachedTargetVersion($version)
319: {
320: if ($this->_targetVersion === null) {
321: return false;
322: }
323:
324: return ($this->_isUp() && $version - 1 >= $this->_targetVersion) ||
325: ($this->_isDown() && $version <= $this->_targetVersion);
326: }
327:
328: 329: 330: 331: 332:
333: protected function _isIrrelevantMigration($version)
334: {
335: return ($this->_isUp() && $version <= self::getCurrentVersion()) ||
336: ($this->_isDown() && $version > self::getCurrentVersion());
337: }
338: }
339: