├── .gitignore ├── README.md ├── config.php.sample └── migrate.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea/ 3 | Thumbs.db 4 | ehthumbs.db 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | config.php -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mysql-migrate 2 | ====================== 3 | 4 | Version control your database with tiny one migration script. 5 | 6 | Handles migrating your MySQL database schema upwards. Easy to use in different environments: multiple developers, staging, production. Just run migrate and it will move the database schema to the latest version. Will detect conflicts and print out errors. 7 | 8 | How to use 9 | ====================== 10 | 11 | For installation inside your project directory: 12 | 13 | git clone https://github.com/idnan/mysql-migrate 14 | 15 | To add a new migration: 16 | 17 | php mysql-migrate/migrate.php make [name-without-spaces] 18 | 19 | To migrate to the latest version: 20 | 21 | php mysql-migrate/migrate.php run 22 | 23 | The migrate script will create a ".version" file in the directory from which it is run. For this reason, I recommend running the migration script from one level up. Do not checkin the version file since it needs to be local! 24 | 25 | When you add a new migration, a new script file will be created under the "migrations/" folder and this folder should be checked into the code repository. This way other environments can migrate the database using them. 26 | 27 | More info 28 | ====================== 29 | 30 | To setup your database information, make sure to run: 31 | 32 | cp config.php.sample config.php 33 | vim config.php 34 | 35 | The database version is tracked locally using file ".version". 36 | 37 | License 38 | ====================== 39 | MIT © [Adnan Ahmed](https://github.com/idnan) -------------------------------------------------------------------------------- /config.php.sample: -------------------------------------------------------------------------------- 1 | 'migrations/', 16 | 17 | 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Database Connections 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Initialize your database parameters 25 | | 26 | */ 27 | 28 | 'host' => 'localhost', 29 | 'port' => '3306', 30 | 'database' => '', 31 | 'username' => 'root', 32 | 'password' => '', 33 | 34 | ]; -------------------------------------------------------------------------------- /migrate.php: -------------------------------------------------------------------------------- 1 | migration = $migration; 46 | $this->config = include_once 'config.php'; 47 | 48 | $this->connect(); 49 | } 50 | 51 | /** 52 | * Connect to database 53 | */ 54 | public function connect() 55 | { 56 | $host = $this->config['host']; 57 | $username = $this->config['username']; 58 | $password = $this->config['password']; 59 | $port = $this->config['port']; 60 | $database = $this->config['database']; 61 | 62 | $this->conn = @(new mysqli($host, $username, $password, $database, $port)); 63 | 64 | if (!empty($this->conn->connect_error)) { 65 | echo "Failed to connect to the database." . PHP_EOL; 66 | exit(1); 67 | } 68 | } 69 | 70 | /** 71 | * Make migration 72 | */ 73 | public function make() 74 | { 75 | $migrationDir = $this->getMigrationDirectory(); 76 | $version = $this->getCurrentVersion(); 77 | 78 | echo "Current database version is: $version\n"; 79 | 80 | $new_version = $version; 81 | // Check the new version against existing migrations. 82 | $files = $this->getMigrations(); 83 | $last_file = end($files); 84 | if ($last_file !== false) { 85 | $file_version = $this->getVersionFromFile($last_file); 86 | if ($file_version > $new_version) 87 | $new_version = $file_version; 88 | } 89 | // Create migration file path. 90 | $new_version++; 91 | 92 | if (!file_exists($migrationDir)) { 93 | mkdir('migrations/', 0777); 94 | } 95 | 96 | $path = $migrationDir . static::MIGRATE_FILE_PREFIX . sprintf('%04d', $new_version); 97 | $path .= '-' . str_replace(' ', '-', $this->migration); 98 | $path .= static::MIGRATE_FILE_POSTFIX; 99 | 100 | echo "Adding a new migration script: $path" . PHP_EOL; 101 | $f = @fopen($path, 'w'); 102 | if ($f) { 103 | fputs($f, "## WRITE YOU QUERY HERE..."); 104 | fclose($f); 105 | echo "Done." . PHP_EOL; 106 | } else { 107 | echo "Failed." . PHP_EOL; 108 | } 109 | } 110 | 111 | /** 112 | * Run migrations 113 | */ 114 | public function run() 115 | { 116 | $files = $this->getMigrations(); 117 | $version = $this->getCurrentVersion(); 118 | $migrationDir = $this->getMigrationDirectory(); 119 | 120 | // Check to make sure there are no conflicts such as 2 files under the same version. 121 | $errors = []; 122 | $last_file = false; 123 | $last_version = false; 124 | foreach ($files as $file) { 125 | $file_version = $this->getVersionFromFile($file); 126 | if ($last_version !== false && $last_version === $file_version) { 127 | $errors[] = "$last_file --- $file"; 128 | } 129 | $last_version = $file_version; 130 | $last_file = $file; 131 | } 132 | if (count($errors) > 0) { 133 | echo "Error: You have multiple files using the same version. " . 134 | "To resolve, move some of the files up so each one gets a unique version." . PHP_EOL; 135 | foreach ($errors as $error) { 136 | echo " $error" . PHP_EOL; 137 | } 138 | exit; 139 | } 140 | 141 | // Run all the new files. 142 | $found_new = false; 143 | foreach ($files as $file) { 144 | $file_version = $this->getVersionFromFile($file); 145 | if ($file_version <= $version) { 146 | continue; 147 | } 148 | 149 | echo "Running: $file" . PHP_EOL; 150 | $query = file_get_contents($migrationDir . $file); 151 | $this->query($query); 152 | echo "Done." . PHP_EOL; 153 | 154 | $version = $file_version; 155 | $found_new = true; 156 | // Output the new version number. 157 | $f = fopen(static::MIGRATE_VERSION_FILE, 'w'); 158 | if ($f) { 159 | fputs($f, $version); 160 | fclose($f); 161 | } else { 162 | echo "Failed to output new version to " . static::MIGRATE_VERSION_FILE . PHP_EOL; 163 | } 164 | } 165 | if ($found_new) { 166 | echo "Migration complete." . PHP_EOL; 167 | } else { 168 | echo "Your database is up-to-date." . PHP_EOL; 169 | } 170 | } 171 | 172 | /** 173 | * Return current migration number 174 | * 175 | * @return int 176 | * 177 | */ 178 | private function getCurrentVersion() 179 | { 180 | $version = 0; 181 | $f = @fopen(static::MIGRATE_VERSION_FILE, 'r'); 182 | if ($f) { 183 | $version = intval(fgets($f)); 184 | fclose($f); 185 | } 186 | 187 | return $version; 188 | } 189 | 190 | /** 191 | * Query Database 192 | * 193 | * @param $query 194 | * 195 | * @return bool 196 | */ 197 | private function query($query) 198 | { 199 | $result = $this->conn->query($query); 200 | if (!$result) { 201 | echo "Migration failed: " . $this->conn->errorInfo() . "\n"; 202 | echo "Aborting.\n"; 203 | $this->conn->rollBack(); 204 | exit; 205 | } 206 | 207 | return true; 208 | } 209 | 210 | /** 211 | * Find all the migration files in the directory and return the sorted. 212 | * 213 | * @return array 214 | */ 215 | private function getMigrations() 216 | { 217 | $files = []; 218 | $dir = @opendir($this->getMigrationDirectory()); 219 | while ($file = @readdir($dir)) { 220 | if (substr($file, 0, strlen(static::MIGRATE_FILE_PREFIX)) == static::MIGRATE_FILE_PREFIX) { 221 | $files[] = $file; 222 | } 223 | } 224 | asort($files); 225 | 226 | return $files; 227 | } 228 | 229 | /** 230 | * Return version from file 231 | * 232 | * @param $file 233 | * 234 | * @return int 235 | */ 236 | private function getVersionFromFile($file) 237 | { 238 | return intval(substr($file, strlen(static::MIGRATE_FILE_PREFIX))); 239 | } 240 | 241 | /** 242 | * Return migration directory 243 | * 244 | * @return string 245 | */ 246 | private function getMigrationDirectory() 247 | { 248 | return $this->config['migrations_dir']; 249 | } 250 | } 251 | 252 | $command = !empty($argv[1]) ? strtolower($argv[1]) : 'invalid'; 253 | $migration = !empty($argv[2]) ? strtolower($argv[2]) : ''; 254 | 255 | if (count($argv) <= 1 || 256 | !in_array($command, ['make', 'run']) || 257 | ($command == 'make' && empty($migration)) 258 | ) { 259 | echo "Usage: 260 | To add new migration: 261 | php php-mysql-migrate/migrate.php make 262 | To migrate your database: 263 | php php-mysql-migrate/migrate.php migrate 264 | " . PHP_EOL; 265 | exit; 266 | } 267 | 268 | $migration = new Migrate($migration); 269 | $migration->$command(); --------------------------------------------------------------------------------