├── .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 |
--------------------------------------------------------------------------------