├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENCE ├── README.md ├── bin ├── phpmig ├── phpmig.bat └── phpmig.stub.php ├── composer.json ├── phpunit.xml.dist ├── src └── Phpmig │ ├── Adapter │ ├── AdapterInterface.php │ ├── CodeIgniter │ │ └── Db.php │ ├── Doctrine │ │ └── DBAL.php │ ├── File │ │ └── Flat.php │ ├── Illuminate │ │ └── Database.php │ ├── Mongo.php │ ├── MongoDb.php │ ├── PDO │ │ ├── Sql.php │ │ ├── SqlOci.php │ │ └── SqlPgsql.php │ └── Zend │ │ ├── Db.php │ │ └── DbAdapter.php │ ├── Api │ └── PhpmigApplication.php │ ├── Console │ ├── Command │ │ ├── AbstractCommand.php │ │ ├── CheckCommand.php │ │ ├── DownCommand.php │ │ ├── GenerateCommand.php │ │ ├── InitCommand.php │ │ ├── MigrateCommand.php │ │ ├── RedoCommand.php │ │ ├── RollbackCommand.php │ │ ├── StatusCommand.php │ │ └── UpCommand.php │ └── PhpmigApplication.php │ ├── Migration │ ├── Migration.php │ └── Migrator.php │ └── Pimple │ └── Pimple.php └── tests └── Phpmig ├── Api └── PhpmigApplicationTest.php ├── Console └── Command │ └── InitCommandTest.php ├── Migration └── MigrationTest.php └── PhpmigApplicationTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | build/ 3 | composer.lock 4 | .phpunit.result.cache 5 | 6 | 7 | .project 8 | .settings 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | before_script: 14 | - composer self-update 15 | - composer install --no-interaction --dev 16 | 17 | script: 18 | vendor/bin/phpunit 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ### 0.5.0 (2013-06-26) 5 | 6 | * Removed pear as an installation option 7 | * BC break: visibility of init method on migration classes changed from protected to public 8 | * BC break: pimple/pimple is no longer installed as a dependency of phpmig -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Phpmig: Copyright (C) 2011 Dave Marshall 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Phpmig 2 | ====== 3 | 4 | [![Build 5 | Status](https://travis-ci.org/davedevelopment/phpmig.png)](https://travis-ci.org/davedevelopment/phpmig) 6 | 7 | What is it? 8 | ----------- 9 | 10 | Phpmig is a (database) migration tool for php, that should be adaptable for use 11 | with most PHP 5.3+ projects. It's kind of like [doctrine 12 | migrations][doctrinemigrations], without the [doctrine][doctrine]. Although you 13 | can use doctrine if you want. And ironically, I use doctrine in my examples. 14 | 15 | How does it work? 16 | ----------------- 17 | 18 | ```bash 19 | $ phpmig migrate 20 | ``` 21 | 22 | Phpmig aims to be vendor/framework independent, and in doing so, requires you to 23 | do a little bit of work up front to use it. 24 | 25 | Phpmig requires a bootstrap file, that must return an object that implements the 26 | ArrayAccess interface with several predefined keys. We recommend returning an 27 | instance of [Pimple][pimple], a simple dependency injection container. This is 28 | also an ideal opportunity to expose your own services to the migrations 29 | themselves, which have access to the container, such as a [schema management 30 | abstraction][doctrineschemamanager]. 31 | 32 | Getting Started 33 | --------------- 34 | 35 | The best way to install phpmig is using composer: 36 | 37 | ```bash 38 | $ curl -sS https://getcomposer.org/installer | php 39 | $ php composer.phar require davedevelopment/phpmig 40 | ``` 41 | 42 | You can then use the localised version of phpmig for that project 43 | 44 | ```bash 45 | $ bin/phpmig --version 46 | ``` 47 | 48 | Phpmig can do a little configuring for you to get started, go to the root of 49 | your project and: 50 | 51 | ```bash 52 | $ phpmig init 53 | +d ./migrations Place your migration files in here 54 | +f ./phpmig.php Create services in here 55 | $ 56 | ``` 57 | 58 | Note that you can move phpmig.php to config/phpmig.php, the commands will look 59 | first in the config directory than in the root. 60 | 61 | Phpmig can generate migrations using the generate command. Migration files are named 62 | versionnumber_name.php, where version number is made up of 0-9 and name is 63 | CamelCase or snake\_case. Each migration file should contain a class with the 64 | same name as the file in CamelCase. 65 | 66 | ```bash 67 | $ phpmig generate AddRatingToLolCats 68 | +f ./migrations/20111018171411_AddRatingToLolCats.php 69 | $ phpmig status 70 | 71 | Status Migration ID Migration Name 72 | ----------------------------------------- 73 | down 20111018171929 AddRatingToLolCats 74 | 75 | Use the migrate command to run migrations 76 | 77 | $ phpmig migrate 78 | == 20111018171411 AddRatingToLolCats migrating 79 | == 20111018171411 AddRatingToLolCats migrated 0.0005s 80 | $ phpmig status 81 | 82 | Status Migration ID Migration Name 83 | ----------------------------------------- 84 | up 20111018171929 AddRatingToLolCats 85 | $ 86 | ``` 87 | 88 | Better Persistence 89 | ------------------ 90 | 91 | The init command creates a bootstrap file that specifies a flat file to use to 92 | track which migrations have been run, which isn't great. You can use the 93 | provided adapters to store this information in your database. 94 | 95 | ```php 96 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 108 | return $dbh; 109 | }; 110 | 111 | $container['phpmig.adapter'] = function ($c) { 112 | return new Adapter\PDO\Sql($c['db'], 'migrations'); 113 | }; 114 | 115 | $container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . 'migrations'; 116 | 117 | return $container; 118 | 119 | ``` 120 | 121 | ### Postgres PDO `SqlPgsql` 122 | Adds support for qualifying the migrations table with a schema. 123 | 124 | ```php 125 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 137 | return $dbh; 138 | }; 139 | 140 | $container['phpmig.adapter'] = function ($c) { 141 | return new Adapter\PDO\SqlPgsql($c['db'], 'migrations', 'migrationschema'); 142 | }; 143 | 144 | return $container; 145 | ``` 146 | 147 | 148 | 149 | Or you can use Doctrine's DBAL: 150 | 151 | ```php 152 | 'pdo_sqlite', 167 | 'path' => __DIR__ . DIRECTORY_SEPARATOR . 'db.sqlite', 168 | )); 169 | }; 170 | 171 | $container['phpmig.adapter'] = function ($c) { 172 | return new Adapter\Doctrine\DBAL($c['db'], 'migrations'); 173 | }; 174 | 175 | $container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . 'migrations'; 176 | 177 | return $container; 178 | ``` 179 | 180 | Setting up migrations with Zend Framework requires a couple additional steps. You first need to prepare 181 | the configuration. It might be in any format supported by Zend_Config. Here is an 182 | example in YAML for MySQL: 183 | 184 | ```yaml 185 | phpmig: 186 | tableName: migrations 187 | createStatement: CREATE TABLE migrations ( version VARCHAR(255) NOT NULL ); 188 | ``` 189 | 190 | In configuration file you need to provide the table name where the migrations will 191 | be stored and a create statement. You can use one of the configurations provided 192 | in the config folder for some common RDBMS. 193 | 194 | Here is how the bootstrap file should look: 195 | 196 | ```php 197 | registerNamespace('Zend_'); 210 | 211 | use Phpmig\Adapter\Zend\Db; 212 | use Pimple\Container; 213 | 214 | $container = new Container(); 215 | 216 | $container['db'] = function () { 217 | return Zend_Db::factory('pdo_mysql', array( 218 | 'dbname' => 'DBNAME', 219 | 'username' => 'USERNAME', 220 | 'password' => 'PASSWORD', 221 | 'host' => 'localhost' 222 | )); 223 | }; 224 | 225 | $container['phpmig.adapter'] = function($c) { 226 | $configuration = null; 227 | $configurationFile = PHPMIG_PATH . '/config/mysql.yaml'; 228 | 229 | if (file_exists($configurationFile)) { 230 | $configuration = new Zend_Config_Yaml($configurationFile, null, array('ignore_constants' => true)); 231 | } 232 | 233 | return new Db($c['db'], $configuration); 234 | }; 235 | 236 | $container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . 'migrations'; 237 | 238 | return $container; 239 | ``` 240 | 241 | Example with Eloquent ORM 5.1 242 | ------------------ 243 | ```php 244 | 'xxx', 254 | 'host' => 'xxx', 255 | 'database' => 'xxx', 256 | 'username' => 'xxx', 257 | 'password' => 'x', 258 | 'charset' => 'xxx', 259 | 'collation' => 'xxx', 260 | 'prefix' => '', 261 | ]; 262 | 263 | $container['db'] = function ($c) { 264 | $capsule = new Capsule(); 265 | $capsule->addConnection($c['config']); 266 | $capsule->setAsGlobal(); 267 | $capsule->bootEloquent(); 268 | 269 | return $capsule; 270 | }; 271 | 272 | $container['phpmig.adapter'] = function($c) { 273 | return new Adapter\Illuminate\Database($c['db'], 'migrations'); 274 | }; 275 | $container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . 'migrations'; 276 | 277 | return $container; 278 | ``` 279 | 280 | 281 | Writing Migrations 282 | ------------------ 283 | 284 | The migrations should extend the Phpmig\Migration\Migration class, and have 285 | access to the container. For example, assuming you've rewritten your bootstrap 286 | file like above: 287 | 288 | ```php 289 | getContainer(); 302 | $container['db']->query($sql); 303 | } 304 | 305 | /** 306 | * Undo the migration 307 | */ 308 | public function down() 309 | { 310 | $sql = "ALTER TABLE `lol_cats` DROP COLUMN `rating`"; 311 | $container = $this->getContainer(); 312 | $container['db']->query($sql); 313 | } 314 | } 315 | ``` 316 | 317 | Customising the migration template 318 | ----------------------------------- 319 | 320 | You can change the default migration template by providing the path to a file 321 | in the `phpmig.migrations_template_path` config value. If the template has a 322 | `.php` extension it is included and parsed as PHP, and the `$className` variable 323 | is replaced: 324 | 325 | ```php 326 | 327 | 328 | use Phpmig\Migration\Migration; 329 | 330 | class extends Migration 331 | { 332 | $someValue = container['value'] ?>; 333 | 334 | /** 335 | * Do the migration 336 | */ 337 | public function up() 338 | { 339 | $container = $this->getContainer(); 340 | } 341 | 342 | /** 343 | * Undo the migration 344 | */ 345 | public function down() 346 | { 347 | $container = $this->getContainer(); 348 | } 349 | } 350 | ``` 351 | 352 | If it uses any other extension (e.g., `.stub` or `.tmpl`) it's parsed using the 353 | `sprintf` function, so the class name should be set to `%s` to ensure it gets 354 | replaced: 355 | 356 | ```php 357 | getContainer(); 369 | } 370 | 371 | /** 372 | * Undo the migration 373 | */ 374 | public function down() 375 | { 376 | $container = $this->getContainer(); 377 | } 378 | } 379 | ``` 380 | 381 | Module Migrations 382 | --------------------- 383 | 384 | If you have an application that consists of different modules and you want to be able to separate the migration, Phpmig has a built-in way to achieve this. 385 | 386 | ```php 387 | array( 393 | 'adapter' => new Adapter\File\Flat('modules/migrationLogs/cms_migrations.log'), 394 | 'migrations_path' => 'migrations/cms', 395 | 'migrations_template_path' => 'PhpmigCmsTemplate.php' 396 | ), 397 | 'blog' => array( 398 | 'adapter' => new Adapter\File\Flat('modules/migrationLogs/blog_migrations.log'), 399 | 'migrations_path' => 'migrations/blog', 400 | 'migrations_template_path' => 'PhpmigBlogTemplate.php', 401 | ) 402 | ); 403 | }; 404 | ``` 405 | 406 | this way each set has their own migration log and the ability to migrate changes independently of each other. 407 | 408 | to run the set migration you just use the command below: 409 | 410 | ```bash 411 | $ phpmig up -s -- 412 | ``` 413 | 414 | For example, if a change was made to the cms migration, you'll type in this command: 415 | 416 | ```bash 417 | $ phpmig up -s cms --2 418 | ``` 419 | 420 | and the migration tool will run the migration setup for cms. 421 | 422 | to downgrade a migration would be: 423 | 424 | ```bash 425 | $ phpmig down -s -- 426 | ``` 427 | 428 | Multi path migrations 429 | --------------------- 430 | 431 | By default you have to provide the path to migrations directory, but you can 432 | organize your migrations script however you like and have several migrations 433 | directory. To do this you can provide an array of migration file paths to the 434 | container : 435 | 436 | ```php 437 | up(); 511 | ``` 512 | 513 | Todo 514 | ---- 515 | 516 | * Some sort of migration manager, that will take some of the logic out of the 517 | commands for calculating which migrations have been run, which need running 518 | etc 519 | * Adapters for Zend\_Db and/or Zend\_Db\_Table and others? 520 | * Redo and rollback commands 521 | * Tests! 522 | * Configuration? 523 | * Someway of protecting against class definition clashes with regard to the 524 | symfony dependencies and the user supplied bootstrap? 525 | 526 | Contributing 527 | ------------ 528 | 529 | Feel free to fork and send me pull requests, I try and keep the tool really 530 | basic, if you want to start adding tons of features to phpmig, I'd recommend 531 | taking a look at [robmorgan/phinx](https://github.com/robmorgan/phinx). 532 | 533 | Inspiration 534 | ----------- 535 | 536 | I basically started copying [ActiveRecord::Migrations][activerecordmigrations] 537 | in terms of the migration features, the bootstrapping was my own idea, the 538 | layout of the code was inspired by [Symfony][symfony] and [Behat][behat] 539 | 540 | Copyright 541 | --------- 542 | 543 | [Pimple][pimple] is copyright Fabien Potencier. Everything I haven't copied from 544 | anyone else is Copyright (c) 2011 Dave Marshall. See LICENCE for further details 545 | 546 | 547 | [pimple]:https://github.com/fabpot/Pimple 548 | [doctrinemigrations]:https://github.com/doctrine/migrations 549 | [doctrine]:https://github.com/doctrine 550 | [behat]:http://behat.org/ 551 | [symfony]:http://symfony.com/ 552 | [activerecordmigrations]:http://api.rubyonrails.org/classes/ActiveRecord/Migration.html 553 | [doctrineschemamanager]:http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/schema-manager.html 554 | -------------------------------------------------------------------------------- /bin/phpmig: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | if (!defined('PHPMIG_VERSION')) { 14 | define('PHPMIG_VERSION', 'dev'); 15 | } 16 | 17 | if (is_file(__DIR__ . '/../vendor/autoload.php')) { 18 | require_once __DIR__ . '/../vendor/autoload.php'; 19 | } else if (is_file(__DIR__ . '/../../../autoload.php')) { 20 | require_once __DIR__ . '/../../../autoload.php'; 21 | } else if (is_file(__DIR__ . '/../autoload.php')) { 22 | require_once __DIR__ . '/../autoload.php'; 23 | } else if (is_dir(__DIR__ . '/../src/Phpmig')) { 24 | require_once __DIR__ . "/../src/Phpmig/autoload.php.dist"; 25 | } else { 26 | require_once "Phpmig/autoload.php.dist"; 27 | } 28 | 29 | $app = new Phpmig\Console\PhpmigApplication(PHPMIG_VERSION); 30 | $app->run(); 31 | -------------------------------------------------------------------------------- /bin/phpmig.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem This script will do the following: 4 | rem - check for PHP_COMMAND env, if found, use it. 5 | rem - if not found detect php, if found use it, otherwise err and terminate 6 | 7 | if "%OS%"=="Windows_NT" @setlocal 8 | 9 | rem %~dp0 is expanded pathname of the current script under NT 10 | set DEFAULT_PHPMIG_HOME=%~dp0.. 11 | 12 | goto init 13 | goto cleanup 14 | 15 | :init 16 | 17 | if "%PHPMIG_HOME%" == "" set PHPMIG_HOME=%DEFAULT_PHPMIG_HOME% 18 | set DEFAULT_PHPMIG_HOME= 19 | 20 | if "%PHP_COMMAND%" == "" goto no_phpcommand 21 | 22 | goto run 23 | goto cleanup 24 | 25 | :run 26 | "%PHP_COMMAND%" -d html_errors=off -qC "%PHPMIG_HOME%\bin\phpmig" %* 27 | goto cleanup 28 | 29 | :no_phpcommand 30 | rem PHP_COMMAND environment variable not found, assuming php.exe is on path. 31 | set PHP_COMMAND=php.exe 32 | goto init 33 | 34 | :err_home 35 | echo ERROR: Environment var PHPMIG_HOME not set. Please point this 36 | echo variable to your local phpmig installation! 37 | goto cleanup 38 | 39 | :cleanup 40 | if "%OS%"=="Windows_NT" @endlocal 41 | rem pause 42 | -------------------------------------------------------------------------------- /bin/phpmig.stub.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | tests/ 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * For the full copyright and license information, please view the LICENSE 16 | * file that was distributed with this source code. 17 | */ 18 | 19 | /** 20 | * Adapter interface 21 | * 22 | * @author Dave Marshall ci = &get_instance(); 31 | $this->ci->load->dbforge(); 32 | $this->tableName = $tableName; 33 | $this->connection = $this->ci->db; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function fetchAll() 40 | { 41 | $rows = array(); 42 | 43 | $query = $this->connection->get($this->tableName); 44 | 45 | foreach ($query->result() as $row) { 46 | $rows[] = $row->version; 47 | } 48 | 49 | return $rows; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function up(Migration $migration) 56 | { 57 | $this->connection->insert( 58 | $this->tableName, 59 | array( 60 | 'version' => $migration->getVersion() 61 | ) 62 | ); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function down(Migration $migration) 71 | { 72 | $this->connection->where('version', $migration->getVersion()); 73 | $this->connection->delete($this->tableName); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function hasSchema() 82 | { 83 | return $this->connection->table_exists($this->tableName); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function createSchema() 90 | { 91 | $fields = array( 92 | "`version` bigint(20) unsigned NOT NULL" 93 | ); 94 | 95 | $this->ci->dbforge->add_field($fields); 96 | $this->ci->dbforge->create_table($this->tableName); 97 | 98 | return $this; 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/Doctrine/DBAL.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * For the full copyright and license information, please view the LICENSE 19 | * file that was distributed with this source code. 20 | */ 21 | 22 | 23 | /** 24 | * Phpmig adapter for doctrine dbal connection 25 | * 26 | * @author Dave Marshall 27 | */ 28 | class DBAL implements AdapterInterface 29 | { 30 | /** 31 | * @var \Doctrine\DBAL\Connection 32 | */ 33 | protected $connection; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $tableName; 39 | 40 | public function __construct(Connection $connection, string $tableName) 41 | { 42 | $this->connection = $connection; 43 | $this->tableName = $tableName; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function fetchAll() 50 | { 51 | $tableName = $this->connection->quoteIdentifier($this->tableName); 52 | $sql = "SELECT version FROM $tableName ORDER BY version ASC"; 53 | $all = $this->connection->fetchAll($sql); 54 | 55 | return array_map(function($v) {return $v['version'];}, $all); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function up(Migration $migration) 62 | { 63 | $this->connection->insert($this->tableName, array( 64 | 'version' => $migration->getVersion(), 65 | )); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function down(Migration $migration) 74 | { 75 | $this->connection->delete($this->tableName, array( 76 | 'version' => $migration->getVersion(), 77 | )); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function hasSchema() 86 | { 87 | $sm = $this->connection->getSchemaManager(); 88 | $tables = $sm->listTables(); 89 | foreach($tables as $table) { 90 | if ($table->getName() == $this->tableName) { 91 | return true; 92 | } 93 | } 94 | 95 | return false; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function createSchema() 102 | { 103 | $schema = new \Doctrine\DBAL\Schema\Schema(); 104 | $table = $schema->createTable($this->tableName); 105 | $table->addColumn("version", "string", array("length" => 255)); 106 | $queries = $schema->toSql($this->connection->getDatabasePlatform()); 107 | foreach($queries as $sql) { 108 | $this->connection->query($sql); 109 | } 110 | 111 | return $this; 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/File/Flat.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * Flat file adapter 22 | * 23 | * @author Dave Marshall filename = $filename; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function fetchAll() 41 | { 42 | $versions = file($this->filename, FILE_IGNORE_NEW_LINES); 43 | sort($versions); 44 | return $versions; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function up(Migration $migration) 51 | { 52 | $versions = $this->fetchAll(); 53 | if (in_array($migration->getVersion(), $versions)) { 54 | return $this; 55 | } 56 | 57 | $versions[] = $migration->getVersion(); 58 | $this->write($versions); 59 | return $this; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function down(Migration $migration) 66 | { 67 | $versions = $this->fetchAll(); 68 | if (!in_array($migration->getVersion(), $versions)) { 69 | return $this; 70 | } 71 | 72 | unset($versions[array_search($migration->getVersion(), $versions)]); 73 | $this->write($versions); 74 | return $this; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function hasSchema() 81 | { 82 | return file_exists($this->filename); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function createSchema() 89 | { 90 | if (!is_writable(dirname($this->filename))) { 91 | throw new \InvalidArgumentException(sprintf('The file "%s" is not writeable', $this->filename)); 92 | } 93 | 94 | if (false === touch($this->filename)) { 95 | throw new \InvalidArgumentException(sprintf('The file "%s" could not be written to', $this->filename)); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Write to file 103 | * 104 | * @param array $versions 105 | */ 106 | protected function write(array $versions) 107 | { 108 | if (false === file_put_contents($this->filename, implode("\n", $versions))) { 109 | throw new \RuntimeException(sprintf('The file "%s" could not be written to', $this->filename)); 110 | } 111 | } 112 | } 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/Illuminate/Database.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter->connection($connectionName); 31 | $this->tableName = $tableName; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function fetchAll() 38 | { 39 | $fetchMode = (method_exists($this->adapter, 'getFetchMode')) ? 40 | $this->adapter->getFetchMode() : PDO::FETCH_OBJ; 41 | 42 | $all = $this->adapter 43 | ->table($this->tableName) 44 | ->orderBy('version') 45 | ->get(); 46 | 47 | if(!is_array($all)) { 48 | $all = $all->toArray(); 49 | } 50 | 51 | return array_map(function($v) use($fetchMode) { 52 | 53 | switch ($fetchMode) { 54 | 55 | case PDO::FETCH_OBJ: 56 | return $v->version; 57 | 58 | case PDO::FETCH_ASSOC: 59 | return $v['version']; 60 | 61 | default: 62 | throw new RuntimeException("The PDO::FETCH_* constant {$fetchMode} is not supported"); 63 | } 64 | }, $all); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function up(Migration $migration) 71 | { 72 | $this->adapter 73 | ->table($this->tableName) 74 | ->insert(array( 75 | 'version' => $migration->getVersion() 76 | )); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function down(Migration $migration) 85 | { 86 | $this->adapter 87 | ->table($this->tableName) 88 | ->where('version', $migration->getVersion()) 89 | ->delete(); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function hasSchema() 98 | { 99 | return $this->adapter->getSchemaBuilder()->hasTable($this->tableName); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function createSchema() 106 | { 107 | /* @var \Illuminate\Database\Schema\Blueprint $table */ 108 | $this->adapter->getSchemaBuilder()->create($this->tableName, function ($table) { 109 | $table->string('version'); 110 | }); 111 | 112 | return $this; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/Mongo.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 26 | $this->tableName = $tableName; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function fetchAll() 33 | { 34 | $cursor = $this->connection->selectCollection($this->tableName)->find(); 35 | $versions = []; 36 | foreach($cursor as $version) { 37 | $versions[] = $version['version']; 38 | } 39 | 40 | return $versions; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function up(Migration $migration) 47 | { 48 | $this->connection->selectCollection($this->tableName) 49 | ->insert(['version' => $migration->getVersion()]); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function down(Migration $migration) 58 | { 59 | $this->connection->selectCollection($this->tableName) 60 | ->remove(['version' => $migration->getVersion()]); 61 | 62 | return $this; 63 | } 64 | 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function hasSchema() 70 | { 71 | return !empty(array_filter( 72 | $this->connection->getCollectionNames(), 73 | function($collection) { 74 | return $collection === $this->tableName; 75 | })); 76 | } 77 | 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function createSchema() 83 | { 84 | $this->connection->selectCollection($this->tableName) 85 | ->ensureIndex('version', ['unique' => 1]); 86 | 87 | return $this; 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/MongoDb.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 27 | $this->tableName = $tableName; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function fetchAll() 34 | { 35 | $cursor = $this->connection->selectCollection($this->tableName)->find( 36 | [], 37 | ['$project' => ['version' => 1]] 38 | ); 39 | 40 | return array_map( 41 | static function ($document) { 42 | return $document['version']; 43 | }, 44 | $cursor->toArray() 45 | ); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function up(Migration $migration) 52 | { 53 | $this->connection->selectCollection($this->tableName) 54 | ->insertOne(['version' => $migration->getVersion()]); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function down(Migration $migration) 63 | { 64 | $this->connection->selectCollection($this->tableName) 65 | ->deleteOne(['version' => $migration->getVersion()]); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function hasSchema() 74 | { 75 | foreach ($this->connection->listCollections() as $collection) { 76 | if ($collection->getName() === $this->tableName) { 77 | return true; 78 | } 79 | } 80 | 81 | return false; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function createSchema() 88 | { 89 | $this->connection->selectCollection($this->tableName) 90 | ->createIndex(['version' => 1], ['unique' => true]); 91 | 92 | return $this; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/PDO/Sql.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 39 | $this->tableName = $tableName; 40 | $this->pdoDriverName = $connection->getAttribute(\PDO::ATTR_DRIVER_NAME); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function fetchAll() 47 | { 48 | $sql = $this->getQuery('fetchAll'); 49 | 50 | return $this->connection->query($sql, PDO::FETCH_COLUMN, 0)->fetchAll(); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function up(Migration $migration) 57 | { 58 | $sql = $this->getQuery('up'); 59 | 60 | $this->connection->prepare($sql) 61 | ->execute(array(':version' => $migration->getVersion())); 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function down(Migration $migration) 70 | { 71 | $sql = $this->getQuery('down'); 72 | 73 | $this->connection->prepare($sql) 74 | ->execute(array(':version' => $migration->getVersion())); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function hasSchema() 83 | { 84 | $sql = $this->getQuery('hasSchema'); 85 | 86 | $tables = $this->connection->query($sql); 87 | 88 | while($table = $tables->fetchColumn()) { 89 | if ($table == $this->tableName) { 90 | return true; 91 | } 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function createSchema() 101 | { 102 | $sql = $this->getQuery('createSchema'); 103 | 104 | $this->connection->exec($sql); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Get the appropriate query for the PDO driver 111 | * 112 | * At present, only queries for sqlite, mysql, & pgsql are specified; if a 113 | * different PDO driver is used, the mysql/pgsql queries will be returned, 114 | * which may or may not work for the given database. 115 | * 116 | * @param string $type 117 | * The type of the query to retrieve 118 | * 119 | * @return string 120 | */ 121 | protected function getQuery(string $type): string 122 | { 123 | switch($this->pdoDriverName) 124 | { 125 | case 'dblib': 126 | case 'sqlsrv': 127 | $queries = [ 128 | 'fetchAll' => "SELECT version FROM {$this->tableName} ORDER BY version ASC", 129 | 'up' => "INSERT INTO {$this->tableName} VALUES (:version)", 130 | 'down' => "DELETE FROM {$this->tableName} WHERE version = :version", 131 | 'hasSchema' => "Select Table_name as 'Table name' 132 | From Information_schema.Tables 133 | Where Table_type = 'BASE TABLE' and Objectproperty 134 | (Object_id(Table_name), 'IsMsShipped') = 0", 135 | 'createSchema' => "CREATE table {$this->tableName} (version varchar(255) NOT NULL)", 136 | ]; 137 | break; 138 | 139 | case 'sqlite': 140 | $queries = [ 141 | 'fetchAll' => "SELECT `version` FROM {$this->quotedTableName()} ORDER BY `version` ASC", 142 | 'up' => "INSERT INTO {$this->quotedTableName()} VALUES (:version);", 143 | 'down' => "DELETE FROM {$this->quotedTableName()} WHERE version = :version", 144 | 'hasSchema' => "SELECT `name` FROM `sqlite_master` WHERE `type`='table';", 145 | 'createSchema' => "CREATE table {$this->quotedTableName()} (`version` NOT NULL);", 146 | ]; 147 | break; 148 | 149 | case 'pgsql': 150 | $queries = [ 151 | 'fetchAll' => "SELECT \"version\" FROM \"{$this->tableName}\" ORDER BY \"version\" ASC", 152 | 'up' => "INSERT INTO \"{$this->tableName}\" VALUES (:version)", 153 | 'down' => "DELETE FROM \"{$this->tableName}\" WHERE \"version\" = :version", 154 | 'hasSchema' => "SELECT \"tablename\" FROM \"pg_tables\"", 155 | 'createSchema' => "CREATE TABLE \"{$this->tableName}\" (\"version\" VARCHAR(255) NOT NULL)", 156 | ]; 157 | break; 158 | 159 | case 'mysql': 160 | default: 161 | $queries = [ 162 | 'fetchAll' => "SELECT `version` FROM {$this->quotedTableName()} ORDER BY `version` ASC", 163 | 'up' => "INSERT into {$this->quotedTableName()} set version = :version", 164 | 'down' => "DELETE from {$this->quotedTableName()} where version = :version", 165 | 'hasSchema' => "SHOW TABLES;", 166 | 'createSchema' => "CREATE TABLE {$this->quotedTableName()} (`version` VARCHAR(255) NOT NULL);", 167 | ]; 168 | break; 169 | } 170 | 171 | if(!array_key_exists($type, $queries)) 172 | throw new \InvalidArgumentException("Query type not found: '{$type}'"); 173 | 174 | return $queries[$type]; 175 | } 176 | 177 | private function quotedTableName(): string 178 | { 179 | return "`{$this->tableName}`"; 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/PDO/SqlOci.php: -------------------------------------------------------------------------------- 1 | connection->getAttribute(PDO::ATTR_DRIVER_NAME); 27 | if (!in_array( $driver, ['oci', 'oci8'])) { 28 | throw new \Exception('Please install OCI drivers for PDO!'); 29 | } 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function fetchAll() 36 | { 37 | $sql = 'SELECT "version" FROM "' . $this->tableName . '" ORDER BY "version" ASC'; 38 | 39 | return $this->connection->query( $sql, PDO::FETCH_COLUMN, 0 )->fetchAll(); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function up(Migration $migration) 46 | { 47 | $sql = 'INSERT INTO "' . $this->tableName . '" ("version") VALUES (:version)'; 48 | $this->connection->prepare( $sql ) 49 | ->execute( array( 50 | ':version' => $migration->getVersion() 51 | ) ); 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function down(Migration $migration) 60 | { 61 | $sql = 'DELETE from "' . $this->tableName . '" where "version" = :version'; 62 | $this->connection->prepare( $sql ) 63 | ->execute( array( ':version' => $migration->getVersion() ) ); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function hasSchema() 72 | { 73 | $sql = 'SELECT count(*) FROM user_tables WHERE table_name = :tableName'; 74 | $sth = $this->connection->prepare( $sql ); 75 | $sth->execute( array( 76 | ':tableName' => $this->tableName 77 | ) ); 78 | 79 | return !($sth->fetchColumn() == 0); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function createSchema() 86 | { 87 | $sql = 'CREATE table "' . $this->tableName . '" ("version" VARCHAR2(4000) NOT NULL, "migrate_date" TIMESTAMP DEFAULT CURRENT_TIMESTAMP)'; 88 | $this->connection->exec( $sql ); 89 | 90 | return $this; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/PDO/SqlPgsql.php: -------------------------------------------------------------------------------- 1 | connection->getAttribute(PDO::ATTR_DRIVER_NAME); 26 | $this->quote = in_array($driver, ['mysql', 'pgsql']) ? '"' : '`'; 27 | $this->schemaName = $schemaName; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function fetchAll() 34 | { 35 | $sql = "SELECT {$this->quote}version{$this->quote} FROM {$this->quotedTableName()} ORDER BY {$this->quote}version{$this->quote} ASC"; 36 | return $this->connection->query($sql, PDO::FETCH_COLUMN, 0)->fetchAll(); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function up(Migration $migration) 43 | { 44 | $sql = "INSERT into {$this->quotedTableName()} (version) VALUES (:version);"; 45 | $this->connection->prepare($sql) 46 | ->execute(array(':version' => $migration->getVersion())); 47 | return $this; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function down(Migration $migration) 54 | { 55 | $sql = "DELETE from {$this->quotedTableName()} where version = :version"; 56 | $this->connection->prepare($sql) 57 | ->execute(array(':version' => $migration->getVersion())); 58 | return $this; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function hasSchema() 65 | { 66 | $tables = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE table_schema = '{$this->schemaName}';"); 67 | while ($table = $tables->fetchColumn()) { 68 | if ($table == $this->tableName) { 69 | return true; 70 | } 71 | } 72 | return false; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function createSchema() 79 | { 80 | $sql = sprintf("SELECT COUNT(*) FROM {$this->quote}information_schema{$this->quote}.{$this->quote}schemata{$this->quote} WHERE schema_name = '%s';", 81 | $this->schemaName); 82 | $res = $this->connection->query($sql); 83 | 84 | if (!$res || !$res->fetchColumn()) { 85 | $sql = sprintf("CREATE SCHEMA %s;", $this->schemaName); 86 | if (FALSE === $this->connection->exec($sql)) { 87 | $e = $this->connection->errorInfo(); 88 | } 89 | } 90 | 91 | $sql = "CREATE table {$this->quotedTableName()} (version %s NOT NULL, {$this->quote}migrate_date{$this->quote} timestamp(6) WITH TIME ZONE DEFAULT now())"; 92 | $driver = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME); 93 | $sql = sprintf($sql, in_array($driver, array('mysql', 'pgsql')) ? 'VARCHAR(255)' : ''); 94 | 95 | if (FALSE === $this->connection->exec($sql)) { 96 | $e = $this->connection->errorInfo(); 97 | } 98 | return $this; 99 | } 100 | 101 | private function quotedTableName() 102 | { 103 | $sql = "{$this->quote}{$this->schemaName}{$this->quote}.{$this->quote}{$this->tableName}{$this->quote}"; 104 | return $sql; 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/Zend/Db.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | 21 | /** 22 | * Phpmig adapter for Zend_Db 23 | * 24 | * @author Wojtek Gancarczyk 25 | */ 26 | class Db implements AdapterInterface 27 | { 28 | const MSSQL_CREATE_STATEMENT = 'CREATE TABLE %s ( version VARCHAR(255) NOT NULL );'; 29 | const MYSQL_CREATE_STATEMENT = 'CREATE TABLE `%s` ( version VARCHAR(255) UNSIGNED NOT NULL );'; 30 | const PGSQL_CREATE_STATEMENT = 'CREATE TABLE %s ( version VARCHAR(255) NOT NULL );'; 31 | const SQLITE_CREATE_STATEMENT = 'CREATE TABLE %s ( version VARCHAR);'; 32 | 33 | /** 34 | * @var string 35 | */ 36 | protected $tableName; 37 | 38 | /** 39 | * @var string 40 | */ 41 | protected $createStatement; 42 | 43 | /** 44 | * @var \Zend_Db_Adapter_Abstract 45 | */ 46 | protected $adapter; 47 | 48 | public function __construct(\Zend_Db_Adapter_Abstract $adapter, \Zend_Config $configuration) 49 | { 50 | $this->adapter = $adapter; 51 | $this->tableName = $configuration->phpmig->tableName; 52 | $this->createStatement = $configuration->phpmig->createStatement; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function fetchAll() 59 | { 60 | $select = $this->adapter->select(); 61 | $select->from($this->tableName, 'version'); 62 | $select->order('version ASC'); 63 | $all = $this->adapter->fetchAll($select); 64 | 65 | return array_map(static function($v) {return $v['version'];}, $all); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function up(Migration $migration) 72 | { 73 | $this->adapter->insert($this->tableName, [ 74 | 'version' => $migration->getVersion(), 75 | ]); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Down 82 | * 83 | * @param Migration $migration 84 | * @return AdapterInterface 85 | */ 86 | public function down(Migration $migration) 87 | { 88 | $this->adapter->delete($this->tableName, 89 | $this->adapter->quoteInto('version = ?', $migration->getVersion()) 90 | ); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function hasSchema() 99 | { 100 | try { 101 | $schema = $this->adapter->describeTable($this->tableName); 102 | } catch (\Zend_Db_Statement_Exception $exception) { 103 | return false; 104 | } catch (\PDOException $exception) { 105 | return false; 106 | } 107 | 108 | return is_array($schema) && !empty($schema); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function createSchema() 115 | { 116 | $sql = $this->createStatement; 117 | if ($sql === null) { 118 | switch(get_class($this->adapter)) { 119 | case 'Zend_Db_Adapter_Pdo_Mssql': 120 | $createStatement = static::MSSQL_CREATE_STATEMENT; 121 | break; 122 | case 'Zend_Db_Adapter_Pdo_Mysql': 123 | case 'Zend_Db_Adapter_Mysqli': 124 | $createStatement = static::MYSQL_CREATE_STATEMENT; 125 | break; 126 | case 'Zend_Db_Adapter_Pdo_Pgsql': 127 | $createStatement = static::PGSQL_CREATE_STATEMENT; 128 | break; 129 | case 'Zend_Db_Adapter_Pdo_Sqlite': 130 | $createStatement = static::SQLITE_CREATE_STATEMENT; 131 | break; 132 | default: 133 | throw new \InvalidArgumentException('Please provide a valid SQL statement for your database system in the config file as phpmig.createStatement'); 134 | break; 135 | } 136 | $sql = sprintf($createStatement, $this->tableName); 137 | } 138 | 139 | try { 140 | $this->adapter->query($sql); 141 | } catch (\Zend_Db_Statement_Exception $exception) { 142 | throw new \InvalidArgumentException('Please provide a valid SQL statement for your database system in the config file as phpmig.createStatement'); 143 | } 144 | 145 | return $this; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Phpmig/Adapter/Zend/DbAdapter.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 43 | $this->tableName = $tableName; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function fetchAll() 50 | { 51 | $result = $this->tableGateway()->select(function (Select $select) { 52 | $select->order('version ASC'); 53 | })->toArray(); 54 | 55 | // imitate fetchCol 56 | return array_map(static function ($item) { 57 | return $item['version']; 58 | }, $result); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function up(Migration $migration) 65 | { 66 | $this->tableGateway()->insert(['version' => $migration->getVersion()]); 67 | return $this; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function down(Migration $migration) 74 | { 75 | $this->tableGateway()->delete(['version' => $migration->getVersion()]); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function hasSchema() 84 | { 85 | try { 86 | $metadata = new Metadata($this->adapter); 87 | $metadata->getTable($this->tableName); 88 | 89 | return true; 90 | } catch (\Exception $exception) { 91 | return false; 92 | } 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function createSchema() 99 | { 100 | $ddl = new CreateTable($this->tableName); 101 | $ddl->addColumn(new Varchar('version', 255)); 102 | 103 | $sql = new Sql($this->adapter); 104 | 105 | $this->adapter->query( 106 | $sql->buildSqlString($ddl), 107 | Adapter::QUERY_MODE_EXECUTE 108 | ); 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @return TableGateway 115 | */ 116 | private function tableGateway() 117 | { 118 | if (!$this->tableGateway) { 119 | $this->tableGateway = new TableGateway($this->tableName, $this->adapter); 120 | } 121 | 122 | return $this->tableGateway; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Phpmig/Api/PhpmigApplication.php: -------------------------------------------------------------------------------- 1 | 17 | * $container = include_once "/full/path/to/phpmig.php"; 18 | * $output = new Symfony\Component\Console\Output\OutputInterface\BufferedOutput; 19 | * $phpmig = new Phpmig\Api\PhpmigApplication($container, $output); 20 | * $phpmig->up(); // upgrade to latest version 21 | * echo $output->output(); // fetch output 22 | * 23 | * 24 | * @author Cody Phillips 25 | */ 26 | class PhpmigApplication 27 | { 28 | protected $container; 29 | protected $output; 30 | protected $migrations; 31 | protected $adapter; 32 | 33 | public function __construct(\ArrayAccess $container, OutputInterface $output) 34 | { 35 | $this->container = $container; 36 | $this->output = $output; 37 | if (!isset($this->container['phpmig.migrator'])) 38 | $this->container['phpmig.migrator'] = new Migration\Migrator($container['phpmig.adapter'], $this->container, $this->output); 39 | 40 | $migrations = array(); 41 | if (isset($this->container['phpmig.migrations'])) { 42 | $migrations = $this->container['phpmig.migrations']; 43 | foreach ($migrations as &$migration) { 44 | $migration = realpath($migration); 45 | } 46 | unset($migration); 47 | } 48 | if (isset($this->container['phpmig.migrations_path'])) { 49 | $migrationsPath = realpath($this->container['phpmig.migrations_path']); 50 | $migrations = array_merge($migrations, glob($migrationsPath . DIRECTORY_SEPARATOR . '*.php')); 51 | } 52 | 53 | $this->migrations = array_unique($migrations); 54 | $this->adapter = $container['phpmig.adapter']; 55 | } 56 | 57 | /** 58 | * Migrate up 59 | * 60 | * @param string $version The version to migrate up to 61 | */ 62 | public function up($version = null) 63 | { 64 | $adapter = ! empty($this->container['phpmig.adapter']) ? $this->container['phpmig.adapter'] : null; 65 | 66 | if ($adapter == null) { 67 | 68 | throw new RuntimeException("The container must contain a phpmig.adapter key!"); 69 | } 70 | 71 | if (!$adapter->hasSchema()) { 72 | 73 | $this->container['phpmig.adapter']->createSchema(); 74 | } 75 | 76 | foreach ($this->getMigrations($this->getVersion(), $version) as $migration) { 77 | $this->container['phpmig.migrator']->up($migration); 78 | } 79 | } 80 | 81 | /** 82 | * Migrate down 83 | * 84 | * @param string $version The version to migrate down to 85 | */ 86 | public function down($version = 0) 87 | { 88 | if ($version === null || $version < 0) 89 | throw new \InvalidArgumentException("Invalid version given, expected >= 0."); 90 | 91 | foreach ($this->getMigrations($this->getVersion(), $version) as $migration) { 92 | $this->container['phpmig.migrator']->down($migration); 93 | } 94 | } 95 | 96 | /** 97 | * Load all migrations to get $from to $to 98 | * 99 | * @param string $from The from version 100 | * @param string $to The to version 101 | * @return array An array of Phpmig\Migration\Migration objects to process 102 | */ 103 | public function getMigrations($from, $to = null) 104 | { 105 | $to_run = array(); 106 | 107 | $migrations = $this->migrations; 108 | $versions = $this->adapter->fetchAll(); 109 | 110 | sort($versions); 111 | 112 | $direction = 'up'; 113 | if($to !== null ){ 114 | $direction = $to > $from ? 'up' : 'down'; 115 | } 116 | 117 | 118 | if ($direction == 'down') { 119 | rsort($migrations); 120 | 121 | foreach($migrations as $path) { 122 | preg_match('/^[0-9]+/', basename($path), $matches); 123 | if (!array_key_exists(0, $matches)) { 124 | continue; 125 | } 126 | 127 | $version = $matches[0]; 128 | 129 | if ($version > $from) { 130 | continue; 131 | } 132 | if ($version <= $to) { 133 | continue; 134 | } 135 | 136 | if (in_array($version, $versions)) { 137 | $to_run[] = $path; 138 | } 139 | } 140 | }else{ 141 | sort($migrations); 142 | foreach($migrations as $path) { 143 | preg_match('/^[0-9]+/', basename($path), $matches); 144 | if (!array_key_exists(0, $matches)) { 145 | continue; 146 | } 147 | 148 | $version = $matches[0]; 149 | 150 | if ($to !== null && ($version > $to)) { 151 | continue; 152 | } 153 | 154 | if (!in_array($version, $versions)) { 155 | $to_run[] = $path; 156 | } 157 | } 158 | 159 | } 160 | 161 | return $this->loadMigrations($to_run); 162 | } 163 | 164 | /** 165 | * Loads migrations from the given set of available migration files 166 | * 167 | * @param array $migrations An array of migration files to prepare migrations for 168 | * @return array An array of Phpmig\Migration\Migration objects 169 | */ 170 | protected function loadMigrations($migrations) 171 | { 172 | $versions = array(); 173 | $names = array(); 174 | foreach ($migrations as $path) { 175 | if (!preg_match('/^[0-9]+/', basename($path), $matches)) { 176 | throw new \InvalidArgumentException(sprintf('The file "%s" does not have a valid migration filename', $path)); 177 | } 178 | 179 | $version = $matches[0]; 180 | 181 | if (isset($versions[$version])) { 182 | throw new \InvalidArgumentException(sprintf('Duplicate migration, "%s" has the same version as "%s"', $path, $versions[$version]->getName())); 183 | } 184 | 185 | $migrationName = preg_replace('/^[0-9]+_/', '', basename($path)); 186 | if (false !== strpos($migrationName, '.')) { 187 | $migrationName = substr($migrationName, 0, strpos($migrationName, '.')); 188 | } 189 | $class = $this->migrationToClassName($migrationName); 190 | 191 | if (isset($names[$class])) { 192 | throw new \InvalidArgumentException(sprintf( 193 | 'Migration "%s" has the same name as "%s"', 194 | $path, 195 | $names[$class] 196 | )); 197 | } 198 | $names[$class] = $path; 199 | 200 | require_once $path; 201 | if (!class_exists($class)) { 202 | throw new \InvalidArgumentException(sprintf( 203 | 'Could not find class "%s" in file "%s"', 204 | $class, 205 | $path 206 | )); 207 | } 208 | 209 | $migration = new $class($version); 210 | 211 | if (!($migration instanceof Migration\Migration)) { 212 | throw new \InvalidArgumentException(sprintf( 213 | 'The class "%s" in file "%s" must extend \Phpmig\Migration\Migration', 214 | $class, 215 | $path 216 | )); 217 | } 218 | 219 | $migration->setOutput($this->output); // inject output 220 | 221 | $versions[$version] = $migration; 222 | } 223 | 224 | return $versions; 225 | } 226 | 227 | /** 228 | * Transform create_table_user to CreateTableUser 229 | * 230 | * @param string $migrationName The migration name 231 | * @return string The CamelCase migration name 232 | */ 233 | protected function migrationToClassName($migrationName) 234 | { 235 | $class = str_replace('_', ' ', $migrationName); 236 | $class = ucwords($class); 237 | return str_replace(' ', '', $class); 238 | } 239 | 240 | /** 241 | * Returns the current version 242 | * 243 | * @return string The current installed version 244 | */ 245 | public function getVersion() 246 | { 247 | $versions = $this->container['phpmig.adapter']->fetchAll(); 248 | sort($versions); 249 | 250 | if (!empty($versions)) { 251 | return end($versions); 252 | } 253 | return 0; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | 21 | * 22 | * For the full copyright and license information, please view the LICENSE 23 | * file that was distributed with this source code. 24 | */ 25 | 26 | /** 27 | * Abstract command, contains bootstrapping info 28 | * 29 | * @author Dave Marshall 30 | */ 31 | abstract class AbstractCommand extends Command 32 | { 33 | /** 34 | * @var \ArrayAccess 35 | */ 36 | protected $container = null; 37 | 38 | /** 39 | * @var AdapterInterface 40 | */ 41 | protected $adapter = null; 42 | 43 | /** 44 | * @var string 45 | */ 46 | protected $bootstrap = null; 47 | 48 | /** 49 | * @var array 50 | */ 51 | protected $migrations = array(); 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | protected function configure() 57 | { 58 | $this->addOption('--set', '-s', InputOption::VALUE_REQUIRED, 'The phpmig.sets key'); 59 | $this->addOption('--bootstrap', '-b', InputOption::VALUE_REQUIRED, 'The bootstrap file to load'); 60 | } 61 | 62 | /** 63 | * Bootstrap phpmig 64 | * 65 | * @return void 66 | */ 67 | protected function bootstrap(InputInterface $input, OutputInterface $output) 68 | { 69 | $this->setBootstrap($this->findBootstrapFile($input->getOption('bootstrap'))); 70 | 71 | $container = $this->bootstrapContainer(); 72 | $this->setContainer($container); 73 | $this->setAdapter($this->bootstrapAdapter($input)); 74 | 75 | $this->setMigrations($this->bootstrapMigrations($input, $output)); 76 | 77 | $container['phpmig.migrator'] = $this->bootstrapMigrator($output); 78 | 79 | } 80 | 81 | /** 82 | * @param string $filename 83 | * @return array|string 84 | */ 85 | protected function findBootstrapFile($filename) 86 | { 87 | if (null === $filename) { 88 | $filename = 'phpmig.php'; 89 | } 90 | 91 | $cwd = getcwd(); 92 | 93 | $locator = new FileLocator(array( 94 | $cwd . DIRECTORY_SEPARATOR . 'config', 95 | $cwd 96 | )); 97 | 98 | return $locator->locate($filename); 99 | } 100 | 101 | /** 102 | * @return \ArrayAccess The container 103 | * @throws \RuntimeException 104 | */ 105 | protected function bootstrapContainer() 106 | { 107 | $bootstrapFile = $this->getBootstrap(); 108 | 109 | $func = function () use ($bootstrapFile) { 110 | return require $bootstrapFile; 111 | }; 112 | 113 | $container = $func(); 114 | 115 | if (!($container instanceof \ArrayAccess)) { 116 | throw new \RuntimeException($bootstrapFile . " must return object of type \ArrayAccess"); 117 | } 118 | 119 | return $container; 120 | } 121 | 122 | /** 123 | * @param InputInterface $input 124 | * @return AdapterInterface 125 | * @throws \RuntimeException 126 | */ 127 | protected function bootstrapAdapter(InputInterface $input) 128 | { 129 | $container = $this->getContainer(); 130 | 131 | $validAdapter = isset($container['phpmig.adapter']); 132 | $validSets = !isset($container['phpmig.sets']) || is_array($container['phpmig.sets']); 133 | 134 | if (!$validAdapter && !$validSets) { 135 | throw new \RuntimeException( 136 | $this->getBootstrap() 137 | . 'must return container with phpmig.adapter or phpmig.sets' 138 | ); 139 | } 140 | 141 | if (isset($container['phpmig.sets'])) { 142 | $set = $input->getOption('set'); 143 | if (!isset($container['phpmig.sets'][$set]['adapter'])) { 144 | throw new \RuntimeException( 145 | $set . ' is undefined keys or adapter at phpmig.sets' 146 | ); 147 | } 148 | $adapter = $container['phpmig.sets'][$set]['adapter']; 149 | } 150 | if (isset($container['phpmig.adapter'])) { 151 | $adapter = $container['phpmig.adapter']; 152 | } 153 | 154 | if (!($adapter instanceof AdapterInterface)) { 155 | throw new \RuntimeException("phpmig.adapter or phpmig.sets must be an instance of \\Phpmig\\Adapter\\AdapterInterface"); 156 | } 157 | 158 | if (!$adapter->hasSchema()) { 159 | $adapter->createSchema(); 160 | } 161 | 162 | return $adapter; 163 | } 164 | 165 | /** 166 | * @param InputInterface $input 167 | * @param OutputInterface $output 168 | * @throws \RuntimeException 169 | * @throws \InvalidArgumentException 170 | */ 171 | protected function bootstrapMigrations(InputInterface $input, OutputInterface $output) 172 | { 173 | $container = $this->getContainer(); 174 | $set = $input->getOption('set'); 175 | 176 | if (!isset($container['phpmig.migrations']) && !isset($container['phpmig.migrations_path']) && (isset($container['phpmig.sets']) && !isset($container['phpmig.sets'][$set]['migrations_path']))) { 177 | throw new \RuntimeException($this->getBootstrap() . ' must return container with array at phpmig.migrations or migrations default path at phpmig.migrations_path or migrations default path at phpmig.sets'); 178 | } 179 | 180 | $migrations = array(); 181 | if (isset($container['phpmig.migrations'])) { 182 | if (!is_array($container['phpmig.migrations'])) { 183 | throw new \RuntimeException($this->getBootstrap() . ' phpmig.migrations must be an array.'); 184 | } 185 | 186 | $migrations = $container['phpmig.migrations']; 187 | } 188 | if (isset($container['phpmig.migrations_path'])) { 189 | if (!is_dir($container['phpmig.migrations_path'])) { 190 | throw new \RuntimeException($this->getBootstrap() . ' phpmig.migrations_path must be a directory.'); 191 | } 192 | 193 | $migrationsPath = realpath($container['phpmig.migrations_path']); 194 | $migrations = array_merge($migrations, glob($migrationsPath . DIRECTORY_SEPARATOR . '*.php')); 195 | } 196 | if (isset($container['phpmig.sets']) && isset($container['phpmig.sets'][$set]['migrations_path'])) { 197 | if (!is_dir($container['phpmig.sets'][$set]['migrations_path'])) { 198 | throw new \RuntimeException($this->getBootstrap() . " ['phpmig.sets']['" . $set . "']['migrations_path'] must be a directory."); 199 | } 200 | 201 | $migrationsPath = realpath($container['phpmig.sets'][$set]['migrations_path']); 202 | $migrations = array_merge($migrations, glob($migrationsPath . DIRECTORY_SEPARATOR . '*.php')); 203 | } 204 | $migrations = array_unique($migrations); 205 | 206 | $versions = array(); 207 | $names = array(); 208 | foreach ($migrations as $path) { 209 | if (!preg_match('/^[0-9]+/', basename($path), $matches)) { 210 | throw new \InvalidArgumentException(sprintf('The file "%s" does not have a valid migration filename', $path)); 211 | } 212 | 213 | $version = $matches[0]; 214 | if (isset($versions[$version])) { 215 | throw new \InvalidArgumentException(sprintf('Duplicate migration, "%s" has the same version as "%s"', $path, $versions[$version]->getName())); 216 | } 217 | 218 | $migrationName = preg_replace('/^[0-9]+_/', '', basename($path)); 219 | if (false !== strpos($migrationName, '.')) { 220 | $migrationName = substr($migrationName, 0, strpos($migrationName, '.')); 221 | } 222 | $class = $this->migrationToClassName($migrationName); 223 | 224 | if ($this instanceof GenerateCommand 225 | && $class == $this->migrationToClassName($input->getArgument('name'))) { 226 | throw new \InvalidArgumentException(sprintf( 227 | 'Migration Class "%s" already exists', 228 | $class 229 | )); 230 | } 231 | 232 | if (isset($names[$class])) { 233 | throw new \InvalidArgumentException(sprintf( 234 | 'Migration "%s" has the same name as "%s"', 235 | $path, 236 | $names[$class] 237 | )); 238 | } 239 | $names[$class] = $path; 240 | 241 | require_once $path; 242 | if (!class_exists($class)) { 243 | throw new \InvalidArgumentException(sprintf( 244 | 'Could not find class "%s" in file "%s"', 245 | $class, 246 | $path 247 | )); 248 | } 249 | 250 | $migration = new $class($version); 251 | 252 | if (!($migration instanceof Migration)) { 253 | throw new \InvalidArgumentException(sprintf( 254 | 'The class "%s" in file "%s" must extend \Phpmig\Migration\Migration', 255 | $class, 256 | $path 257 | )); 258 | } 259 | 260 | $migration->setOutput($output); // inject output 261 | 262 | $versions[$version] = $migration; 263 | } 264 | 265 | if (isset($container['phpmig.sets']) && isset($container['phpmig.sets'][$set]['connection'])) { 266 | $container['phpmig.connection'] = $container['phpmig.sets'][$set]['connection']; 267 | } 268 | 269 | ksort($versions); 270 | 271 | return $versions; 272 | } 273 | 274 | /** 275 | * @param OutputInterface $output 276 | * @return mixed 277 | */ 278 | protected function bootstrapMigrator(OutputInterface $output) 279 | { 280 | return new Migrator($this->getAdapter(), $this->getContainer(), $output); 281 | } 282 | 283 | /** 284 | * Set bootstrap 285 | * 286 | * @var string 287 | * @return AbstractCommand 288 | */ 289 | public function setBootstrap($bootstrap) 290 | { 291 | $this->bootstrap = $bootstrap; 292 | return $this; 293 | } 294 | 295 | /** 296 | * Get bootstrap 297 | * 298 | * @return string 299 | */ 300 | public function getBootstrap() 301 | { 302 | return $this->bootstrap; 303 | } 304 | 305 | /** 306 | * Set migrations 307 | * 308 | * @param array $migrations 309 | * @return AbstractCommand 310 | */ 311 | public function setMigrations(array $migrations) 312 | { 313 | $this->migrations = $migrations; 314 | return $this; 315 | } 316 | 317 | /** 318 | * Get migrations 319 | * 320 | * @return array 321 | */ 322 | public function getMigrations() 323 | { 324 | return $this->migrations; 325 | } 326 | 327 | /** 328 | * Set container 329 | * 330 | * @var \ArrayAccess 331 | * @return AbstractCommand 332 | */ 333 | public function setContainer(\ArrayAccess $container) 334 | { 335 | $this->container = $container; 336 | return $this; 337 | } 338 | 339 | /** 340 | * Get container 341 | * 342 | * @return \ArrayAccess 343 | */ 344 | public function getContainer() 345 | { 346 | return $this->container; 347 | } 348 | 349 | /** 350 | * Set adapter 351 | * 352 | * @param AdapterInterface $adapter 353 | * @return AbstractCommand 354 | */ 355 | public function setAdapter(AdapterInterface $adapter) 356 | { 357 | $this->adapter = $adapter; 358 | return $this; 359 | } 360 | 361 | /** 362 | * Get Adapter 363 | * 364 | * @return AdapterInterface 365 | */ 366 | public function getAdapter() 367 | { 368 | return $this->adapter; 369 | } 370 | 371 | /** 372 | * transform create_table_user to CreateTableUser 373 | * @param $migrationName 374 | * @return string 375 | */ 376 | protected function migrationToClassName( $migrationName ) 377 | { 378 | $class = str_replace('_', ' ', $migrationName); 379 | $class = ucwords($class); 380 | $class = str_replace(' ', '', $class); 381 | 382 | if (!$this->isValidClassName($class)) { 383 | throw new \InvalidArgumentException(sprintf( 384 | 'Migration class "%s" is invalid', 385 | $class 386 | )); 387 | } 388 | 389 | return $class; 390 | } 391 | 392 | /** 393 | * @param $className 394 | * @return bool 395 | * @see http://php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class 396 | */ 397 | private function isValidClassName($className) 398 | { 399 | return preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $className) === 1; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/CheckCommand.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | class CheckCommand extends AbstractCommand 18 | { 19 | protected function configure() 20 | { 21 | parent::configure(); 22 | 23 | $this->setName('check') 24 | ->setDescription('Check all migrations have been run, exit with non-zero if not') 25 | ->setHelp(<<check checks that all migrations have been run and exits with a 27 | non-zero exit code if not, useful for build or deployment scripts. 28 | 29 | phpmig check 30 | 31 | EOT 32 | ); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $this->bootstrap($input, $output); 41 | $versions = $this->getAdapter()->fetchAll(); 42 | $down = array(); 43 | foreach($this->getMigrations() as $migration) { 44 | if (!in_array($migration->getVersion(), $versions)) { 45 | $down[] = $migration; 46 | } 47 | } 48 | 49 | if (!empty($down)) { 50 | $output->writeln(""); 51 | $output->writeln(" Status Migration ID Migration Name "); 52 | $output->writeln("-----------------------------------------"); 53 | 54 | foreach ($down as $migration) { 55 | $output->writeln( 56 | sprintf( 57 | " down %14s %s", 58 | $migration->getVersion(), 59 | $migration->getName() 60 | ) 61 | ); 62 | } 63 | 64 | $output->writeln(""); 65 | 66 | return 1; 67 | } 68 | 69 | return 0; 70 | } 71 | } 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/DownCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * Down command 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class DownCommand extends AbstractCommand 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | parent::configure(); 34 | 35 | $this->setName('down') 36 | ->addArgument('version', InputArgument::REQUIRED, 'The version number for the migration') 37 | ->setDescription('Revert a specific migration') 38 | ->setHelp(<<down command reverts a specific migration 40 | 41 | phpmig down 20111018185412 42 | 43 | EOT 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $this->bootstrap($input, $output); 53 | 54 | $migrations = $this->getMigrations(); 55 | $versions = $this->getAdapter()->fetchAll(); 56 | 57 | $version = $input->getArgument('version'); 58 | 59 | if (!in_array($version, $versions)) { 60 | return 0; 61 | } 62 | 63 | if (!isset($migrations[$version])) { 64 | return 0; 65 | } 66 | 67 | $container = $this->getContainer(); 68 | $container['phpmig.migrator']->down($migrations[$version]); 69 | 70 | return 0; 71 | } 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * For the full copyright and license information, please view the LICENSE 19 | * file that was distributed with this source code. 20 | */ 21 | 22 | /** 23 | * Generate command 24 | * 25 | * @author Dave Marshall 26 | */ 27 | class GenerateCommand extends AbstractCommand 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | parent::configure(); 35 | 36 | $this->setName('generate') 37 | ->addArgument('name', InputArgument::REQUIRED, 'The name for the migration') 38 | ->addArgument('path', InputArgument::OPTIONAL, 39 | 'The directory in which to put the migration ( optional if phpmig.migrations_path is setted )') 40 | ->setDescription('Generate a new migration') 41 | ->setHelp(<<generate command creates a new migration with the name and path specified 43 | 44 | phpmig generate Dave ./migrations 45 | 46 | EOT 47 | ); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $this->bootstrap($input, $output); 56 | 57 | $path = $input->getArgument('path'); 58 | $set = $input->getOption('set'); 59 | if (null === $path) { 60 | if (true === isset($this->container['phpmig.migrations_path'])) { 61 | $path = $this->container['phpmig.migrations_path']; 62 | } 63 | // do not deep link to nested keys without first testing the parent key 64 | if (true === isset($this->container['phpmig.sets'])){ 65 | if (true === isset($this->container['phpmig.sets'][$set]['migrations_path'])) { 66 | $path = $this->container['phpmig.sets'][$set]['migrations_path']; 67 | } 68 | } 69 | } 70 | $locator = new FileLocator(array()); 71 | $path = $locator->locate($path, getcwd(), $first = true); 72 | 73 | if (!is_writable($path)) { 74 | throw new \InvalidArgumentException(sprintf( 75 | 'The directory "%s" is not writeable', 76 | $path 77 | )); 78 | } 79 | 80 | $path = realpath($path); 81 | 82 | $migrationName = $this->transMigName($input->getArgument('name')); 83 | 84 | $basename = date('YmdHis') . '_' . $migrationName . '.php'; 85 | 86 | $path = $path . DIRECTORY_SEPARATOR . $basename; 87 | 88 | if (file_exists($path)) { 89 | throw new \InvalidArgumentException(sprintf( 90 | 'The file "%s" already exists', 91 | $path 92 | )); 93 | } 94 | 95 | $className = $this->migrationToClassName($migrationName); 96 | 97 | if (isset($this->container['phpmig.migrations_template_path']) || (isset($this->container['phpmig.sets']) && isset($this->container['phpmig.sets'][$set]['migrations_template_path']))) { 98 | if (true === isset($this->container['phpmig.migrations_template_path'])) { 99 | $migrationsTemplatePath = $this->container['phpmig.migrations_template_path']; 100 | } else { 101 | $migrationsTemplatePath = $this->container['phpmig.sets'][$set]['migrations_template_path']; 102 | } 103 | 104 | if (false === file_exists($migrationsTemplatePath)) { 105 | throw new \RuntimeException(sprintf( 106 | 'The template file "%s" not found', 107 | $migrationsTemplatePath 108 | )); 109 | } 110 | 111 | if (preg_match('/\.php$/', $migrationsTemplatePath)) { 112 | ob_start(); 113 | include $migrationsTemplatePath; 114 | $contents = ob_get_clean(); 115 | } else { 116 | $contents = file_get_contents($migrationsTemplatePath); 117 | $contents = sprintf($contents, $className); 118 | } 119 | } else { 120 | $contents = <<writeln( 155 | '+f ' . 156 | '.' . str_replace(getcwd(), '', $path) 157 | ); 158 | 159 | return 0; 160 | } 161 | 162 | protected function transMigName($migrationName) 163 | { 164 | //http://php.net/manual/en/language.variables.basics.php 165 | if (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $migrationName)) { 166 | return $migrationName; 167 | } 168 | return 'mig' . $migrationName; 169 | } 170 | } 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * Init command 22 | * 23 | * @author Dave Marshall 24 | */ 25 | class InitCommand extends AbstractCommand 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('init') 33 | ->setDescription('Initialise this directory for use with phpmig') 34 | ->setHelp(<<init command creates a skeleton bootstrap file and a migrations directory 36 | 37 | phpmig init 38 | 39 | EOT 40 | ); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $cwd = getcwd(); 49 | $bootstrap = $cwd . DIRECTORY_SEPARATOR . 'phpmig.php'; 50 | $relative = 'migrations'; 51 | $migrations = $cwd . DIRECTORY_SEPARATOR . $relative; 52 | 53 | $this->initMigrationsDir($migrations, $output); 54 | $this->initBootstrap($bootstrap, $relative, $output); 55 | 56 | return 0; 57 | } 58 | 59 | /** 60 | * Create migrations dir 61 | * 62 | * @param $path 63 | * @return void 64 | */ 65 | protected function initMigrationsDir($migrations, OutputInterface $output) 66 | { 67 | if (file_exists($migrations) && is_dir($migrations)) { 68 | $output->writeln( 69 | '-- ' . 70 | str_replace(getcwd(), '.', $migrations) . ' already exists -' . 71 | ' Place your migration files in here' 72 | ); 73 | return; 74 | } 75 | 76 | if (false === mkdir($migrations)) { 77 | throw new \RuntimeException(sprintf('Could not create directory "%s"', $migrations)); 78 | } 79 | 80 | $output->writeln( 81 | '+d ' . 82 | str_replace(getcwd(), '.', $migrations) . 83 | ' Place your migration files in here' 84 | ); 85 | } 86 | 87 | /** 88 | * Create bootstrap 89 | * 90 | * @param string $bootstrap where to put bootstrap file 91 | * @param string $migrations path to migrations dir relative to bootstrap 92 | * @return void 93 | */ 94 | protected function initBootstrap($bootstrap, $migrations, OutputInterface $output) 95 | { 96 | if (file_exists($bootstrap)) { 97 | $output->writeln( 98 | '-- ' . 99 | str_replace(getcwd(), '.', $bootstrap) . ' already exists -' . 100 | ' Create services in here' 101 | ); 102 | return; 103 | } 104 | 105 | if (!is_writable(dirname($bootstrap))) { 106 | throw new \RuntimeException(sprintf('The file "%s" is not writeable', $bootstrap)); 107 | } 108 | 109 | $contents = <<writeln( 135 | '+f ' . 136 | str_replace(getcwd(), '.', $bootstrap) . 137 | ' Create services in here' 138 | ); 139 | } 140 | } 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * Migrate command 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class MigrateCommand extends AbstractCommand 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | parent::configure(); 34 | 35 | $this->setName('migrate') 36 | ->addOption('--target', '-t', InputArgument::OPTIONAL, 'The version number to migrate to') 37 | ->setDescription('Run all migrations') 38 | ->setHelp(<<migrate command runs all available migrations, optionally up to a specific version 40 | 41 | phpmig migrate 42 | phpmig migrate -t 20111018185412 43 | 44 | EOT 45 | ); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function execute(InputInterface $input, OutputInterface $output): int 52 | { 53 | $this->bootstrap($input, $output); 54 | 55 | $migrations = $this->getMigrations(); 56 | $versions = $this->getAdapter()->fetchAll(); 57 | 58 | $version = $input->getOption('target'); 59 | 60 | ksort($migrations); 61 | sort($versions); 62 | 63 | if (!empty($versions)) { 64 | // Get the last run migration number 65 | $current = end($versions); 66 | } else { 67 | $current = 0; 68 | } 69 | 70 | if (null !== $version) { 71 | if (0 != $version && !isset($migrations[$version])) { 72 | return 0; 73 | } 74 | } else { 75 | $versionNumbers = array_merge($versions, array_keys($migrations)); 76 | 77 | if (empty($versionNumbers)) { 78 | return 0; 79 | } 80 | 81 | $version = max($versionNumbers); 82 | } 83 | 84 | $direction = $version > $current ? 'up' : 'down'; 85 | 86 | if ($direction === 'down') { 87 | /** 88 | * Run downs first 89 | */ 90 | krsort($migrations); 91 | foreach($migrations as $migration) { 92 | if ($migration->getVersion() <= $version) { 93 | break; 94 | } 95 | 96 | if (in_array($migration->getVersion(), $versions)) { 97 | $container = $this->getContainer(); 98 | $container['phpmig.migrator']->down($migration); 99 | } 100 | } 101 | } 102 | 103 | ksort($migrations); 104 | foreach($migrations as $migration) { 105 | if ($migration->getVersion() > $version) { 106 | break; 107 | } 108 | 109 | if (!in_array($migration->getVersion(), $versions)) { 110 | $container = $this->getContainer(); 111 | $container['phpmig.migrator']->up($migration); 112 | } 113 | } 114 | 115 | return 0; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/RedoCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * Redo command 23 | * 24 | * @author nagodon 25 | */ 26 | class RedoCommand extends AbstractCommand 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | parent::configure(); 34 | 35 | $this->setName('redo') 36 | ->addArgument('version', InputArgument::REQUIRED, 'The version number for the migration') 37 | ->setDescription('Redo a specific migration') 38 | ->setHelp(<<redo command redo a specific migration 40 | 41 | phpmig redo 20111018185412 42 | 43 | EOT 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $this->bootstrap($input, $output); 53 | 54 | $migrations = $this->getMigrations(); 55 | $versions = $this->getAdapter()->fetchAll(); 56 | 57 | $version = $input->getArgument('version'); 58 | 59 | if (!in_array($version, $versions)) { 60 | return 0; 61 | } 62 | 63 | if (!isset($migrations[$version])) { 64 | return 0; 65 | } 66 | 67 | $container = $this->getContainer(); 68 | $container['phpmig.migrator']->down($migrations[$version]); 69 | $container['phpmig.migrator']->up($migrations[$version]); 70 | 71 | return 0; 72 | } 73 | } 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/RollbackCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * Rollback command 23 | * 24 | * @author David Neilsen 25 | */ 26 | class RollbackCommand extends AbstractCommand 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | parent::configure(); 34 | 35 | $this->setName('rollback') 36 | ->addOption('--target', '-t', InputArgument::OPTIONAL, 'The version number to rollback to') 37 | ->setDescription('Rollback last, or to a specific migration') 38 | ->setHelp(<<rollback command reverts the last migration, or optionally up to a specific version 40 | 41 | phpmig rollback 42 | phpmig rollback -t 20111018185412 43 | 44 | EOT 45 | ); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function execute(InputInterface $input, OutputInterface $output): int 52 | { 53 | $this->bootstrap($input, $output); 54 | 55 | $migrations = $this->getMigrations(); 56 | $versions = $this->getAdapter()->fetchAll(); 57 | 58 | $version = $input->getOption('target'); 59 | 60 | ksort($migrations); 61 | sort($versions); 62 | 63 | // Check we have at least 1 migration to revert 64 | if (empty($versions) || $version == end($versions)) { 65 | $output->writeln("No migrations to rollback"); 66 | return 0; 67 | } 68 | 69 | // If no target version was supplied, revert the last migration 70 | if (null === $version) { 71 | // Get the migration before the last run migration 72 | $prev = count($versions) - 2; 73 | $version = $prev >= 0 ? $versions[$prev] : 0; 74 | } else { 75 | // Get the first migration number 76 | $first = reset($versions); 77 | 78 | // If the target version is before the first migration, revert all migrations 79 | if ($version < $first) { 80 | $version = 0; 81 | } 82 | } 83 | 84 | // Check the target version exists 85 | if (0 !== $version && !isset($migrations[$version])) { 86 | $output->writeln("Target version ($version) not found"); 87 | return 0; 88 | } 89 | 90 | // Revert the migration(s) 91 | $container = $this->getContainer(); 92 | krsort($migrations); 93 | foreach($migrations as $migration) { 94 | if ($migration->getVersion() <= $version) { 95 | break; 96 | } 97 | 98 | if (in_array($migration->getVersion(), $versions)) { 99 | $container['phpmig.migrator']->down($migration); 100 | } 101 | } 102 | 103 | return 0; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/StatusCommand.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * Status command 22 | * 23 | * @author Dave Marshall 24 | */ 25 | class StatusCommand extends AbstractCommand 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | parent::configure(); 33 | 34 | $this->setName('status') 35 | ->setDescription('Show the up/down status of all migrations') 36 | ->setHelp(<<status command prints a list of all migrations, along with their current status 38 | 39 | phpmig status 40 | 41 | EOT 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function execute(InputInterface $input, OutputInterface $output): int 49 | { 50 | $this->bootstrap($input, $output); 51 | $output->writeln(""); 52 | $output->writeln(" Status Migration ID Migration Name "); 53 | $output->writeln("-----------------------------------------"); 54 | 55 | $versions = $this->getAdapter()->fetchAll(); 56 | foreach($this->getMigrations() as $migration) { 57 | 58 | if (in_array($migration->getVersion(), $versions)) { 59 | $status = " up "; 60 | unset($versions[array_search($migration->getVersion(), $versions)]); 61 | } else { 62 | $status = " down "; 63 | } 64 | 65 | $output->writeln( 66 | $status . 67 | sprintf(" %14s ", $migration->getVersion()) . 68 | " " . $migration->getName() . "" 69 | ); 70 | } 71 | 72 | foreach($versions as $missing) { 73 | $output->writeln( 74 | ' up ' . 75 | sprintf(" %14s ", $missing) . 76 | ' ** MISSING ** ' 77 | ); 78 | } 79 | 80 | // print status 81 | $output->writeln(""); 82 | return 0; 83 | } 84 | } 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/Phpmig/Console/Command/UpCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /** 22 | * Up command 23 | * 24 | * @author Dave Marshall 25 | */ 26 | class UpCommand extends AbstractCommand 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function configure() 32 | { 33 | parent::configure(); 34 | 35 | $this->setName('up') 36 | ->addArgument('version', InputArgument::REQUIRED, 'The version number for the migration') 37 | ->setDescription('Run a specific migration') 38 | ->setHelp(<<up command runs a specific migration 40 | 41 | phpmig up 20111018185121 42 | 43 | EOT 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $this->bootstrap($input, $output); 53 | 54 | $migrations = $this->getMigrations(); 55 | $versions = $this->getAdapter()->fetchAll(); 56 | 57 | $version = $input->getArgument('version'); 58 | 59 | if (in_array($version, $versions)) { 60 | return 0; 61 | } 62 | 63 | if (!isset($migrations[$version])) { 64 | return 0; 65 | } 66 | 67 | $container = $this->getContainer(); 68 | $container['phpmig.migrator']->up($migrations[$version]); 69 | 70 | return 0; 71 | } 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/Phpmig/Console/PhpmigApplication.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * The main phpmig application 22 | * 23 | * @author Dave Marshall 24 | */ 25 | class PhpmigApplication extends Application 26 | { 27 | /** 28 | * @param string $version 29 | */ 30 | public function __construct($version = 'dev') 31 | { 32 | parent::__construct('phpmig', $version); 33 | 34 | $this->addCommands(array( 35 | new Command\InitCommand(), 36 | new Command\StatusCommand(), 37 | new Command\CheckCommand(), 38 | new Command\GenerateCommand(), 39 | new Command\UpCommand(), 40 | new Command\DownCommand(), 41 | new Command\MigrateCommand(), 42 | new Command\RollbackCommand(), 43 | new Command\RedoCommand() 44 | )); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/Phpmig/Migration/Migration.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * For the full copyright and license information, please view the LICENSE 19 | * file that was distributed with this source code. 20 | */ 21 | 22 | /** 23 | * Migration 24 | * 25 | * A migration describes the changes that should be made (or unmade) 26 | * 27 | * @author Dave Marshall 28 | */ 29 | class Migration 30 | { 31 | /** 32 | * @var int 33 | */ 34 | protected $version = null; 35 | 36 | /** 37 | * @var \ArrayAccess 38 | */ 39 | protected $container = null; 40 | 41 | /** 42 | * @var OutputInterface 43 | */ 44 | protected $output = null; 45 | 46 | /** 47 | * @var InputInterface 48 | */ 49 | protected $input = null; 50 | 51 | /** 52 | * @var QuestionHelper 53 | */ 54 | protected $dialogHelper = null; 55 | 56 | /** 57 | * Constructor 58 | * 59 | * @param int $version 60 | */ 61 | final public function __construct($version) 62 | { 63 | $this->version = $version; 64 | } 65 | 66 | /** 67 | * init 68 | * 69 | * @return void 70 | */ 71 | public function init() 72 | { 73 | return; 74 | } 75 | 76 | /** 77 | * Do the migration 78 | * 79 | * @return void 80 | */ 81 | public function up() 82 | { 83 | return; 84 | } 85 | 86 | /** 87 | * Undo the migration 88 | * 89 | * @return void 90 | */ 91 | public function down() 92 | { 93 | return; 94 | } 95 | 96 | /** 97 | * Get Version 98 | * 99 | * @return int 100 | */ 101 | public function getVersion(): ?int 102 | { 103 | return $this->version; 104 | } 105 | 106 | /** 107 | * Set version 108 | * 109 | * @param int $version 110 | * @return Migration 111 | */ 112 | public function setVersion(int $version): Migration 113 | { 114 | $this->version = $version; 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get name 120 | * 121 | * @return string 122 | */ 123 | public function getName(): string 124 | { 125 | return get_class($this); 126 | } 127 | 128 | /** 129 | * Get Container 130 | * 131 | * @return \ArrayAccess 132 | */ 133 | public function getContainer(): ?\ArrayAccess 134 | { 135 | return $this->container; 136 | } 137 | 138 | /** 139 | * Set Container 140 | * 141 | * @param \ArrayAccess $container 142 | * @return Migration 143 | */ 144 | public function setContainer(\ArrayAccess $container): Migration 145 | { 146 | $this->container = $container; 147 | return $this; 148 | } 149 | 150 | /** 151 | * Get Output 152 | * 153 | * @return OutputInterface 154 | */ 155 | public function getOutput(): ?OutputInterface 156 | { 157 | return $this->output; 158 | } 159 | 160 | /** 161 | * Get Input 162 | * 163 | * @return InputInterface 164 | */ 165 | public function getInput(): ?InputInterface 166 | { 167 | return $this->input; 168 | } 169 | 170 | /** 171 | * Set Output 172 | * 173 | * @param OutputInterface $output 174 | * @return Migration 175 | */ 176 | public function setOutput(OutputInterface $output): Migration 177 | { 178 | $this->output = $output; 179 | return $this; 180 | } 181 | 182 | /** 183 | * Set Input 184 | * 185 | * @param InputInterface $input 186 | * @return Migration 187 | */ 188 | public function setInput(InputInterface $input): Migration 189 | { 190 | $this->input = $input; 191 | return $this; 192 | } 193 | 194 | /** 195 | * Ask for input 196 | * 197 | * @param Question $question 198 | * @return string The users answer 199 | */ 200 | public function ask(Question $question): string 201 | { 202 | return $this->getDialogHelper()->ask($this->getInput(), $this->getOutput(), $question); 203 | } 204 | 205 | /** 206 | * Ask for confirmation 207 | * 208 | * @param Question $question 209 | * @return string The users answer 210 | */ 211 | public function confirm(Question $question): string 212 | { 213 | return $this->getDialogHelper()->ask($this->getInput(), $this->getOutput(), $question); 214 | } 215 | 216 | /** 217 | * Get something from the container 218 | * 219 | * @param string $key 220 | * @return mixed 221 | */ 222 | public function get($key) 223 | { 224 | $c = $this->getContainer(); 225 | return $c[$key]; 226 | } 227 | 228 | /** 229 | * Get Dialog Helper 230 | * 231 | * @return QuestionHelper 232 | */ 233 | public function getDialogHelper(): ?QuestionHelper 234 | { 235 | if ($this->dialogHelper) { 236 | return $this->dialogHelper; 237 | } 238 | 239 | return $this->dialogHelper = new QuestionHelper(); 240 | } 241 | 242 | /** 243 | * Set Dialog Helper 244 | * 245 | * @param QuestionHelper $dialogHelper 246 | * @return Migration 247 | */ 248 | public function setDialogHelper(QuestionHelper $dialogHelper): Migration 249 | { 250 | $this->dialogHelper = $dialogHelper; 251 | return $this; 252 | } 253 | } 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /src/Phpmig/Migration/Migrator.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * For the full copyright and license information, please view the LICENSE 17 | * file that was distributed with this source code. 18 | */ 19 | 20 | /** 21 | * Migrator 22 | * 23 | * Decided what to migrate and migrates 24 | * 25 | * @author Dave Marshall 26 | */ 27 | class Migrator 28 | { 29 | /** 30 | * @var \ArrayAccess 31 | */ 32 | protected $container = null; 33 | 34 | /** 35 | * @var AdapterInterface 36 | */ 37 | protected $adapter = null; 38 | 39 | /** 40 | * @var OutputInterface 41 | */ 42 | protected $output = null; 43 | 44 | /** 45 | * Constructor 46 | * 47 | * @param AdapterInterface $adapter 48 | * @param \ArrayAccess $container 49 | */ 50 | public function __construct(AdapterInterface $adapter, \ArrayAccess $container, OutputInterface $output) 51 | { 52 | $this->container = $container; 53 | $this->adapter = $adapter; 54 | $this->output = $output; 55 | } 56 | 57 | /** 58 | * Run the up method on a migration 59 | * 60 | * @param Migration $migration 61 | * @return void 62 | */ 63 | public function up(Migration $migration) 64 | { 65 | $this->run($migration, 'up'); 66 | return; 67 | } 68 | 69 | /** 70 | * Run the down method on a migration 71 | * 72 | * @param Migration $migration 73 | * @return void 74 | */ 75 | public function down(Migration $migration) 76 | { 77 | $this->run($migration, 'down'); 78 | return; 79 | } 80 | 81 | /** 82 | * Run a migration in a particular direction 83 | * 84 | * @param Migration $migration 85 | * @param string $direction 86 | * @return void 87 | */ 88 | protected function run(Migration $migration, $direction = 'up') 89 | { 90 | $direction = ($direction == 'down' ? 'down' :'up'); 91 | $this->getOutput()->writeln(sprintf( 92 | ' == ' . 93 | $migration->getVersion() . ' ' . 94 | $migration->getName() . ' ' . 95 | '' . 96 | ($direction == 'up' ? 'migrating' : 'reverting') . 97 | '' 98 | )); 99 | $start = microtime(1); 100 | $migration->setContainer($this->getContainer()); 101 | $migration->init(); 102 | $migration->{$direction}(); 103 | $this->getAdapter()->{$direction}($migration); 104 | $end = microtime(1); 105 | $this->getOutput()->writeln(sprintf( 106 | ' == ' . 107 | $migration->getVersion() . ' ' . 108 | $migration->getName() . ' ' . 109 | '' . 110 | ($direction == 'up' ? 'migrated ' : 'reverted ') . 111 | sprintf("%.4fs", $end - $start) . 112 | '' 113 | )); 114 | } 115 | 116 | /** 117 | * Get Container 118 | * 119 | * @return \ArrayAccess 120 | */ 121 | public function getContainer() 122 | { 123 | return $this->container; 124 | } 125 | 126 | /** 127 | * Set Container 128 | * 129 | * @param \ArrayAccess $container 130 | * @return Migrator 131 | */ 132 | public function setContainer(\ArrayAccess $container) 133 | { 134 | $this->container = $container; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Get Adapter 140 | * 141 | * @return AdapterInterface 142 | */ 143 | public function getAdapter() 144 | { 145 | return $this->adapter; 146 | } 147 | 148 | /** 149 | * Set Adapter 150 | * 151 | * @param AdapterInterface $adapter 152 | * @return Migrator 153 | */ 154 | public function setAdapter(AdapterInterface $adapter) 155 | { 156 | $this->adapter = $adapter; 157 | return $this; 158 | } 159 | 160 | /** 161 | * Get Output 162 | * 163 | * @return OutputInterface 164 | */ 165 | public function getOutput() 166 | { 167 | return $this->output; 168 | } 169 | 170 | /** 171 | * Set Output 172 | * 173 | * @param OutputInterface $output 174 | * @return Migrator 175 | */ 176 | public function setOutput(OutputInterface $output) 177 | { 178 | $this->output = $output; 179 | return $this; 180 | } 181 | } 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /src/Phpmig/Pimple/Pimple.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | /** 15 | * Here to provide a little bit of BC, this used to be a bundled version of 16 | * Pimple 17 | */ 18 | class Pimple extends \Pimple {} 19 | -------------------------------------------------------------------------------- /tests/Phpmig/Api/PhpmigApplicationTest.php: -------------------------------------------------------------------------------- 1 | output = new Output\NullOutput(); 25 | $this->setTempDir($this->makeTempDir()); 26 | } 27 | 28 | public function tearDown(): void 29 | { 30 | $this->cleanTempDir(); 31 | } 32 | 33 | /** 34 | * @covers ::__construct 35 | */ 36 | public function test__construct() 37 | { 38 | $app = new PhpmigApplication( 39 | $this->getContainer( 40 | $this->getAdapter(), 41 | array($this->getTempDir() . DIRECTORY_SEPARATOR . "InvalidMigration.php"), 42 | $this->getTempDir() 43 | ), 44 | $this->output 45 | ); 46 | $this->assertInstanceOf("Phpmig\Api\PhpmigApplication", $app); 47 | } 48 | 49 | /** 50 | * @covers ::up 51 | */ 52 | public function testUp() 53 | { 54 | $adapter = $this->getAdapter(array($this->prev_version, $this->current_version)); 55 | $adapter->expects($this->once()) 56 | ->method('hasSchema') 57 | ->will($this->returnValue(false)); 58 | 59 | $migrations = $this->getMigrations(); 60 | $this->createTestMigrations($migrations); 61 | 62 | $container = $this->getContainer($adapter, $migrations, $this->getTempDir()); 63 | $container['phpmig.migrator'] = $this->getMigrator($adapter, $container, $this->output, 1, 0); 64 | 65 | $app = new PhpmigApplication($container, $this->output); 66 | 67 | $app->up($this->next_version); 68 | } 69 | 70 | /** 71 | * @covers ::down 72 | */ 73 | public function testDown() 74 | { 75 | $adapter = $this->getAdapter(array($this->prev_version, $this->current_version)); 76 | $migrations = $this->getMigrations(); 77 | $this->createTestMigrations($migrations); 78 | 79 | $container = $this->getContainer($adapter, $migrations, $this->getTempDir()); 80 | $container['phpmig.migrator'] = $this->getMigrator($adapter, $container, $this->output, 0, 1); 81 | 82 | $app = new PhpmigApplication($container, $this->output); 83 | 84 | $app->down($this->prev_version); 85 | } 86 | 87 | /** 88 | * @covers ::getMigrations 89 | * @covers ::loadMigrations 90 | * @covers ::migrationToClassName 91 | */ 92 | public function testGetMigrations() 93 | { 94 | $migrations = $this->getMigrations(); 95 | $this->createTestMigrations($migrations); 96 | 97 | $app = new PhpmigApplication( 98 | $this->getContainer( 99 | $this->getAdapter(array($this->current_version)), 100 | $migrations, 101 | $this->getTempDir() 102 | ), 103 | $this->output 104 | ); 105 | 106 | // up 107 | $this->assertCount(2, $app->getMigrations(0, $this->next_version)); 108 | $this->assertCount(2, $app->getMigrations(0, null)); 109 | $this->assertCount(2, $app->getMigrations($this->prev_version, $this->next_version)); 110 | $this->assertCount(2, $app->getMigrations($this->current_version, $this->next_version)); 111 | $this->assertCount(0, $app->getMigrations($this->next_version, $this->next_version)); 112 | 113 | // down 114 | $this->assertCount(0, $app->getMigrations($this->next_version, $this->current_version)); 115 | $this->assertCount(1, $app->getMigrations($this->current_version, $this->prev_version)); 116 | $this->assertCount(1, $app->getMigrations($this->next_version, $this->prev_version)); 117 | $this->assertCount(1, $app->getMigrations($this->next_version, 0)); 118 | } 119 | 120 | /** 121 | * @covers ::getMigrations 122 | * @covers ::loadMigrations 123 | * @covers ::migrationToClassName 124 | * @dataProvider getMigrationsExceptionProvider 125 | */ 126 | public function testGetMigrationsException($migrations, $class_names, $extends) 127 | { 128 | foreach ($migrations as &$migration) { 129 | $migration = $this->getTempDir() . DIRECTORY_SEPARATOR . $migration; 130 | } 131 | 132 | $this->createTestMigrations($migrations, $class_names, $extends); 133 | $adapter = $this->getAdapter(); 134 | $container = $this->getContainer($adapter, $migrations, $this->getTempDir()); 135 | 136 | $app = new PhpmigApplication($container, $this->output); 137 | $this->expectException(\InvalidArgumentException::class); 138 | $app->getMigrations(0); 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | public function getMigrationsExceptionProvider() 145 | { 146 | return array( 147 | // Duplicate version 148 | array( 149 | array("20141112000000_Test01.php", "20141112000000_Test02.php"), 150 | array("Test01", "Test02"), 151 | "Migration" 152 | ), 153 | // Duplicate name 154 | array( 155 | array("20141112030000_Test03.php", "20141112040000_Test03.php"), 156 | array("Test03", "Test04"), 157 | "Migration" 158 | ), 159 | // Class not found 160 | array( 161 | array("20141112050000_Test05.php"), 162 | array("InvalidClass"), 163 | "Migration" 164 | ), 165 | // Bad inheritance 166 | array( 167 | array("20141112060000_Test06.php"), 168 | array("Test06"), 169 | "\Exception" 170 | ) 171 | ); 172 | } 173 | 174 | /** 175 | * @covers ::getVersion 176 | */ 177 | public function testGetVersion() 178 | { 179 | $migrations = $this->getMigrations(); 180 | $this->createTestMigrations($migrations); 181 | 182 | $app = new PhpmigApplication( 183 | $this->getContainer( 184 | $this->getAdapter(array($this->current_version)), 185 | $migrations, 186 | $this->getTempDir() 187 | ), 188 | $this->output 189 | ); 190 | 191 | $this->assertEquals($this->current_version, $app->getVersion()); 192 | 193 | $app = new PhpmigApplication( 194 | $this->getContainer( 195 | $this->getAdapter(), 196 | array(), 197 | $this->getTempDir() 198 | ), 199 | $this->output 200 | ); 201 | 202 | $this->assertEquals(0, $app->getVersion()); 203 | } 204 | 205 | /** 206 | * @param array $version 207 | * @return Phpmig\Adapter\AdapterInterface mock 208 | */ 209 | protected function getAdapter(array $versions = array()) 210 | { 211 | $adapter = $this->createMock('Phpmig\Adapter\AdapterInterface'); 212 | $adapter->expects($this->any()) 213 | ->method('fetchAll') 214 | ->will($this->returnValue($versions)); 215 | return $adapter; 216 | } 217 | 218 | /** 219 | * @param object $adapter 220 | * @param object $container 221 | * @param object $output 222 | * @param int $times_up 223 | * @param int $times_down 224 | * @return Phpmig\Migration\Migrator mock 225 | */ 226 | protected function getMigrator($adapter, $container, $output, $times_up, $times_down) 227 | { 228 | $migrator = $this->createMock("Phpmig\Migration\Migrator", array("up", "down"), array($adapter, $container, $output)); 229 | if ($times_up > 0) { 230 | $migrator->expects($this->exactly($times_up)) 231 | ->method("up") 232 | ->with($this->isInstanceOf("Phpmig\Migration\Migration")); 233 | } 234 | 235 | if ($times_down > 0) { 236 | $migrator->expects($this->exactly($times_down)) 237 | ->method("down") 238 | ->with($this->isInstanceOf("Phpmig\Migration\Migration")); 239 | } 240 | return $migrator; 241 | } 242 | 243 | /** 244 | * @param object $adapter 245 | * @param array $migrations 246 | * @param string $migrations_path 247 | */ 248 | protected function getContainer($adapter, array $migrations, $migrations_path) 249 | { 250 | return new \ArrayObject(array( 251 | 'phpmig.adapter' => $adapter, 252 | 'phpmig.migrations' => $migrations, 253 | 'phpmig.migrations_path' => $migrations_path 254 | )); 255 | } 256 | 257 | /** 258 | * @return array Migration filenames 259 | */ 260 | protected function getMigrations() 261 | { 262 | $tmp_dir = $this->getTempDir() . DIRECTORY_SEPARATOR; 263 | $seed = md5(mt_rand()); 264 | return array( 265 | $tmp_dir . $this->prev_version . "_Test" . substr($seed, 0, 8) . ".php", 266 | $tmp_dir . $this->current_version . "_Test" . substr($seed, 8, 8) . ".php", 267 | $tmp_dir . $this->next_version . "_Test" . substr($seed, 16, 8) . ".php", 268 | $tmp_dir . "InvalidTest" . substr($seed, 24, 8) . ".php" 269 | ); 270 | } 271 | 272 | /** 273 | * @return string 274 | */ 275 | protected function getTempDir() 276 | { 277 | return $this->temp_dir; 278 | } 279 | 280 | /** 281 | * @param string $dir 282 | */ 283 | protected function setTempDir($dir) 284 | { 285 | $this->temp_dir = $dir; 286 | } 287 | 288 | /** 289 | * @return string The temp directory created 290 | */ 291 | protected function makeTempDir() 292 | { 293 | $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(mt_rand()); 294 | mkdir($dir); 295 | 296 | return $dir; 297 | } 298 | 299 | protected function cleanTempDir() 300 | { 301 | $dir = $this->getTempDir(); 302 | $dh = opendir($dir); 303 | if ($dh !== false) { 304 | while (($file = readdir($dh))) { 305 | if ($file[0] == ".") { 306 | continue; 307 | } 308 | 309 | if (is_dir($dir . DIRECTORY_SEPARATOR . $file)) { 310 | $this->cleanTempDir($dir . DIRECTORY_SEPARATOR . $file); 311 | } else { 312 | unlink($dir . DIRECTORY_SEPARATOR . $file); 313 | } 314 | } 315 | } 316 | closedir($dh); 317 | rmdir($dir); 318 | } 319 | 320 | protected function createTestMigrations(array $migrations, ?array $class_names = null, $extends = "Migration") 321 | { 322 | $class =<<< 'CODE' 323 | getContainer(); 335 | } 336 | 337 | /** 338 | * Undo the migration 339 | */ 340 | public function down() 341 | { 342 | $container = $this->getContainer(); 343 | } 344 | } 345 | CODE; 346 | foreach ($migrations as $i => $file) { 347 | if ($class_names !== null && isset($class_names[$i])) { 348 | $class_name = $class_names[$i]; 349 | } else { 350 | $class_name = str_replace(' ', '', ucwords(str_replace('_', ' ', preg_replace('/^[0-9]+_/', '', basename($file, ".php"))))); 351 | } 352 | file_put_contents($file, sprintf($class, $class_name, $extends)); 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /tests/Phpmig/Console/Command/InitCommandTest.php: -------------------------------------------------------------------------------- 1 | setTempDir($this->createTempDir()); 21 | } 22 | 23 | public function tearDown(): void 24 | { 25 | $this->cleanUpTempDir($this->getTempDir()); 26 | } 27 | 28 | public function testExecuteCreatesMigrationsDir() 29 | { 30 | $command = $this->createCommand(); 31 | $tester = $this->createCommandTester($command); 32 | 33 | $tempDir = $this->getTempDir(); 34 | chdir($tempDir); 35 | 36 | $migrationsDir = $tempDir . DIRECTORY_SEPARATOR . 'migrations'; 37 | 38 | $tester->execute(array('command' => $command->getName())); 39 | 40 | $this->assertTrue(file_exists($migrationsDir)); 41 | $this->assertTrue(is_dir($migrationsDir)); 42 | } 43 | 44 | public function testExecuteMigrationDirAlreadyExists() 45 | { 46 | $command = $this->createCommand(); 47 | $tester = $this->createCommandTester($command); 48 | 49 | $tempDir = $this->getTempDir(); 50 | chdir($tempDir); 51 | 52 | mkdir($tempDir . DIRECTORY_SEPARATOR . 'migrations'); 53 | 54 | $tester->execute(array('command' => $command->getName())); 55 | 56 | $this->assertStringContainsStringIgnoringCase('migrations already exists', $tester->getDisplay()); 57 | } 58 | 59 | public function testExecuteCreatesBootstrapFile() 60 | { 61 | $command = $this->createCommand(); 62 | $tester = $this->createCommandTester($command); 63 | 64 | $tempDir = $this->getTempDir(); 65 | chdir($tempDir); 66 | 67 | $tester->execute(array('command' => $command->getName())); 68 | 69 | $this->assertTrue(file_exists($tempDir . DIRECTORY_SEPARATOR . 'phpmig.php')); 70 | } 71 | 72 | public function testExecuteBootstrapFileExists() 73 | { 74 | $command = $this->createCommand(); 75 | $tester = $this->createCommandTester($command); 76 | 77 | $tempDir = $this->getTempDir(); 78 | chdir($tempDir); 79 | 80 | touch($tempDir . DIRECTORY_SEPARATOR . 'phpmig.php'); 81 | 82 | $tester->execute(array('command' => $command->getName())); 83 | 84 | $this->assertStringContainsStringIgnoringCase('phpmig.php already exists', $tester->getDisplay()); 85 | } 86 | 87 | /** 88 | * @return Application 89 | */ 90 | private function createConsoleApp() 91 | { 92 | $app = new Application(); 93 | $app->add(new InitCommand()); 94 | 95 | return $app; 96 | } 97 | 98 | /** 99 | * @param Command $command 100 | * @return CommandTester 101 | */ 102 | private function createCommandTester(Command $command) 103 | { 104 | return new CommandTester($command); 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | private function createTempDir() 111 | { 112 | $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(microtime()); 113 | mkdir($tempDir); 114 | 115 | return $tempDir; 116 | } 117 | 118 | /** 119 | * @param string $dir 120 | */ 121 | private function cleanUpTempDir($dir) 122 | { 123 | $dh = opendir($dir); 124 | 125 | while (false !== $file = readdir($dh)) { 126 | if ('.' !== substr($file, 0, 1)) { 127 | if (is_dir($dir . DIRECTORY_SEPARATOR . $file)) { 128 | $this->cleanUpTempDir($dir . DIRECTORY_SEPARATOR . $file); 129 | } else { 130 | unlink($dir . DIRECTORY_SEPARATOR . $file); 131 | } 132 | } 133 | } 134 | 135 | closedir($dh); 136 | rmdir($dir); 137 | } 138 | 139 | /** 140 | * @param string $tempDir 141 | */ 142 | public function setTempDir($tempDir) 143 | { 144 | $this->tempDir = $tempDir; 145 | } 146 | 147 | /** 148 | * @return string 149 | */ 150 | public function getTempDir() 151 | { 152 | return $this->tempDir; 153 | } 154 | 155 | /** 156 | * @return Command 157 | */ 158 | private function createCommand() 159 | { 160 | $app = $this->createConsoleApp(); 161 | 162 | return $app->find('init'); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/Phpmig/Migration/MigrationTest.php: -------------------------------------------------------------------------------- 1 | migrationOutput = m::mock('Symfony\Component\Console\Output\OutputInterface')->shouldIgnoreMissing(); 37 | $this->migrationInput = m::mock('Symfony\Component\Console\Input\InputInterface')->shouldIgnoreMissing(); 38 | $this->migrationDialogHelper = m::mock('Symfony\Component\Console\Helper\QuestionHelper')->shouldIgnoreMissing(); 39 | 40 | $this->migration = new Migration(1); 41 | $this->migration->setOutput($this->migrationOutput); 42 | $this->migration->setInput($this->migrationInput); 43 | $this->migration->setDialogHelper($this->migrationDialogHelper); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function shouldAskForInput() 50 | { 51 | $this->migrationDialogHelper->shouldReceive('ask') 52 | ->with($this->migrationInput, $this->migrationOutput, $question = new Question('Wat?','huh?')) 53 | ->andReturn($ans = 'dave') 54 | ->once(); 55 | 56 | $this->assertEquals($ans, $this->migration->ask($question)); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function shouldAskForConfirmation() 63 | { 64 | $this->migrationDialogHelper->shouldReceive('ask') 65 | ->with($this->migrationInput, $this->migrationOutput, $question = new Question('Wat?',true)) 66 | ->andReturn($ans = 'dave') 67 | ->once(); 68 | 69 | $this->assertEquals($ans, $this->migration->confirm($question)); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function shouldRetrieveServices() 76 | { 77 | $this->migration->setContainer(new \ArrayObject(array('service' => 123))); 78 | $this->assertEquals(123, $this->migration->get('service')); 79 | } 80 | 81 | public function testMigrationVersion() 82 | { 83 | $this->assertEquals(1, $this->migration->getVersion()); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /tests/Phpmig/PhpmigApplicationTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($app->has($commandName)); 21 | } 22 | 23 | $this->assertSame($version, $app->getVersion()); 24 | } 25 | } 26 | --------------------------------------------------------------------------------