| 1 | <?php␊ |
| 2 | /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */␊ |
| 3 | /*␊ |
| 4 | # ***** BEGIN LICENSE BLOCK *****␊ |
| 5 | # This file is part of Plume Framework, a simple PHP Application Framework.␊ |
| 6 | # Copyright (C) 2001-2007 Loic d'Anterroches and contributors.␊ |
| 7 | #␊ |
| 8 | # Plume Framework is free software; you can redistribute it and/or modify␊ |
| 9 | # it under the terms of the GNU Lesser General Public License as published by␊ |
| 10 | # the Free Software Foundation; either version 2.1 of the License, or␊ |
| 11 | # (at your option) any later version.␊ |
| 12 | #␊ |
| 13 | # Plume Framework is distributed in the hope that it will be useful,␊ |
| 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of␊ |
| 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the␊ |
| 16 | # GNU Lesser General Public License for more details.␊ |
| 17 | #␊ |
| 18 | # You should have received a copy of the GNU Lesser General Public License␊ |
| 19 | # along with this program; if not, write to the Free Software␊ |
| 20 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA␊ |
| 21 | #␊ |
| 22 | # ***** END LICENSE BLOCK ***** */␊ |
| 23 | ␊ |
| 24 | /**␊ |
| 25 | * A class to manage the migration of the code from one version to␊ |
| 26 | * another, upward or downward.␊ |
| 27 | *␊ |
| 28 | * You can directly use the migrate.php script.␊ |
| 29 | *␊ |
| 30 | * Simple example usage:␊ |
| 31 | *␊ |
| 32 | * <pre>␊ |
| 33 | * $m = new Pluf_Migration('MyApp');␊ |
| 34 | * $m->migrate();␊ |
| 35 | *␊ |
| 36 | * // Install the application MyApp␊ |
| 37 | * $m = new Pluf_Migration('MyApp');␊ |
| 38 | * $m->install();␊ |
| 39 | * // Uninstall the application MyApp␊ |
| 40 | * $m->unInstall();␊ |
| 41 | *␊ |
| 42 | * $m = new Pluf_Migration();␊ |
| 43 | * $m->migrate(); // migrate all the installed app to the newest version.␊ |
| 44 | *␊ |
| 45 | * $m = new Pluf_Migration();␊ |
| 46 | * $m->migrate(3); // migrate (upgrade or downgrade) to version 3␊ |
| 47 | * </pre>␊ |
| 48 | *␊ |
| 49 | */␊ |
| 50 | class Pluf_Migration␊ |
| 51 | {␊ |
| 52 | protected $app = ''; /**< Application beeing migrated. */␊ |
| 53 | public $apps = array(); /**< Applications which are going to be migrated. */␊ |
| 54 | public $to_version = null; /**< Target version for the migration. */␊ |
| 55 | public $dry_run = false; /**< Set to true to not act. */␊ |
| 56 | public $display = false; /**< Display on the console what is done. */␊ |
| 57 | ␊ |
| 58 | /**␊ |
| 59 | * Create a new migration.␊ |
| 60 | *␊ |
| 61 | * @param mixed Application or array of applications to migrate.␊ |
| 62 | */␊ |
| 63 | public function __construct($app=null)␊ |
| 64 | {␊ |
| 65 | if (!is_null($app)) {␊ |
| 66 | if (is_array($app)) {␊ |
| 67 | $this->apps = $app;␊ |
| 68 | } else {␊ |
| 69 | $this->apps = array($app);␊ |
| 70 | }␊ |
| 71 | } else {␊ |
| 72 | $this->apps = Pluf::f('installed_apps');␊ |
| 73 | }␊ |
| 74 | }␊ |
| 75 | ␊ |
| 76 | ␊ |
| 77 | /**␊ |
| 78 | * Install the application.␊ |
| 79 | *␊ |
| 80 | * Basically run the base install function for each application␊ |
| 81 | * and then set the version to the latest migration.␊ |
| 82 | */␊ |
| 83 | public function install()␊ |
| 84 | {␊ |
| 85 | foreach ($this->apps as $app) {␊ |
| 86 | $this->installApp($app);␊ |
| 87 | }␊ |
| 88 | return true;␊ |
| 89 | }␊ |
| 90 | ␊ |
| 91 | /**␊ |
| 92 | * Uninstall the application.␊ |
| 93 | */␊ |
| 94 | public function unInstall()␊ |
| 95 | {␊ |
| 96 | $apps = array_reverse($this->apps);␊ |
| 97 | foreach ($apps as $app) {␊ |
| 98 | $this->installApp($app, true);␊ |
| 99 | }␊ |
| 100 | }␊ |
| 101 | ␊ |
| 102 | /**␊ |
| 103 | * Backup the application.␊ |
| 104 | *␊ |
| 105 | * @param string Path to the backup folder␊ |
| 106 | * @param string Backup name (null)␊ |
| 107 | */␊ |
| 108 | public function backup($path, $name=null)␊ |
| 109 | {␊ |
| 110 | foreach ($this->apps as $app) {␊ |
| 111 | $func = $app.'_Migrations_Backup_run';␊ |
| 112 | Pluf::loadFunction($func);␊ |
| 113 | if ($this->display) {␊ |
| 114 | echo($func."\n");␊ |
| 115 | }␊ |
| 116 | if (!$this->dry_run) {␊ |
| 117 | $ret = $func($path, $name); ␊ |
| 118 | }␊ |
| 119 | }␊ |
| 120 | return true;␊ |
| 121 | }␊ |
| 122 | ␊ |
| 123 | /**␊ |
| 124 | * Restore the application.␊ |
| 125 | *␊ |
| 126 | * @param string Path to the backup folder␊ |
| 127 | * @param string Backup name ␊ |
| 128 | */␊ |
| 129 | public function restore($path, $name)␊ |
| 130 | {␊ |
| 131 | foreach ($this->apps as $app) {␊ |
| 132 | $func = $app.'_Migrations_Backup_restore';␊ |
| 133 | Pluf::loadFunction($func);␊ |
| 134 | if ($this->display) {␊ |
| 135 | echo($func."\n");␊ |
| 136 | }␊ |
| 137 | if (!$this->dry_run) {␊ |
| 138 | $ret = $func($path, $name); ␊ |
| 139 | }␊ |
| 140 | }␊ |
| 141 | return true;␊ |
| 142 | }␊ |
| 143 | ␊ |
| 144 | /**␊ |
| 145 | * Run the migration.␊ |
| 146 | *␊ |
| 147 | */␊ |
| 148 | public function migrate($to_version=null)␊ |
| 149 | {␊ |
| 150 | $this->to_version = $to_version;␊ |
| 151 | foreach ($this->apps as $app) {␊ |
| 152 | $this->app = $app;␊ |
| 153 | $migrations = $this->findMigrations();␊ |
| 154 | // The run will throw an exception in case of error.␊ |
| 155 | $this->runMigrations($migrations); ␊ |
| 156 | }␊ |
| 157 | return true;␊ |
| 158 | }␊ |
| 159 | ␊ |
| 160 | /**␊ |
| 161 | * Un/Install the given application.␊ |
| 162 | *␊ |
| 163 | * @param string Application to install.␊ |
| 164 | * @param bool Uninstall (false)␊ |
| 165 | */␊ |
| 166 | public function installApp($app, $uninstall=false)␊ |
| 167 | {␊ |
| 168 | if ($uninstall) {␊ |
| 169 | $func = $app.'_Migrations_Install_teardown';␊ |
| 170 | } else {␊ |
| 171 | $func = $app.'_Migrations_Install_setup';␊ |
| 172 | }␊ |
| 173 | $ret = true;␊ |
| 174 | Pluf::loadFunction($func);␊ |
| 175 | if ($this->display) {␊ |
| 176 | echo($func."\n");␊ |
| 177 | }␊ |
| 178 | if (!$this->dry_run) {␊ |
| 179 | $ret = $func(); // Run the install/uninstall␊ |
| 180 | if (!$uninstall) {␊ |
| 181 | // ␊ |
| 182 | $this->app = $app;␊ |
| 183 | $migrations = $this->findMigrations();␊ |
| 184 | if (count($migrations) > 0) {␊ |
| 185 | $to_version = max(array_keys($migrations));␊ |
| 186 | } else {␊ |
| 187 | $to_version = 0;␊ |
| 188 | }␊ |
| 189 | $this->setAppVersion($app, $to_version);␊ |
| 190 | } else {␊ |
| 191 | if ($app != 'Pluf') {␊ |
| 192 | // If Pluf we do not have the schema info table␊ |
| 193 | // anymore␊ |
| 194 | $this->delAppInfo($app);␊ |
| 195 | }␊ |
| 196 | }␊ |
| 197 | }␊ |
| 198 | return $ret;␊ |
| 199 | }␊ |
| 200 | ␊ |
| 201 | ␊ |
| 202 | /**␊ |
| 203 | * Find the migrations for the current app.␊ |
| 204 | *␊ |
| 205 | * @return array Migrations names indexed by order.␊ |
| 206 | */␊ |
| 207 | public function findMigrations()␊ |
| 208 | {␊ |
| 209 | $migrations = array();␊ |
| 210 | if (false !== ($mdir = Pluf::fileExists($this->app.'/Migrations'))) {␊ |
| 211 | $dir = new DirectoryIterator($mdir);␊ |
| 212 | foreach($dir as $file) {␊ |
| 213 | $matches = array();␊ |
| 214 | if (!$file->isDot() && !$file->isDir()␊ |
| 215 | && preg_match('#^(\d+)#', $file->getFilename(), $matches)) {␊ |
| 216 | $info = pathinfo($file->getFilename());␊ |
| 217 | $migrations[(int)$matches[1]] = $info['filename'];␊ |
| 218 | }␊ |
| 219 | }␊ |
| 220 | }␊ |
| 221 | return $migrations;␊ |
| 222 | }␊ |
| 223 | ␊ |
| 224 | /**␊ |
| 225 | * Run the migrations.␊ |
| 226 | *␊ |
| 227 | * From an array of possible migrations, it will first get the␊ |
| 228 | * current version of the app and then based on $this->to_version␊ |
| 229 | * will run the migrations in the right order or do nothing if␊ |
| 230 | * nothing to be done.␊ |
| 231 | *␊ |
| 232 | * @param array Possible migrations.␊ |
| 233 | */␊ |
| 234 | public function runMigrations($migrations)␊ |
| 235 | {␊ |
| 236 | if (empty($migrations)) {␊ |
| 237 | return;␊ |
| 238 | }␊ |
| 239 | $current = $this->getAppVersion($this->app);␊ |
| 240 | if ($this->to_version === null) {␊ |
| 241 | $to_version = max(array_keys($migrations));␊ |
| 242 | } else {␊ |
| 243 | $to_version = $this->to_version;␊ |
| 244 | }␊ |
| 245 | if ($to_version == $current) {␊ |
| 246 | return; // Nothing to do␊ |
| 247 | }␊ |
| 248 | $the_way = 'up'; // Tribute to Pat Metheny␊ |
| 249 | if ($to_version > $current) {␊ |
| 250 | // upgrade␊ |
| 251 | $min = $current + 1;␊ |
| 252 | $max = $to_version;␊ |
| 253 | } else {␊ |
| 254 | // downgrade␊ |
| 255 | $the_way = 'do';␊ |
| 256 | $max = $current;␊ |
| 257 | $min = $to_version + 1;␊ |
| 258 | }␊ |
| 259 | // Filter the migrations␊ |
| 260 | $to_run = array();␊ |
| 261 | foreach ($migrations as $order=>$name) {␊ |
| 262 | if ($order < $min or $order > $max) {␊ |
| 263 | continue;␊ |
| 264 | }␊ |
| 265 | if ($the_way == 'up') {␊ |
| 266 | $to_run[] = array($order, $name);␊ |
| 267 | } else {␊ |
| 268 | array_unshift($to_run, array($order, $name));␊ |
| 269 | }␊ |
| 270 | }␊ |
| 271 | asort($to_run);␊ |
| 272 | // Run the migrations␊ |
| 273 | foreach ($to_run as $migration) {␊ |
| 274 | $this->runMigration($migration, $the_way);␊ |
| 275 | }␊ |
| 276 | }␊ |
| 277 | ␊ |
| 278 | /**␊ |
| 279 | * Run the given migration.␊ |
| 280 | */␊ |
| 281 | public function runMigration($migration, $the_way='up')␊ |
| 282 | {␊ |
| 283 | $target_version = ($the_way == 'up') ? $migration[0] : $migration[0]-1;␊ |
| 284 | if ($this->display) {␊ |
| 285 | echo($migration[0].' '.$migration[1].' '.$the_way."\n");␊ |
| 286 | }␊ |
| 287 | if (!$this->dry_run) {␊ |
| 288 | if ($the_way == 'up') {␊ |
| 289 | $func = $this->app.'_Migrations_'.$migration[1].'_up';␊ |
| 290 | } else {␊ |
| 291 | $func = $this->app.'_Migrations_'.$migration[1].'_down';␊ |
| 292 | }␊ |
| 293 | Pluf::loadFunction($func);␊ |
| 294 | $func(); // Real migration run␊ |
| 295 | $this->setAppVersion($this->app, $target_version);␊ |
| 296 | } ␊ |
| 297 | }␊ |
| 298 | ␊ |
| 299 | /**␊ |
| 300 | * Set the application version.␊ |
| 301 | *␊ |
| 302 | * @param string Application␊ |
| 303 | * @param int Version␊ |
| 304 | * @return true␊ |
| 305 | */␊ |
| 306 | public function setAppVersion($app, $version)␊ |
| 307 | {␊ |
| 308 | $gschema = new Pluf_DB_SchemaInfo();␊ |
| 309 | $sql = new Pluf_SQL('application=%s', $app);␊ |
| 310 | $appinfo = $gschema->getList(array('filter' => $sql->gen()));␊ |
| 311 | if ($appinfo->count() == 1) {␊ |
| 312 | $appinfo[0]->version = $version;␊ |
| 313 | $appinfo[0]->update();␊ |
| 314 | } else {␊ |
| 315 | $schema = new Pluf_DB_SchemaInfo();␊ |
| 316 | $schema->application = $app;␊ |
| 317 | $schema->version = $version;␊ |
| 318 | $schema->create();␊ |
| 319 | }␊ |
| 320 | return true;␊ |
| 321 | }␊ |
| 322 | ␊ |
| 323 | /**␊ |
| 324 | * Remove the application information.␊ |
| 325 | *␊ |
| 326 | * @param string Application␊ |
| 327 | * @return true␊ |
| 328 | */␊ |
| 329 | public function delAppInfo($app)␊ |
| 330 | {␊ |
| 331 | $gschema = new Pluf_DB_SchemaInfo();␊ |
| 332 | $sql = new Pluf_SQL('application=%s', $app);␊ |
| 333 | $appinfo = $gschema->getList(array('filter' => $sql->gen()));␊ |
| 334 | if ($appinfo->count() == 1) {␊ |
| 335 | $appinfo[0]->delete();␊ |
| 336 | }␊ |
| 337 | return true;␊ |
| 338 | }␊ |
| 339 | ␊ |
| 340 | ␊ |
| 341 | ␊ |
| 342 | /**␊ |
| 343 | * Get the current version of the app.␊ |
| 344 | *␊ |
| 345 | * @param string Application.␊ |
| 346 | * @return int Version.␊ |
| 347 | */␊ |
| 348 | public function getAppVersion($app)␊ |
| 349 | {␊ |
| 350 | try {␊ |
| 351 | $db =& Pluf::db();␊ |
| 352 | $res = $db->select('SELECT version FROM '.$db->pfx.'schema_info WHERE application='.$db->esc($app));␊ |
| 353 | return (int) $res[0]['version'];␊ |
| 354 | } catch (Exception $e) {␊ |
| 355 | // We should not be here, only in the case of nothing␊ |
| 356 | // installed. I am not sure if this is a good way to␊ |
| 357 | // handle this border case anyway. Maybe better to have an␊ |
| 358 | // 'install' method to run all the migrations in order.␊ |
| 359 | return 0;␊ |
| 360 | }␊ |
| 361 | }␊ |
| 362 | } |