├── .github ├── actions │ └── setup-php │ │ └── action.yml ├── linters │ └── phpcs.xml └── workflows │ ├── pr-lint.yml │ └── testing.yml ├── .gitignore ├── .travis.yml ├── Module.php ├── README.md ├── composer.json ├── config └── module.config.php ├── phpunit.xml ├── src └── ZfSimpleMigrations │ ├── Controller │ ├── MigrateController.php │ └── MigrateControllerFactory.php │ ├── Library │ ├── AbstractMigration.php │ ├── Migration.php │ ├── MigrationAbstractFactory.php │ ├── MigrationException.php │ ├── MigrationInterface.php │ ├── MigrationSkeletonGenerator.php │ ├── MigrationSkeletonGeneratorAbstractFactory.php │ └── OutputWriter.php │ └── Model │ ├── MigrationVersion.php │ ├── MigrationVersionTable.php │ ├── MigrationVersionTableAbstractFactory.php │ └── MigrationVersionTableGatewayAbstractFactory.php └── tests └── ZfSimpleMigrations ├── Controller └── MigrateControllerFactoryTest.php ├── Library ├── ApplyMigration │ ├── Version01.php │ └── Version02.php ├── MigrationAbstractFactoryTest.php ├── MigrationSkeletonGeneratorAbstractFactoryTest.php └── MigrationTest.php └── Model ├── MigrationVersionTableAbstractFactoryTest.php └── MigrationVersionTableGatewayAbstractFactoryTest.php /.github/actions/setup-php/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup PHP environment 2 | description: Reusable action to avoid copy-pasting between workflows 3 | inputs: 4 | composer-install: 5 | description: Install dependencies to teh pre-setup PHP environment 6 | required: false 7 | default: "true" 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Setup PHP 12 | uses: shivammathur/setup-php@v2 13 | with: 14 | php-version: '7.0' 15 | tools: 'composer:v2' 16 | coverage: none 17 | env: 18 | fail-fast: "true" 19 | 20 | - name: Install PHP deps 21 | if: ${{ inputs.composer-install }} 22 | shell: bash 23 | run: composer install 24 | -------------------------------------------------------------------------------- /.github/linters/phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The default coding standard for usage with GitHub Super-Linter. It just includes PSR12. 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/pr-lint.yml: -------------------------------------------------------------------------------- 1 | name: "PR: Lint" 2 | 3 | concurrency: 4 | group: pr-lint-${{ github.head_ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@v2 20 | with: 21 | # Full git history is needed to get a proper list of changed files within `super-linter` 22 | fetch-depth: 0 23 | 24 | - name: Setup PHP 25 | uses: ./.github/actions/setup-php 26 | 27 | - name: Lint Code Base 28 | uses: github/super-linter@v4 29 | env: 30 | VALIDATE_ALL_CODEBASE: false 31 | DEFAULT_BRANCH: master 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | VALIDATE_PHP_PSALM: false 34 | # TODO: should be enabled back one day 35 | VALIDATE_PHP_PHPSTAN: false 36 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | unit-test: 11 | name: Unit Test 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup PHP 20 | uses: ./.github/actions/setup-php 21 | 22 | - name: Run tests 23 | run: phpdbg -qrr vendor/bin/phpunit -c ./phpunit.xml --group "unit" --coverage-clover="./coverage.xml" --coverage-text="php://stdout" --log-junit="./junit-logfile.xml" 24 | 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v2 27 | if: success() 28 | with: 29 | files: ./coverage.xml,./junit-logfile.xml 30 | flags: unit 31 | fail_ci_if_error: false 32 | 33 | integration-test: 34 | name: Integration Test 35 | needs: unit-test 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 10 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | db: 43 | - DB_TYPE: 'Pdo_Sqlite' 44 | DB_HOST: '' 45 | DB_USERNAME: '' 46 | DB_PASSWORD: '' 47 | DB_NAME: '/tmp/db.sqlite' 48 | DB_PORT: '' 49 | - DB_TYPE: 'Pdo_Pgsql' 50 | DB_HOST: 'localhost' 51 | DB_USERNAME: 'test' 52 | DB_PASSWORD: 'test' 53 | DB_NAME: 'test' 54 | DB_PORT: '5432' 55 | - DB_TYPE: 'Pdo_Mysql' 56 | DB_HOST: '127.0.0.1' 57 | DB_USERNAME: 'test' 58 | DB_PASSWORD: 'test' 59 | DB_NAME: 'test' 60 | DB_PORT: '3306' 61 | - DB_TYPE: 'Mysqli' 62 | DB_HOST: '127.0.0.1' 63 | DB_USERNAME: 'test' 64 | DB_PASSWORD: 'test' 65 | DB_NAME: 'test' 66 | DB_PORT: '3306' 67 | 68 | # Run all the DBs at once, although every test will use only one 69 | services: 70 | postgres: 71 | image: postgres:9.6-alpine 72 | ports: 73 | - "5432:5432" 74 | env: 75 | LC_ALL: C.UTF-8 76 | POSTGRES_USER: test 77 | POSTGRES_PASSWORD: test 78 | POSTGRES_DB: test 79 | options: >- 80 | --health-cmd pg_isready 81 | --health-interval 10s 82 | --health-timeout 5s 83 | --health-retries 5 84 | 85 | mysql: 86 | image: mysql:5.7 87 | ports: 88 | - "3306:3306" 89 | env: 90 | MYSQL_ROOT_PASSWORD: test 91 | MYSQL_DATABASE: test 92 | MYSQL_USER: test 93 | MYSQL_PASSWORD: test 94 | options: >- 95 | --health-cmd "mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD" 96 | --health-interval 10s 97 | --health-timeout 5s 98 | --health-retries 5 99 | 100 | steps: 101 | - name: Check out code 102 | uses: actions/checkout@v2 103 | 104 | - name: Setup PHP 105 | uses: ./.github/actions/setup-php 106 | 107 | - name: Run tests 108 | run: phpdbg -qrr vendor/bin/phpunit -c ./phpunit.xml --group "integration" --coverage-clover="./coverage.xml" --coverage-text="php://stdout" --log-junit="./junit-logfile.xml" 109 | env: 110 | DB_TYPE: ${{ matrix.db.DB_TYPE }} 111 | DB_HOST: ${{ matrix.db.DB_HOST }} 112 | DB_USERNAME: ${{ matrix.db.DB_USERNAME }} 113 | DB_PASSWORD: ${{ matrix.db.DB_PASSWORD }} 114 | DB_NAME: ${{ matrix.db.DB_NAME }} 115 | DB_PORT: ${{ matrix.db.DB_PORT }} 116 | 117 | - name: Upload coverage to Codecov 118 | uses: codecov/codecov-action@v2 119 | if: success() 120 | with: 121 | files: ./coverage.xml,./junit-logfile.xml 122 | flags: integration 123 | fail_ci_if_error: false 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | junit-logfile.xml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | env: 7 | - DB=sqlite 8 | - DB=mysql 9 | - DB=mysqli 10 | - DB=postgresql 11 | 12 | matrix: 13 | allow_failures: 14 | - php: 5.4 15 | env: DB=postgresql 16 | - php: 5.5 17 | env: DB=postgresql 18 | - php: 5.6 19 | env: DB=postgresql 20 | 21 | services: 22 | - postgresql 23 | 24 | before_script: 25 | - composer install 26 | - mysql -e "create database IF NOT EXISTS test;" -uroot 27 | - psql -c 'create database test;' -U postgres 28 | 29 | script: phpunit -c phpunit.${DB}.xml -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | getApplication()->getEventManager(); 30 | $moduleRouteListener = new ModuleRouteListener(); 31 | $moduleRouteListener->attach($eventManager); 32 | } 33 | 34 | public function getConfig() 35 | { 36 | return include __DIR__ . '/config/module.config.php'; 37 | } 38 | 39 | public function getAutoloaderConfig() 40 | { 41 | return [ 42 | 'Zend\Loader\StandardAutoloader' => [ 43 | 'namespaces' => [ 44 | __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, 45 | ], 46 | ], 47 | ]; 48 | } 49 | 50 | public function getServiceConfig() 51 | { 52 | return [ 53 | 'abstract_factories' => [ 54 | 'ZfSimpleMigrations\\Library\\MigrationAbstractFactory', 55 | 'ZfSimpleMigrations\\Model\\MigrationVersionTableAbstractFactory', 56 | 'ZfSimpleMigrations\\Model\\MigrationVersionTableGatewayAbstractFactory', 57 | 'ZfSimpleMigrations\\Library\\MigrationSkeletonGeneratorAbstractFactory' 58 | ], 59 | ]; 60 | } 61 | 62 | public function getConsoleUsage(Console $console) 63 | { 64 | return [ 65 | 'Get last applied migration version', 66 | 'migration version []' => '', 67 | ['[]', 'specify which configured migrations to run, defaults to `default`'], 68 | 69 | 'List available migrations', 70 | 'migration list [] [--all]' => '', 71 | ['--all', 'Include applied migrations'], 72 | ['[]', 'specify which configured migrations to run, defaults to `default`'], 73 | 74 | 'Generate new migration skeleton class', 75 | 'migration generate []' => '', 76 | ['[]', 'specify which configured migrations to run, defaults to `default`'], 77 | 78 | 'Execute migration', 79 | 'migration apply [] [] [--force] [--down] [--fake]' => '', 80 | ['[]', 'specify which configured migrations to run, defaults to `default`'], 81 | [ 82 | '--force', 83 | 'Force apply migration even if it\'s older than the last migrated. Works only with explicitly set.' 84 | ], 85 | [ 86 | '--down', 87 | 'Force apply down migration. Works only with --force flag set.' 88 | ], 89 | [ 90 | '--fake', 91 | 'Fake apply or apply down migration. Adds/removes migration to the list of applied w/out really applying it. Works only with explicitly set.' 92 | ], 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZfSimpleMigrations 2 | 3 | Simple Migrations for Zend Framework 2. Project originally based 4 | on [ZendDbMigrations](https://github.com/vadim-knyzev/ZendDbMigrations) but module author did not response for issues 5 | and pull-requests so fork became independent project. 6 | 7 | ## Supported Drivers 8 | 9 | The following DB adapter drivers are supported by this module. 10 | 11 | * Pdo_Sqlite 12 | * Pdo_Mysql 13 | * Pdo_Pgsql 14 | * Mysqli _only if you configure the driver options with `'buffer_results' => true`_ 15 | 16 | ## Installation 17 | 18 | ### Using composer 19 | 20 | ```bash 21 | php composer.phar require vgarvardt/zf-simple-migrations:dev-master 22 | php composer.phar update 23 | ``` 24 | 25 | add `ZfSimpleMigrations` to the `modules` array in application.config.php 26 | 27 | ## Usage 28 | 29 | ### Available commands 30 | 31 | * `migration version []` - show last applied migration (`name` specifies a configured migration) 32 | * `migration list [] [--all]` - list available migrations (`all` includes applied migrations) 33 | * `migration apply [] [] [--force] [--down] [--fake]` - apply or rollback migration 34 | * `migration generate []` - generate migration skeleton class 35 | 36 | Migration classes are stored in `/path/to/project/migrations/` dir by default. 37 | 38 | Generic migration class has name `Version` and implement `ZfSimpleMigrations\Library\MigrationInterface`. 39 | 40 | ### Migration class example 41 | 42 | ```php 43 | addSql(/*Sql instruction*/); 57 | } 58 | 59 | public function down(MetadataInterface $schema) 60 | { 61 | //$this->addSql(/*Sql instruction*/); 62 | } 63 | } 64 | ``` 65 | 66 | #### Multi-statement sql 67 | 68 | While this module supports execution of multiple SQL statements it does not have way to detect if any other statement 69 | than the first contained an error. It is *highly* recommended you only provide single SQL statements to `addSql` at a 70 | time. I.e instead of 71 | 72 | ```php 73 | $this->addSql('SELECT NOW(); SELECT NOW(); SELECT NOW();'); 74 | ``` 75 | 76 | You should use 77 | 78 | ```php 79 | $this->addSql('SELECT NOW();'); 80 | $this->addSql('SELECT NOW();'); 81 | $this->addSql('SELECT NOW();'); 82 | ``` 83 | 84 | ### Accessing ServiceLocator In Migration Class 85 | 86 | By implementing the `Zend\ServiceManager\ServiceLocatorAwareInterface` in your migration class you get access to the 87 | ServiceLocator used in the application. 88 | 89 | ```php 90 | getServiceLocator()->get(/*Get service by alias*/); 109 | //$this->addSql(/*Sql instruction*/); 110 | 111 | } 112 | 113 | public function down(MetadataInterface $schema) 114 | { 115 | //$this->getServiceLocator()->get(/*Get service by alias*/); 116 | //$this->addSql(/*Sql instruction*/); 117 | } 118 | } 119 | ``` 120 | 121 | ### Accessing Zend Db Adapter In Migration Class 122 | 123 | By implementing the `Zend\Db\Adapter\AdapterAwareInterface` in your migration class you get access to the Db Adapter 124 | configured for the migration. 125 | 126 | ```php 127 | addColumn(new Integer('id', false)); 151 | $table->addConstraint(new PrimaryKey('id')); 152 | $table->addColumn(new Varchar('my_column', 64)); 153 | $this->addSql($table->getSqlString($this->adapter->getPlatform())); 154 | } 155 | 156 | public function down(MetadataInterface $schema) 157 | { 158 | $drop = new DropTable('my_table'); 159 | $this->addSql($drop->getSqlString($this->adapter->getPlatform())); 160 | } 161 | } 162 | ``` 163 | 164 | ## Configuration 165 | 166 | ### User Configuration 167 | 168 | The top-level key used to configure this module is `migrations`. 169 | 170 | #### Migration Configurations: Migrations Name 171 | 172 | Each key under `migrations` is a migrations configuration, and the value is an array with one or more of the following 173 | keys. 174 | 175 | ##### Sub-key: `dir` 176 | 177 | The path to the directory where migration files are stored. Defaults to `./migrations` in the project root dir. 178 | 179 | ##### Sub-key: `namespace` 180 | 181 | The class namespace that migration classes will be generated with. Defaults to `ZfSimpleMigrations\Migrations`. 182 | 183 | ##### Sub-key: `show_log` (optional) 184 | 185 | Flag to log output of the migration. Defaults to `true`. 186 | 187 | ##### Sub-key: `adapter` (optional) 188 | 189 | The service alias that will be used to fetch a `Zend\Db\Adapter\Adapter` from the service manager. 190 | 191 | #### User configuration example 192 | 193 | ```php 194 | 'migrations' => array( 195 | 'default' => array( 196 | 'dir' => dirname(__FILE__) . '/../../../../migrations-app', 197 | 'namespace' => 'App\Migrations', 198 | ), 199 | 'albums' => array( 200 | 'dir' => dirname(__FILE__) . '/../../../../migrations-albums', 201 | 'namespace' => 'Albums\Migrations', 202 | 'adapter' => 'AlbumDb' 203 | ), 204 | ), 205 | ``` 206 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vgarvardt/zf-simple-migrations", 3 | "description": "Module for database migrations management.", 4 | "type": "library", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "database", 8 | "db", 9 | "migrations", 10 | "zf2" 11 | ], 12 | "homepage": "https://github.com/vgarvardt/ZfSimpleMigrations", 13 | "authors": [ 14 | { 15 | "name": "Vadim Knyzev", 16 | "email": "vadim.knyzev@gmail.com", 17 | "homepage": "https://vadim-knyzev.blogspot.com/" 18 | }, 19 | { 20 | "name": "Vladimir Garvardt", 21 | "email": "vgarvardt@gmail.com", 22 | "homepage": "https://itskrig.com/" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=7.0,<8", 27 | "zendframework/zendframework": ">=2.2.0,<3" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "6.5.*" 31 | }, 32 | "autoload": { 33 | "psr-0": { 34 | "ZfSimpleMigrations": "src/" 35 | }, 36 | "classmap": [ 37 | "./Module.php" 38 | ] 39 | }, 40 | "autoload-dev": { 41 | "psr-0": { 42 | "ZfSimpleMigrations": "tests/" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config/module.config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default' => [ 6 | 'dir' => dirname(__FILE__) . '/../../../../migrations', 7 | 'namespace' => 'ZfSimpleMigrations\Migrations', 8 | 'show_log' => true, 9 | 'adapter' => 'Zend\Db\Adapter\Adapter' 10 | ], 11 | ], 12 | 'console' => [ 13 | 'router' => [ 14 | 'routes' => [ 15 | 'migration-version' => [ 16 | 'type' => 'simple', 17 | 'options' => [ 18 | 'route' => 'migration version [] [--env=]', 19 | 'defaults' => [ 20 | 'controller' => 'ZfSimpleMigrations\Controller\Migrate', 21 | 'action' => 'version', 22 | 'name' => 'default' 23 | ] 24 | ] 25 | ], 26 | 'migration-list' => [ 27 | 'type' => 'simple', 28 | 'options' => [ 29 | 'route' => 'migration list [] [--env=] [--all]', 30 | 'defaults' => [ 31 | 'controller' => 'ZfSimpleMigrations\Controller\Migrate', 32 | 'action' => 'list', 33 | 'name' => 'default' 34 | ] 35 | ] 36 | ], 37 | 'migration-apply' => [ 38 | 'type' => 'simple', 39 | 'options' => [ 40 | 'route' => 'migration apply [] [] [--env=] [--force] [--down] [--fake]', 41 | 'defaults' => [ 42 | 'controller' => 'ZfSimpleMigrations\Controller\Migrate', 43 | 'action' => 'apply', 44 | 'name' => 'default' 45 | ] 46 | ] 47 | ], 48 | 'migration-generate' => [ 49 | 'type' => 'simple', 50 | 'options' => [ 51 | 'route' => 'migration generate [] [--env=]', 52 | 'defaults' => [ 53 | 'controller' => 'ZfSimpleMigrations\Controller\Migrate', 54 | 'action' => 'generateSkeleton', 55 | 'name' => 'default' 56 | ] 57 | ] 58 | ] 59 | ] 60 | ] 61 | ], 62 | 'controllers' => [ 63 | 'factories' => [ 64 | 'ZfSimpleMigrations\Controller\Migrate' => 'ZfSimpleMigrations\\Controller\\MigrateControllerFactory' 65 | ], 66 | ], 67 | ]; 68 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | 13 | 14 | ./ 15 | 16 | tests/ 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Controller/MigrateController.php: -------------------------------------------------------------------------------- 1 | skeletonGenerator; 29 | } 30 | 31 | /** 32 | * @param MigrationSkeletonGenerator $skeletonGenerator 33 | * @return self 34 | */ 35 | public function setSkeletonGenerator(MigrationSkeletonGenerator $skeletonGenerator): self 36 | { 37 | $this->skeletonGenerator = $skeletonGenerator; 38 | return $this; 39 | } 40 | 41 | public function onDispatch(MvcEvent $e) 42 | { 43 | if (!$this->getRequest() instanceof ConsoleRequest) { 44 | throw new \RuntimeException('You can only use this action from a console!'); 45 | } 46 | 47 | return parent::onDispatch($e); 48 | } 49 | 50 | /** 51 | * Overridden only for PHPDoc return value for IDE code helpers 52 | * 53 | * @return ConsoleRequest 54 | */ 55 | public function getRequest(): ConsoleRequest 56 | { 57 | return parent::getRequest(); 58 | } 59 | 60 | /** 61 | * Get current migration version 62 | * 63 | * @return string 64 | */ 65 | public function versionAction(): string 66 | { 67 | return sprintf("Current version %s\n", $this->getMigration()->getCurrentVersion()); 68 | } 69 | 70 | /** 71 | * List migrations - not applied by default, all with 'all' flag. 72 | * 73 | * @return string 74 | * @throws ReflectionException 75 | */ 76 | public function listAction(): string 77 | { 78 | $migrations = $this->getMigration()->getMigrationClasses($this->getRequest()->getParam('all')); 79 | $list = []; 80 | foreach ($migrations as $m) { 81 | $list[] = sprintf("%s %s - %s", $m['applied'] ? '-' : '+', $m['version'], $m['description']); 82 | } 83 | return (empty($list) ? 'No migrations available.' : implode("\n", $list)) . "\n"; 84 | } 85 | 86 | /** 87 | * Apply migration 88 | * @throws ReflectionException 89 | * @throws MigrationException 90 | */ 91 | public function applyAction(): string 92 | { 93 | $migrations = $this->getMigration()->getMigrationClasses(); 94 | $currentMigrationVersion = $this->getMigration()->getCurrentVersion(); 95 | 96 | $version = $this->getRequest()->getParam('version'); 97 | $force = $this->getRequest()->getParam('force'); 98 | $down = $this->getRequest()->getParam('down'); 99 | $fake = $this->getRequest()->getParam('fake'); 100 | 101 | if (is_null($version) && $force) { 102 | return "Can't force migration apply without migration version explicitly set."; 103 | } 104 | if (is_null($version) && $fake) { 105 | return "Can't fake migration apply without migration version explicitly set."; 106 | } 107 | 108 | $maxMigrationVersion = $this->getMigration()->getMaxMigrationVersion($migrations); 109 | if (!$force && is_null($version) && $currentMigrationVersion >= $maxMigrationVersion) { 110 | return "No migrations to apply.\n"; 111 | } 112 | 113 | $this->getMigration()->migrate($version, $force, $down, $fake); 114 | return "Migrations applied!\n"; 115 | } 116 | 117 | /** 118 | * Generate new migration skeleton class 119 | * @throws MigrationException 120 | */ 121 | public function generateSkeletonAction(): string 122 | { 123 | $classPath = $this->getSkeletonGenerator()->generate(); 124 | 125 | return sprintf("Generated skeleton class @ %s\n", realpath($classPath)); 126 | } 127 | 128 | /** 129 | * @return Migration 130 | */ 131 | public function getMigration(): Migration 132 | { 133 | return $this->migration; 134 | } 135 | 136 | /** 137 | * @param Migration $migration 138 | * @return self 139 | */ 140 | public function setMigration(Migration $migration): self 141 | { 142 | $this->migration = $migration; 143 | return $this; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Controller/MigrateControllerFactory.php: -------------------------------------------------------------------------------- 1 | getServiceLocator(); 24 | } 25 | 26 | /** @var RouteMatch $routeMatch */ 27 | $routeMatch = $serviceLocator->get('Application')->getMvcEvent()->getRouteMatch(); 28 | 29 | $name = $routeMatch->getParam('name', 'default'); 30 | 31 | /** @var Migration $migration */ 32 | $migration = $serviceLocator->get('migrations.migration.' . $name); 33 | /** @var MigrationSkeletonGenerator $generator */ 34 | $generator = $serviceLocator->get('migrations.skeleton-generator.' . $name); 35 | 36 | $controller = new MigrateController(); 37 | 38 | $controller->setMigration($migration); 39 | $controller->setSkeletonGenerator($generator); 40 | 41 | return $controller; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/AbstractMigration.php: -------------------------------------------------------------------------------- 1 | metadata = $metadata; 17 | $this->writer = $writer; 18 | } 19 | 20 | /** 21 | * Add migration query 22 | * 23 | * @param string $sql 24 | */ 25 | protected function addSql($sql) 26 | { 27 | $this->sql[] = $sql; 28 | } 29 | 30 | /** 31 | * Get migration queries 32 | * 33 | * @return array 34 | */ 35 | public function getUpSql() 36 | { 37 | $this->sql = []; 38 | $this->up($this->metadata); 39 | 40 | return $this->sql; 41 | } 42 | 43 | /** 44 | * Get migration rollback queries 45 | * 46 | * @return array 47 | */ 48 | public function getDownSql() 49 | { 50 | $this->sql = []; 51 | $this->down($this->metadata); 52 | 53 | return $this->sql; 54 | } 55 | 56 | /** 57 | * @return OutputWriter 58 | */ 59 | protected function getWriter() 60 | { 61 | return $this->writer; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/Migration.php: -------------------------------------------------------------------------------- 1 | outputWriter; 58 | } 59 | 60 | /** 61 | * @param Adapter $adapter 62 | * @param array $config 63 | * @param MigrationVersionTable $migrationVersionTable 64 | * @param OutputWriter $writer 65 | * @throws MigrationException 66 | */ 67 | public function __construct( 68 | Adapter $adapter, 69 | array $config, 70 | MigrationVersionTable $migrationVersionTable, 71 | OutputWriter $writer = null 72 | ) { 73 | $this->adapter = $adapter; 74 | $this->metadata = new Metadata($this->adapter); 75 | $this->connection = $this->adapter->getDriver()->getConnection(); 76 | $this->migrationsDir = $config['dir']; 77 | $this->migrationsNamespace = $config['namespace']; 78 | $this->migrationVersionTable = $migrationVersionTable; 79 | $this->outputWriter = is_null($writer) ? new OutputWriter() : $writer; 80 | 81 | if (is_null($this->migrationsDir)) { 82 | throw new MigrationException('Migrations directory not set!'); 83 | } 84 | 85 | if (is_null($this->migrationsNamespace)) { 86 | throw new MigrationException('Unknown namespaces!'); 87 | } 88 | 89 | if (!is_dir($this->migrationsDir)) { 90 | if (!mkdir($this->migrationsDir, 0775)) { 91 | throw new MigrationException(sprintf('Failed to create migrations directory %s', $this->migrationsDir)); 92 | } 93 | } 94 | 95 | $this->checkCreateMigrationTable(); 96 | } 97 | 98 | /** 99 | * Create migrations table of not exists 100 | */ 101 | protected function checkCreateMigrationTable() 102 | { 103 | if ($this->adapter->getPlatform()->getName() === 'PostgreSQL') { 104 | $this->checkCreateMigrationTablePg(); 105 | return; 106 | } 107 | 108 | $table = new Ddl\CreateTable(MigrationVersion::TABLE_NAME); 109 | $table->addColumn(new Ddl\Column\Integer('id', false, null, ['autoincrement' => true])); 110 | $table->addColumn(new Ddl\Column\BigInteger('version')); 111 | $table->addConstraint(new Ddl\Constraint\PrimaryKey('id')); 112 | $table->addConstraint(new Ddl\Constraint\UniqueKey('version')); 113 | 114 | $sql = new Sql($this->adapter); 115 | 116 | try { 117 | $this->adapter->query($sql->buildSqlString($table), Adapter::QUERY_MODE_EXECUTE); 118 | } catch (Exception $e) { 119 | // currently there are no db-independent way to check if table exists 120 | // so we assume that table exists when we catch exception 121 | } 122 | } 123 | 124 | /** 125 | * Special case because ZF2 DDL does not support pg serial field 126 | */ 127 | protected function checkCreateMigrationTablePg() 128 | { 129 | $sql = <<adapter->query(sprintf($sql, MigrationVersion::TABLE_NAME), Adapter::QUERY_MODE_EXECUTE); 137 | } 138 | 139 | /** 140 | * @return int 141 | */ 142 | public function getCurrentVersion(): int 143 | { 144 | return $this->migrationVersionTable->getCurrentVersion(); 145 | } 146 | 147 | /** 148 | * @param int $version target migration version, if not set all not applied available migrations will be applied 149 | * @param bool $force force apply migration 150 | * @param bool $down rollback migration 151 | * @param bool $dryRun 152 | * @throws MigrationException 153 | * @throws ReflectionException 154 | */ 155 | public function migrate($version = null, bool $force = false, bool $down = false, bool $dryRun = false) 156 | { 157 | $migrations = $this->getMigrationClasses($force); 158 | 159 | if (!is_null($version) && !$this->hasMigrationVersions($migrations, $version)) { 160 | throw new MigrationException(sprintf('Migration version %s is not found!', $version)); 161 | } 162 | 163 | $currentMigrationVersion = $this->migrationVersionTable->getCurrentVersion(); 164 | if (!is_null($version) && $version == $currentMigrationVersion && !$force) { 165 | throw new MigrationException(sprintf('Migration version %s is current version!', $version)); 166 | } 167 | 168 | if ($version && $force) { 169 | foreach ($migrations as $migration) { 170 | if ($migration['version'] == $version) { 171 | // if existing migration is forced to apply - delete its information from migrated 172 | // to avoid duplicate key error 173 | if (!$down) { 174 | $this->migrationVersionTable->delete($migration['version']); 175 | } 176 | $this->applyMigration($migration, $down, $dryRun); 177 | break; 178 | } 179 | } 180 | // target migration version not set or target version is greater than 181 | // the last applied migration -> apply migrations 182 | } elseif (is_null($version) || (!is_null($version) && $version > $currentMigrationVersion)) { 183 | foreach ($migrations as $migration) { 184 | if ($migration['version'] > $currentMigrationVersion) { 185 | if (is_null($version) || (!is_null($version) && $version >= $migration['version'])) { 186 | $this->applyMigration($migration, false, $dryRun); 187 | } 188 | } 189 | } 190 | // target migration version is set -> rollback migration 191 | } elseif (!is_null($version) && $version < $currentMigrationVersion) { 192 | $migrationsByDesc = $this->sortMigrationsByVersionDesc($migrations); 193 | foreach ($migrationsByDesc as $migration) { 194 | if ($migration['version'] > $version && $migration['version'] <= $currentMigrationVersion) { 195 | $this->applyMigration($migration, true, $dryRun); 196 | } 197 | } 198 | } 199 | } 200 | 201 | /** 202 | * @param ArrayIterator $migrations 203 | * @return ArrayIterator 204 | */ 205 | public function sortMigrationsByVersionDesc(ArrayIterator $migrations): ArrayIterator 206 | { 207 | $sortedMigrations = clone $migrations; 208 | 209 | $sortedMigrations->uasort(function ($a, $b) { 210 | if ($a['version'] == $b['version']) { 211 | return 0; 212 | } 213 | 214 | return ($a['version'] > $b['version']) ? -1 : 1; 215 | }); 216 | 217 | return $sortedMigrations; 218 | } 219 | 220 | /** 221 | * Check migrations classes existence 222 | * 223 | * @param ArrayIterator $migrations 224 | * @param int $version 225 | * @return bool 226 | */ 227 | public function hasMigrationVersions(ArrayIterator $migrations, int $version): bool 228 | { 229 | foreach ($migrations as $migration) { 230 | if ($migration['version'] == $version) { 231 | return true; 232 | } 233 | } 234 | 235 | return false; 236 | } 237 | 238 | /** 239 | * @param ArrayIterator $migrations 240 | * @return int 241 | */ 242 | public function getMaxMigrationVersion(ArrayIterator $migrations): int 243 | { 244 | $versions = []; 245 | foreach ($migrations as $migration) { 246 | $versions[] = $migration['version']; 247 | } 248 | 249 | sort($versions, SORT_NUMERIC); 250 | $versions = array_reverse($versions); 251 | 252 | return count($versions) > 0 ? $versions[0] : 0; 253 | } 254 | 255 | /** 256 | * @param bool $all 257 | * @return ArrayIterator 258 | * @throws ReflectionException 259 | */ 260 | public function getMigrationClasses(bool $all = false): ArrayIterator 261 | { 262 | $classes = new ArrayIterator(); 263 | 264 | $pattern = sprintf('%s/Version*.php', $this->migrationsDir); 265 | $iterator = new GlobIterator($pattern, FilesystemIterator::KEY_AS_FILENAME); 266 | foreach ($iterator as $item) { 267 | /** @var $item SplFileInfo */ 268 | if (preg_match('/(Version(\d+))\.php/', $item->getFilename(), $matches)) { 269 | $applied = $this->migrationVersionTable->applied($matches[2]); 270 | if ($all || !$applied) { 271 | $className = $this->migrationsNamespace . '\\' . $matches[1]; 272 | 273 | if (!class_exists($className)) { 274 | require_once $this->migrationsDir . '/' . $item->getFilename(); 275 | } 276 | 277 | if (class_exists($className)) { 278 | $reflectionClass = new ReflectionClass($className); 279 | $reflectionDescription = new ReflectionProperty($className, 'description'); 280 | 281 | if ($reflectionClass->implementsInterface(MigrationInterface::class)) { 282 | $classes->append([ 283 | 'version' => $matches[2], 284 | 'class' => $className, 285 | 'description' => $reflectionDescription->getValue(), 286 | 'applied' => $applied, 287 | ]); 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | $classes->uasort(function ($a, $b) { 295 | if ($a['version'] == $b['version']) { 296 | return 0; 297 | } 298 | 299 | return ($a['version'] < $b['version']) ? -1 : 1; 300 | }); 301 | 302 | return $classes; 303 | } 304 | 305 | /** 306 | * @throws MigrationException 307 | */ 308 | protected function applyMigration(array $migration, $down = false, $dryRun = false) 309 | { 310 | $this->connection->beginTransaction(); 311 | 312 | try { 313 | /** @var AbstractMigration $migrationObject */ 314 | $migrationObject = new $migration['class']($this->metadata, $this->outputWriter); 315 | 316 | if ($migrationObject instanceof ServiceLocatorAwareInterface) { 317 | if (is_null($this->serviceLocator)) { 318 | throw new RuntimeException( 319 | sprintf( 320 | 'Migration class %s requires a ServiceLocator, but there is no instance available.', 321 | get_class($migrationObject) 322 | ) 323 | ); 324 | } 325 | 326 | $migrationObject->setServiceLocator($this->serviceLocator); 327 | } 328 | 329 | if ($migrationObject instanceof AdapterAwareInterface) { 330 | if (is_null($this->adapter)) { 331 | throw new RuntimeException( 332 | sprintf( 333 | 'Migration class %s requires an Adapter, but there is no instance available.', 334 | get_class($migrationObject) 335 | ) 336 | ); 337 | } 338 | 339 | $migrationObject->setDbAdapter($this->adapter); 340 | } 341 | 342 | $this->outputWriter->writeLine( 343 | sprintf( 344 | '%sExecute migration class %s %s', 345 | $dryRun ? '[DRY RUN] ' : '', 346 | $migration['class'], 347 | $down ? 'down' : 'up' 348 | ) 349 | ); 350 | 351 | if (!$dryRun) { 352 | $sqlList = $down ? $migrationObject->getDownSql() : $migrationObject->getUpSql(); 353 | foreach ($sqlList as $sql) { 354 | $this->outputWriter->writeLine("Execute query:\n\n" . $sql); 355 | $this->connection->execute($sql); 356 | } 357 | } 358 | 359 | if ($down) { 360 | $this->migrationVersionTable->delete($migration['version']); 361 | } else { 362 | $this->migrationVersionTable->save($migration['version']); 363 | } 364 | $this->connection->commit(); 365 | } catch (InvalidQueryException $e) { 366 | $this->connection->rollback(); 367 | throw new MigrationException('Could not apply migration because of the invalid query', 0, $e); 368 | } catch (Throwable $e) { 369 | $this->connection->rollback(); 370 | throw new MigrationException('Could not apply migration', 0, $e); 371 | } 372 | } 373 | 374 | /** 375 | * Set service locator 376 | * 377 | * @param ServiceLocatorInterface $serviceLocator 378 | * @return $this 379 | */ 380 | public function setServiceLocator(ServiceLocatorInterface $serviceLocator): Migration 381 | { 382 | $this->serviceLocator = $serviceLocator; 383 | 384 | return $this; 385 | } 386 | 387 | /** 388 | * Get service locator 389 | * 390 | * @return ServiceLocatorInterface 391 | */ 392 | public function getServiceLocator(): ServiceLocatorInterface 393 | { 394 | return $this->serviceLocator; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/MigrationAbstractFactory.php: -------------------------------------------------------------------------------- 1 | getServiceLocator(); 42 | } 43 | 44 | $config = $serviceLocator->get('Config'); 45 | 46 | preg_match(self::FACTORY_PATTERN, $name, $matches) 47 | || preg_match(self::FACTORY_PATTERN, $requestedName, $matches); 48 | 49 | $name = $matches[1]; 50 | 51 | if (!isset($config['migrations'][$name])) { 52 | throw new RuntimeException(sprintf("`%s` does not exist in migrations configuration", $name)); 53 | } 54 | 55 | $migrationConfig = $config['migrations'][$name]; 56 | 57 | $adapterName = $migrationConfig['adapter'] ?: Adapter::class; 58 | /** @var Adapter $adapter */ 59 | $adapter = $serviceLocator->get($adapterName); 60 | 61 | $output = null; 62 | if (isset($migrationConfig['show_log']) && $migrationConfig['show_log']) { 63 | /** @var OutputWriter $console */ 64 | $console = $serviceLocator->get('console'); 65 | $output = new OutputWriter(function ($message) use ($console) { 66 | $console->write($message . "\n"); 67 | }); 68 | } 69 | 70 | /** @var MigrationVersionTable $versionTable */ 71 | $versionTable = $serviceLocator->get('migrations.versiontable.' . $adapterName); 72 | 73 | $migration = new Migration($adapter, $migrationConfig, $versionTable, $output); 74 | 75 | $migration->setServiceLocator($serviceLocator); 76 | 77 | return $migration; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/MigrationException.php: -------------------------------------------------------------------------------- 1 | migrationsDir = $migrationsDir; 21 | $this->migrationNamespace = $migrationsNamespace; 22 | 23 | if (!is_dir($this->migrationsDir)) { 24 | if (!mkdir($this->migrationsDir, 0775)) { 25 | throw new MigrationException(sprintf('Failed to create migrations directory %s', $this->migrationsDir)); 26 | } 27 | } elseif (!is_writable($this->migrationsDir)) { 28 | throw new MigrationException(sprintf('Migrations directory is not writable %s', $this->migrationsDir)); 29 | } 30 | } 31 | 32 | /** 33 | * Generate new migration skeleton class 34 | * 35 | * @return string path to new skeleton class file 36 | * @throws MigrationException 37 | */ 38 | public function generate() 39 | { 40 | $className = 'Version' . date('YmdHis', time()); 41 | $classPath = $this->migrationsDir . DIRECTORY_SEPARATOR . $className . '.php'; 42 | 43 | if (file_exists($classPath)) { 44 | throw new MigrationException(sprintf('Migration %s exists!', $className)); 45 | } 46 | file_put_contents($classPath, $this->getTemplate($className)); 47 | 48 | return $classPath; 49 | } 50 | 51 | /** 52 | * Get migration skeleton class raw text 53 | * 54 | * @param string $className 55 | * @return string 56 | */ 57 | protected function getTemplate($className) 58 | { 59 | return sprintf('addSql(/*Sql instruction*/); 73 | } 74 | 75 | public function down(MetadataInterface $schema) 76 | { 77 | //throw new \RuntimeException(\'No way to go down!\'); 78 | //$this->addSql(/*Sql instruction*/); 79 | } 80 | } 81 | ', $this->migrationNamespace, $className); 82 | } 83 | } 84 | 85 | ?> 86 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/MigrationSkeletonGeneratorAbstractFactory.php: -------------------------------------------------------------------------------- 1 | getServiceLocator(); 41 | } 42 | 43 | preg_match(self::FACTORY_PATTERN, $name, $matches) 44 | || preg_match(self::FACTORY_PATTERN, $requestedName, $matches); 45 | $migration_name = $matches[1]; 46 | 47 | 48 | $config = $serviceLocator->get('Config'); 49 | 50 | if (! isset($config['migrations'][$migration_name])) { 51 | throw new RuntimeException(sprintf("`%s` is not in migrations configuration", $migration_name)); 52 | } 53 | 54 | $migration_config = $config['migrations'][$migration_name]; 55 | 56 | if (! isset($migration_config['dir'])) { 57 | throw new RuntimeException(sprintf("`dir` has not be specified in `%s` migrations configuration", $migration_name)); 58 | } 59 | 60 | if (! isset($migration_config['namespace'])) { 61 | throw new RuntimeException(sprintf("`namespace` has not be specified in `%s` migrations configuration", $migration_name)); 62 | } 63 | 64 | $generator = new MigrationSkeletonGenerator($migration_config['dir'], $migration_config['namespace']); 65 | 66 | return $generator; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Library/OutputWriter.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 16 | } 17 | 18 | /** 19 | * @param string $message message to write 20 | */ 21 | public function write($message) 22 | { 23 | call_user_func($this->closure, $message); 24 | } 25 | 26 | /** 27 | * @param $line 28 | */ 29 | public function writeLine($line) 30 | { 31 | $this->write($line . "\n"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Model/MigrationVersion.php: -------------------------------------------------------------------------------- 1 | {$property} = (isset($data[$property])) ? $data[$property] : null; 22 | } 23 | } 24 | 25 | /** 26 | * @param int $id 27 | */ 28 | public function setId($id) 29 | { 30 | $this->id = $id; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getId() 37 | { 38 | return $this->id; 39 | } 40 | 41 | /** 42 | * @param int $version 43 | */ 44 | public function setVersion($version) 45 | { 46 | $this->version = $version; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getVersion() 53 | { 54 | return $this->version; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Model/MigrationVersionTable.php: -------------------------------------------------------------------------------- 1 | tableGateway = $tableGateway; 16 | } 17 | 18 | public function save($version): int 19 | { 20 | if ($this->tableGateway->getAdapter()->getPlatform()->getName() === 'PostgreSQL') { 21 | return $this->savePg($version); 22 | } 23 | 24 | $this->tableGateway->insert(['version' => $version]); 25 | return $this->tableGateway->lastInsertValue; 26 | } 27 | 28 | protected function savePg($version): int 29 | { 30 | $sql = sprintf('INSERT INTO "%s" ("version") VALUES (?) RETURNING "id"', $this->tableGateway->getTable()); 31 | $stmt = $this->tableGateway->getAdapter()->getDriver()->createStatement($sql); 32 | $result = $stmt->execute([$version]); 33 | return $result->current()["id"]; 34 | } 35 | 36 | public function delete($version) 37 | { 38 | $this->tableGateway->delete(['version' => $version]); 39 | } 40 | 41 | public function applied($version): bool 42 | { 43 | $result = $this->tableGateway->select(['version' => $version]); 44 | return $result->count() > 0; 45 | } 46 | 47 | public function getCurrentVersion(): int 48 | { 49 | $result = $this->tableGateway->select(function (Select $select) { 50 | $select->order('version DESC')->limit(1); 51 | }); 52 | 53 | if (!$result->count()) { 54 | return 0; 55 | } 56 | 57 | return $result->current()->getVersion(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Model/MigrationVersionTableAbstractFactory.php: -------------------------------------------------------------------------------- 1 | get('migrations.versiontablegateway.' . $adapter_name); 45 | $table = new MigrationVersionTable($tableGateway); 46 | return $table; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ZfSimpleMigrations/Model/MigrationVersionTableGatewayAbstractFactory.php: -------------------------------------------------------------------------------- 1 | get($adapter_name); 45 | $resultSetPrototype = new ResultSet(); 46 | $resultSetPrototype->setArrayObjectPrototype(new MigrationVersion()); 47 | return new TableGateway(MigrationVersion::TABLE_NAME, $dbAdapter, null, $resultSetPrototype); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Controller/MigrateControllerFactoryTest.php: -------------------------------------------------------------------------------- 1 | setServiceLocator($this->buildServiceManager()); 30 | 31 | $factory = new MigrateControllerFactory(); 32 | $instance = $factory->createService($controllerManager); 33 | 34 | $this->assertInstanceOf( 35 | MigrateController::class, 36 | $instance, 37 | 'factory should return an instance of ' . MigrateController::class 38 | ); 39 | } 40 | 41 | private function buildServiceManager(): ServiceManager 42 | { 43 | $mvcEvent = new MvcEvent(); 44 | 45 | $migration = $this->prophesize(Migration::class); 46 | $migrationSkeletonGenerator = $this->prophesize(MigrationSkeletonGenerator::class); 47 | $application = $this->prophesize(Application::class); 48 | 49 | $application->getMvcEvent() 50 | ->shouldBeCalled() 51 | ->willReturn($mvcEvent); 52 | 53 | $serviceManager = new ServiceManager(); 54 | $serviceManager->setService( 55 | 'migrations.migration.foo', 56 | $migration->reveal() 57 | ); 58 | $serviceManager->setService( 59 | 'migrations.skeleton-generator.foo', 60 | $migrationSkeletonGenerator->reveal() 61 | ); 62 | $serviceManager->setService( 63 | 'Application', 64 | $application->reveal() 65 | ); 66 | 67 | $mvcEvent->setRouteMatch(new RouteMatch(['name' => 'foo'])); 68 | 69 | return $serviceManager; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Library/ApplyMigration/Version01.php: -------------------------------------------------------------------------------- 1 | addColumn(new Integer('a')); 29 | $this->addSql($createTable->getSqlString($this->adapter->getPlatform())); 30 | } 31 | 32 | public function down(MetadataInterface $schema) 33 | { 34 | $dropTable = new DropTable('test'); 35 | $this->addSql($dropTable->getSqlString($this->adapter->getPlatform())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Library/ApplyMigration/Version02.php: -------------------------------------------------------------------------------- 1 | addColumn(new Integer('a')); 29 | $sql = $createTable->getSqlString($this->adapter->getPlatform()); 30 | 31 | // attempt to drop a non-existing table on second statement 32 | $dropTable = new DropTable('fake'); 33 | $sql .= '; ' . $dropTable->getSqlString($this->adapter->getPlatform()); 34 | 35 | // execute multi-statement sql 36 | $this->addSql($sql); 37 | } 38 | 39 | public function down(MetadataInterface $schema) 40 | { 41 | // clean up result of first statement 42 | $dropTable = new DropTable('test'); 43 | $this->addSql($dropTable->getSqlString($this->adapter->getPlatform())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Library/MigrationAbstractFactoryTest.php: -------------------------------------------------------------------------------- 1 | buildServiceManager(false); 26 | 27 | $factory = new MigrationAbstractFactory(); 28 | $this->assertTrue( 29 | $factory->canCreateServiceWithName( 30 | $serviceManager, 31 | 'migrations.migration.foo', 32 | 'asdf' 33 | ), 34 | "should indicate it provides service for \$name" 35 | ); 36 | 37 | $this->assertTrue( 38 | $factory->canCreateServiceWithName( 39 | $serviceManager, 40 | 'asdf', 41 | 'migrations.migration.foo' 42 | ), 43 | "should indicate it provides service for requestedName" 44 | ); 45 | 46 | $this->assertFalse( 47 | $factory->canCreateServiceWithName( 48 | $serviceManager, 49 | 'asdf', 50 | 'asdf' 51 | ), 52 | "should indicate it does not provide service for name or requestedName" 53 | ); 54 | } 55 | 56 | public function testItReturnsAMigration() 57 | { 58 | $serviceManager = $this->buildServiceManager(true); 59 | 60 | $controllerManager = new ControllerManager(); 61 | $controllerManager->setServiceLocator($serviceManager); 62 | 63 | $factory = new MigrationAbstractFactory(); 64 | $instance = $factory->createServiceWithName( 65 | $controllerManager, 66 | 'migrations.migration.foo', 67 | 'asdf' 68 | ); 69 | $this->assertInstanceOf( 70 | Migration::class, 71 | $instance, 72 | "factory should return an instance of " . Migration::class . " when asked by name" 73 | ); 74 | 75 | $instance2 = $factory->createServiceWithName( 76 | $serviceManager, 77 | 'asdf', 78 | 'migrations.migration.foo' 79 | ); 80 | $this->assertInstanceOf( 81 | Migration::class, 82 | $instance2, 83 | "factory should return an instance of " . Migration::class . " when asked by requestedName" 84 | ); 85 | } 86 | 87 | public function testItInjectsAnOutputWriter() 88 | { 89 | $serviceManager = $this->buildServiceManager(true); 90 | 91 | $serviceManager->setService('Config', [ 92 | 'migrations' => [ 93 | 'foo' => [ 94 | 'dir' => __DIR__, 95 | 'namespace' => 'Foo', 96 | 'adapter' => 'fooDb', 97 | 'show_log' => true 98 | ] 99 | ] 100 | ]); 101 | $factory = new MigrationAbstractFactory(); 102 | $instance = $factory->createServiceWithName( 103 | $serviceManager, 104 | 'migrations.migration.foo', 105 | 'asdf' 106 | ); 107 | 108 | $this->assertInstanceOf( 109 | OutputWriter::class, 110 | $instance->getOutputWriter(), 111 | "factory should inject a " . OutputWriter::class 112 | ); 113 | } 114 | 115 | public function testItComplainsIfNamedMigrationNotConfigured() 116 | { 117 | $serviceManager = $this->buildServiceManager(false); 118 | $this->expectException(RuntimeException::class); 119 | 120 | $factory = new MigrationAbstractFactory(); 121 | $factory->createServiceWithName( 122 | $serviceManager, 123 | 'migrations.migration.bar', 124 | 'asdf' 125 | ); 126 | } 127 | 128 | private function buildServiceManager(bool $expectVersionTableQuery): ServiceManager 129 | { 130 | $migrationVersionTable = $this->prophesize(MigrationVersionTable::class); 131 | $console = $this->prophesize(Console::class); 132 | $adapter = $this->prophesize(Adapter::class); 133 | $driver = $this->prophesize(Pdo::class); 134 | $connection = $this->prophesize(Connection::class); 135 | 136 | if ($expectVersionTableQuery) { 137 | $sqlite = new Sqlite(); 138 | 139 | $driver->getConnection() 140 | ->shouldBeCalled() 141 | ->willReturn($connection->reveal()); 142 | 143 | $adapter->getCurrentSchema() 144 | ->shouldBeCalled() 145 | ->willReturn('fooDb'); 146 | $adapter->getPlatform() 147 | ->shouldBeCalled() 148 | ->willReturn($sqlite); 149 | $adapter->getDriver() 150 | ->shouldBeCalled() 151 | ->willReturn($driver->reveal()); 152 | $adapter->query(Argument::containingString('CREATE TABLE "migration_version"'), Adapter::QUERY_MODE_EXECUTE) 153 | ->shouldBeCalled(); 154 | } 155 | 156 | $serviceManager = new ServiceManager(new Config(['allow_override' => true])); 157 | $serviceManager->setService('Config', [ 158 | 'migrations' => [ 159 | 'foo' => [ 160 | 'dir' => __DIR__, 161 | 'namespace' => 'Foo', 162 | 'adapter' => 'fooDb' 163 | ] 164 | ] 165 | ]); 166 | $serviceManager->setService( 167 | 'migrations.versiontable.fooDb', 168 | $migrationVersionTable->reveal() 169 | ); 170 | $serviceManager->setService( 171 | 'console', 172 | $console->reveal() 173 | ); 174 | $serviceManager->setService( 175 | 'fooDb', 176 | $adapter->reveal() 177 | ); 178 | 179 | return $serviceManager; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Library/MigrationSkeletonGeneratorAbstractFactoryTest.php: -------------------------------------------------------------------------------- 1 | buildServiceManager(); 19 | 20 | $factory = new MigrationSkeletonGeneratorAbstractFactory(); 21 | $this->assertTrue( 22 | $factory->canCreateServiceWithName( 23 | $serviceManager, 24 | 'migrations.skeletongenerator.foo', 25 | 'asdf' 26 | ), 27 | "should indicate it provides service for \$name" 28 | ); 29 | 30 | $this->assertTrue( 31 | $factory->canCreateServiceWithName( 32 | $serviceManager, 33 | 'asdf', 34 | 'migrations.skeletongenerator.foo' 35 | ), 36 | "should indicate it provides service for \$requestedName" 37 | ); 38 | 39 | $this->assertFalse( 40 | $factory->canCreateServiceWithName( 41 | $serviceManager, 42 | 'asdf', 43 | 'asdf' 44 | ), 45 | "should indicate it does not provide service for \$name or \$requestedName" 46 | ); 47 | } 48 | 49 | public function testItReturnsASkeletonGenerator() 50 | { 51 | $serviceManager = $this->buildServiceManager(); 52 | 53 | $controllerManager = new ControllerManager(); 54 | $controllerManager->setServiceLocator($serviceManager); 55 | 56 | $factory = new MigrationSkeletonGeneratorAbstractFactory(); 57 | $instance = $factory->createServiceWithName( 58 | $controllerManager, 59 | 'migrations.skeletongenerator.foo', 60 | 'asdf' 61 | ); 62 | $this->assertInstanceOf( 63 | MigrationSkeletonGenerator::class, 64 | $instance, 65 | "factory should return an instance of " . MigrationSkeletonGenerator::class . " when asked by name" 66 | ); 67 | 68 | $instance2 = $factory->createServiceWithName( 69 | $serviceManager, 70 | 'asdf', 71 | 'migrations.skeletongenerator.foo' 72 | ); 73 | $this->assertInstanceOf( 74 | MigrationSkeletonGenerator::class, 75 | $instance2, 76 | "factory should return an instance of " . MigrationSkeletonGenerator::class . " when asked by requestedName" 77 | ); 78 | } 79 | 80 | public function testItComplainsIfNamedMigrationIsNotConfigured() 81 | { 82 | $serviceManager = $this->buildServiceManager(); 83 | 84 | $this->expectException(RuntimeException::class); 85 | 86 | $factory = new MigrationSkeletonGeneratorAbstractFactory(); 87 | $factory->createServiceWithName( 88 | $serviceManager, 89 | 'migrations.skeletongenerator.bar', 90 | 'asdf' 91 | ); 92 | } 93 | 94 | public function testItComplainsIfDirIsNotConfigured() 95 | { 96 | $serviceManager = $this->buildServiceManager(); 97 | 98 | $serviceManager->setService('Config', [ 99 | 'migrations' => [ 100 | 'bar' => [ 101 | 'namespace' => 'Bar' 102 | ] 103 | ] 104 | ]); 105 | 106 | $this->expectException(RuntimeException::class); 107 | 108 | $factory = new MigrationSkeletonGeneratorAbstractFactory(); 109 | $factory->createServiceWithName( 110 | $serviceManager, 111 | 'migrations.skeletongenerator.bar', 112 | 'asdf' 113 | ); 114 | } 115 | 116 | public function testItComplainsIfNamespaceIsNotConfigured() 117 | { 118 | $serviceManager = $this->buildServiceManager(); 119 | 120 | $serviceManager->setService('Config', [ 121 | 'migrations' => [ 122 | 'bar' => [ 123 | 'dir' => __DIR__ 124 | ] 125 | ] 126 | ]); 127 | 128 | $this->expectException(RuntimeException::class); 129 | 130 | $factory = new MigrationSkeletonGeneratorAbstractFactory(); 131 | $factory->createServiceWithName( 132 | $serviceManager, 133 | 'migrations.skeletongenerator.bar', 134 | 'asdf' 135 | ); 136 | } 137 | 138 | private function buildServiceManager(): ServiceManager 139 | { 140 | $migrationSkeletonGenerator = $this->prophesize(MigrationSkeletonGenerator::class); 141 | 142 | $serviceManager = new ServiceManager(new Config(['allow_override' => true])); 143 | $serviceManager->setService('Config', [ 144 | 'migrations' => [ 145 | 'foo' => [ 146 | 'dir' => __DIR__, 147 | 'namespace' => 'Foo' 148 | ] 149 | ] 150 | ]); 151 | $serviceManager->setService( 152 | 'migrations.skeletongenerator.foo', 153 | $migrationSkeletonGenerator->reveal() 154 | ); 155 | 156 | return $serviceManager; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Library/MigrationTest.php: -------------------------------------------------------------------------------- 1 | getenv('DB_TYPE'), 36 | 'database' => getenv('DB_NAME'), 37 | 'username' => getenv('DB_USERNAME'), 38 | 'password' => getenv('DB_PASSWORD'), 39 | 'hostname' => getenv('DB_HOST'), 40 | 'port' => getenv('DB_PORT'), 41 | 'options' => [ 42 | 'buffer_results' => true, 43 | ], 44 | ]; 45 | $config = [ 46 | 'dir' => __DIR__ . '/ApplyMigration', 47 | 'namespace' => 'ZfSimpleMigrations\\Library\\ApplyMigration' 48 | ]; 49 | 50 | $this->adapter = new Adapter($driverConfig); 51 | 52 | $metadata = new Metadata($this->adapter); 53 | $tableNames = $metadata->getTableNames(); 54 | 55 | $dropIfExists = [ 56 | 'test', 57 | MigrationVersion::TABLE_NAME 58 | ]; 59 | foreach ($dropIfExists as $table) { 60 | if (in_array($table, $tableNames)) { 61 | // ensure db is in expected state 62 | $drop = new DropTable($table); 63 | $this->adapter->query($drop->getSqlString($this->adapter->getPlatform())); 64 | } 65 | } 66 | 67 | /** @var ArrayObject $version */ 68 | $version = new MigrationVersion(); 69 | $resultSetPrototype = new ResultSet(); 70 | $resultSetPrototype->setArrayObjectPrototype($version); 71 | 72 | $gateway = new TableGateway(MigrationVersion::TABLE_NAME, $this->adapter, null, $resultSetPrototype); 73 | $table = new MigrationVersionTable($gateway); 74 | 75 | $this->migration = new Migration($this->adapter, $config, $table); 76 | } 77 | 78 | public function testApplyMigration() 79 | { 80 | $this->migration->migrate('01'); 81 | 82 | $metadata = new Metadata($this->adapter); 83 | $this->assertContains('test', $metadata->getTableNames(), 'up should create table'); 84 | 85 | $this->migration->migrate('01', true, true); 86 | 87 | $metadata = new Metadata($this->adapter); 88 | $this->assertNotContains('test', $metadata->getTableNames(), 'down should drop table'); 89 | } 90 | 91 | /** 92 | * @expectedException \ZfSimpleMigrations\Library\MigrationException 93 | */ 94 | public function testMultiStatementErrorDetection() 95 | { 96 | $this->markTestSkipped( 97 | 'need to implement driver specific features & test if this driver supports multi-row functionality' 98 | ); 99 | 100 | try { 101 | $this->migration->migrate('02'); 102 | } catch (\Exception $e) { 103 | $this->migration->migrate('02', true, true); 104 | $this->assertEquals('ZfSimpleMigrations\Library\MigrationException', get_class($e)); 105 | return; 106 | } 107 | 108 | $this->fail(sprintf('expected exception %s', '\ZfSimpleMigrations\Library\MigrationException')); 109 | } 110 | 111 | public function testMigrationInitializesMigrationTable() 112 | { 113 | // because Migration was instantiated in setup, the version table should exist 114 | $metadata = new Metadata($this->adapter); 115 | $tableNames = $metadata->getTableNames(); 116 | $this->assertContains(MigrationVersion::TABLE_NAME, $tableNames); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Model/MigrationVersionTableAbstractFactoryTest.php: -------------------------------------------------------------------------------- 1 | buildServiceManager(); 24 | 25 | $factory = new MigrationVersionTableAbstractFactory(); 26 | $this->assertTrue( 27 | $factory->canCreateServiceWithName( 28 | $serviceManager, 29 | 'migrations.versiontable.foo', 30 | 'asdf' 31 | ), 32 | "should indicate it provides service for \$name" 33 | ); 34 | 35 | $this->assertTrue( 36 | $factory->canCreateServiceWithName( 37 | $serviceManager, 38 | 'asdf', 39 | 'migrations.versiontable.foo' 40 | ), 41 | "should indicate it provides service for \$requestedName" 42 | ); 43 | 44 | $this->assertFalse( 45 | $factory->canCreateServiceWithName( 46 | $serviceManager, 47 | 'asdf', 48 | 'asdf' 49 | ), 50 | "should indicate it does not provide service for \$name or \$requestedName" 51 | ); 52 | } 53 | 54 | public function testItReturnsAMigrationVersionTable() 55 | { 56 | $serviceManager = $this->buildServiceManager(); 57 | 58 | $factory = new MigrationVersionTableAbstractFactory(); 59 | $instance = $factory->createServiceWithName($serviceManager, 'migrations.versiontable.foo', 'asdf'); 60 | $this->assertInstanceOf( 61 | MigrationVersionTable::class, 62 | $instance, 63 | "factory should return an instance of " . MigrationVersionTable::class . " when asked by \$name" 64 | ); 65 | 66 | $instance2 = $factory->createServiceWithName($serviceManager, 'asdf', 'migrations.versiontable.foo'); 67 | $this->assertInstanceOf( 68 | MigrationVersionTable::class, 69 | $instance2, 70 | "factory should return an instance of " . MigrationVersionTable::class . " when asked by \$requestedName" 71 | ); 72 | } 73 | 74 | private function buildServiceManager(): ServiceManager 75 | { 76 | $tableGateway = $this->prophesize(TableGateway::class); 77 | 78 | $serviceManager = new ServiceManager(); 79 | $serviceManager->setService( 80 | 'migrations.versiontablegateway.foo', 81 | $tableGateway->reveal() 82 | ); 83 | 84 | return $serviceManager; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/ZfSimpleMigrations/Model/MigrationVersionTableGatewayAbstractFactoryTest.php: -------------------------------------------------------------------------------- 1 | buildServiceManager(); 18 | 19 | $factory = new MigrationVersionTableGatewayAbstractFactory(); 20 | $this->assertTrue( 21 | $factory->canCreateServiceWithName( 22 | $serviceManager, 23 | 'migrations.versiontablegateway.foo', 24 | 'asdf' 25 | ), 26 | "should indicate it provides service for \$name" 27 | ); 28 | 29 | $this->assertTrue( 30 | $factory->canCreateServiceWithName( 31 | $serviceManager, 32 | 'asdf', 33 | 'migrations.versiontablegateway.foo' 34 | ), 35 | "should indicate it provides service for \$requestedName" 36 | ); 37 | 38 | $this->assertFalse( 39 | $factory->canCreateServiceWithName( 40 | $serviceManager, 41 | 'asdf', 42 | 'asdf' 43 | ), 44 | "should indicate it does not provide service for \$name or \$requestedName" 45 | ); 46 | } 47 | 48 | public function testItReturnsATableGateway() 49 | { 50 | $serviceManager = $this->buildServiceManager(); 51 | 52 | $factory = new MigrationVersionTableGatewayAbstractFactory(); 53 | $instance = $factory->createServiceWithName($serviceManager, 'migrations.versiontablegateway.foo', 'asdf'); 54 | $this->assertInstanceOf( 55 | TableGateway::class, 56 | $instance, 57 | "factory should return an instance of " . TableGateway::class . " when asked by \$name" 58 | ); 59 | 60 | $instance2 = $factory->createServiceWithName($serviceManager, 'asdf', 'migrations.versiontablegateway.foo'); 61 | $this->assertInstanceOf( 62 | TableGateway::class, 63 | $instance2, 64 | "factory should return an instance of " . TableGateway::class . " when asked by \$requestedName" 65 | ); 66 | } 67 | 68 | private function buildServiceManager(): ServiceManager 69 | { 70 | $adapter = $this->prophesize(Adapter::class); 71 | 72 | $serviceManager = new ServiceManager(); 73 | $serviceManager->setService( 74 | 'foo', 75 | $adapter->reveal() 76 | ); 77 | 78 | return $serviceManager; 79 | } 80 | } 81 | --------------------------------------------------------------------------------