diff --git a/.github/workflows/ci-mssql.yml b/.github/workflows/ci-mssql.yml
new file mode 100644
index 0000000..0401c56
--- /dev/null
+++ b/.github/workflows/ci-mssql.yml
@@ -0,0 +1,86 @@
+on:
+ - pull_request
+ - push
+
+name: ci-mssql
+
+jobs:
+ tests:
+ name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }}
+
+ env:
+ key: cache
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - php: '7.2'
+ extensions: pdo, pdo_sqlsrv-5.8.1
+ mssql: 'server:2017-latest'
+ - php: '7.3'
+ extensions: pdo, pdo_sqlsrv-5.8.1
+ mssql: 'server:2017-latest'
+ - php: '7.4'
+ extensions: pdo, pdo_sqlsrv
+ mssql: 'server:2017-latest'
+ - php: '7.4'
+ extensions: pdo, pdo_sqlsrv
+ mssql: 'server:2019-latest'
+ - php: '8.0'
+ extensions: pdo, pdo_sqlsrv
+ mssql: 'server:2017-latest'
+ - php: '8.0'
+ extensions: pdo, pdo_sqlsrv
+ mssql: 'server:2019-latest'
+
+ services:
+ mssql:
+ image: mcr.microsoft.com/mssql/${{ matrix.mssql }}
+ env:
+ SA_PASSWORD: SSpaSS__1
+ ACCEPT_EULA: Y
+ MSSQL_PID: Developer
+ ports:
+ - 11433:1433
+ options: --name=mssql --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'SSpaSS__1' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2.3.4
+
+ - name: Install PHP with extensions
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: ${{ matrix.extensions }}
+ ini-values: date.timezone='UTC'
+ tools: composer:v2, pecl
+
+ - name: Determine composer cache directory on Linux
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v2
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php }}-composer-
+
+ - name: Update composer
+ run: composer self-update
+
+ - name: Install dependencies with composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Install dependencies with composer php 8.0
+ if: matrix.php == '8.0'
+ run: composer update --ignore-platform-reqs --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run tests with phpunit without coverage
+ env:
+ DB: sqlserver
+ run: vendor/bin/phpunit --group driver-sqlserver --colors=always
diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml
new file mode 100644
index 0000000..be40e2a
--- /dev/null
+++ b/.github/workflows/ci-mysql.yml
@@ -0,0 +1,89 @@
+on:
+ - pull_request
+ - push
+
+name: ci-mysql
+
+jobs:
+ tests:
+ name: PHP ${{ matrix.php-version }}-mysql-${{ matrix.mysql-version }}
+ env:
+ extensions: curl, intl, pdo, pdo_mysql
+ key: cache-v1
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php-version:
+ - "7.2"
+ - "7.3"
+ - "7.4"
+ - "8.0"
+
+ mysql-version:
+ - "5.7"
+ - "8.0"
+
+ services:
+ mysql:
+ image: mysql:${{ matrix.mysql-version }}
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: spiral
+ MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
+ ports:
+ - 13306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup cache environment
+ id: cache-env
+ uses: shivammathur/cache-extensions@v1
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: ${{ env.extensions }}
+ key: ${{ env.key }}
+
+ - name: Cache extensions
+ uses: actions/cache@v1
+ with:
+ path: ${{ steps.cache-env.outputs.dir }}
+ key: ${{ steps.cache-env.outputs.key }}
+ restore-keys: ${{ steps.cache-env.outputs.key }}
+
+ - name: Install PHP with extensions
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: ${{ env.extensions }}
+ ini-values: date.timezone='UTC'
+ coverage: pcov
+
+ - name: Determine composer cache directory
+ if: matrix.os == 'ubuntu-latest'
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v1
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
+
+ - name: Install dependencies with composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run mysql tests with phpunit
+ env:
+ DB: mysql
+ MYSQL: ${{ matrix.mysql-version }}
+ run: vendor/bin/phpunit --group driver-mysql --colors=always
diff --git a/.github/workflows/ci-pgsql.yml b/.github/workflows/ci-pgsql.yml
new file mode 100644
index 0000000..4c31a6d
--- /dev/null
+++ b/.github/workflows/ci-pgsql.yml
@@ -0,0 +1,90 @@
+on:
+ - pull_request
+ - push
+
+name: ci-pgsql
+
+jobs:
+ tests:
+ name: PHP ${{ matrix.php-version }}-pgsql-${{ matrix.pgsql-version }}
+ env:
+ extensions: curl, intl, pdo, pdo_pgsql
+ key: cache-v1
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ php-version:
+ - "7.2"
+ - "7.3"
+ - "7.4"
+ - "8.0"
+
+ pgsql-version:
+ - "10"
+ - "11"
+ - "12"
+ - "13"
+
+ services:
+ postgres:
+ image: postgres:${{ matrix.pgsql-version }}
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: spiral
+ ports:
+ - 15432:5432
+ options: --name=postgres --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup cache environment
+ id: cache-env
+ uses: shivammathur/cache-extensions@v1
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: ${{ env.extensions }}
+ key: ${{ env.key }}
+
+ - name: Cache extensions
+ uses: actions/cache@v1
+ with:
+ path: ${{ steps.cache-env.outputs.dir }}
+ key: ${{ steps.cache-env.outputs.key }}
+ restore-keys: ${{ steps.cache-env.outputs.key }}
+
+ - name: Install PHP with extensions
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: ${{ env.extensions }}
+ ini-values: date.timezone='UTC'
+ coverage: pcov
+
+ - name: Determine composer cache directory
+ if: matrix.os == 'ubuntu-latest'
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v1
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
+
+ - name: Install dependencies with composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run pgsql tests with phpunit
+ env:
+ DB: postgres
+ POSTGRES: ${{ matrix.pgsql-version }}
+ run: vendor/bin/phpunit --group driver-postgres --colors=always
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3eb2678..a3a8b7d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,11 +24,11 @@ jobs:
run: vendor/bin/spiral-cs check src tests
test:
needs: lint
- name: Test PHP ${{ matrix.php-versions }}
+ name: Test PHP ${{ matrix.php-versions }} with Code Coverage
runs-on: ubuntu-latest
strategy:
matrix:
- php-versions: ['7.2', '7.3', '7.4']
+ php-versions: ['7.2', '7.3', '7.4', '8.0']
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -38,18 +38,12 @@ jobs:
docker-compose up -d
cd ..
- name: Setup PHP ${{ matrix.php-versions }}
- uses: shivammathur/setup-php@v1
+ uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: pcov
tools: pecl
- extensions: mbstring, pdo, pdo_sqlsrv
- - name: Install MS SQL Server deps
- run: |
- bash ./tests/install-sqlsrv.sh
- sudo sed -i.bak '/^extension="pdo_sqlsrv.so"/d' /etc/php/${{ matrix.php-versions }}/cli/php.ini
- sudo bash -c 'printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/${{ matrix.php-versions }}/mods-available/pdo_sqlsrv.ini'
- sudo phpenmod -s cli -v ${{ matrix.php-versions }} pdo_sqlsrv
+ extensions: mbstring, pdo, pdo_sqlite, pdo_pgsql, pdo_sqlsrv, pdo_mysql
- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
@@ -65,77 +59,29 @@ jobs:
run: |
vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage to Codecov
+ continue-on-error: true # if is fork
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
- test_postgres:
+
+ sqlite:
needs: lint
- name: Test PostgreSQL ${{ matrix.configs.postgres-version }}
+ name: SQLite PHP ${{ matrix.php-versions }}
runs-on: ubuntu-latest
strategy:
matrix:
- configs: [
- {php-version: 7.2, postgres-version: 9.6},
- {php-version: 7.3, postgres-version: 10},
- {php-version: 7.3, postgres-version: 11}
- ]
- services:
- postgres:
- image: postgres:${{ matrix.configs.postgres-version }}
- ports:
- - 5432:5432
- env:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: spiral
- options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
+ php-versions: ['7.2', '7.3', '7.4', '8.0']
steps:
- name: Checkout
uses: actions/checkout@v2
- - name: Setup PHP ${{ matrix.configs.php-version }}
- run: sudo update-alternatives --set php /usr/bin/php${{ matrix.configs.php-version }}
- - name: Get Composer Cache Directory
- id: composer-cache
- run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- - name: Restore Composer Cache
- uses: actions/cache@v1
+ - name: Setup PHP ${{ matrix.php-versions }}
+ uses: shivammathur/setup-php@v1
with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Install Dependencies
- run: composer install --no-interaction --prefer-dist
- - name: Execute Tests
- env:
- DB: postgres
- POSTGRES: ${{ matrix.configs.postgres-version }}
- run: |
- vendor/bin/phpunit tests/Migrations/Postgres
- test_mariadb:
- needs: lint
- name: Test MariaDB ${{ matrix.configs.mariadb-version }}
- runs-on: ubuntu-latest
- strategy:
- matrix:
- configs: [
-# {php-version: 7.2, mariadb-version: 10.2},
- {php-version: 7.3, mariadb-version: 10.4}
- ]
- services:
- mariadb:
- image: mariadb:${{ matrix.configs.mariadb-version }}
- ports:
- - 23306:3306
- env:
- MYSQL_ROOT_PASSWORD: root
- MYSQL_DATABASE: spiral
- options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Setup PHP ${{ matrix.configs.php-version }}
- run: sudo update-alternatives --set php /usr/bin/php${{ matrix.configs.php-version }}
+ php-version: ${{ matrix.php-versions }}
+ coverage: pcov
+ tools: pecl
+ extensions: mbstring, pdo, pdo_sqlite
- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
@@ -149,7 +95,6 @@ jobs:
run: composer install --no-interaction --prefer-dist
- name: Execute Tests
env:
- DB: mariadb
- MARIADB: ${{ matrix.configs.mariadb-version }}
+ DB: sqlite
run: |
- vendor/bin/phpunit tests/Migrations/MySQL
+ vendor/bin/phpunit --group driver-sqlite --colors=always
diff --git a/.gitignore b/.gitignore
index 7182f88..182a270 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ build/logs/*
build/
*.db
clover.xml
-clover.json
\ No newline at end of file
+clover.json
+.phpunit.result.cache
diff --git a/composer.json b/composer.json
index ec2f14c..ee32768 100644
--- a/composer.json
+++ b/composer.json
@@ -1,36 +1,47 @@
{
- "name": "spiral/migrations",
- "type": "library",
- "description": "Database migrations, migration scaffolding",
- "license": "MIT",
- "authors": [
- {
- "name": "Anton Titov / Wolfy-J",
- "email": "wolfy.jd@gmail.com"
- }
- ],
- "require": {
- "php": ">=7.2",
- "spiral/core": "^2.7",
- "spiral/database": "^2.7",
- "spiral/files": "^2.7",
- "spiral/tokenizer": "^2.7",
- "spiral/reactor": "^2.7"
- },
- "require-dev": {
- "phpunit/phpunit": "~8.0",
- "mockery/mockery": "^1.1",
- "spiral/code-style": "^1.0"
- },
- "autoload": {
- "psr-4": {
- "Spiral\\Migrations\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Spiral\\Migrations\\Tests\\": "tests/Migrations/",
- "Spiral\\Migrations\\Fixtures\\": "tests/Fixtures/"
- }
- }
+ "name": "spiral/migrations",
+ "type": "library",
+ "description": "Database migrations, migration scaffolding",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Anton Titov / Wolfy-J",
+ "email": "wolfy.jd@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.2",
+ "spiral/core": "^2.7",
+ "spiral/database": "^2.7",
+ "spiral/files": "^2.7",
+ "spiral/tokenizer": "^2.7",
+ "spiral/reactor": "^2.7"
+ },
+ "autoload": {
+ "psr-4": {
+ "Spiral\\Migrations\\": "src"
+ }
+ },
+ "require-dev": {
+ "vimeo/psalm": "^4.9",
+ "phpunit/phpunit": "^8.5|^9.0",
+ "mockery/mockery": "^1.3",
+ "spiral/code-style": "^1.0"
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Spiral\\Migrations\\Tests\\": "tests/Migrations",
+ "Spiral\\Migrations\\Fixtures\\": "tests/Fixtures"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3.x-dev"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..7c0333d
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Atomizer/Atomizer.php b/src/Atomizer/Atomizer.php
index 72cf7fe..b84e056 100644
--- a/src/Atomizer/Atomizer.php
+++ b/src/Atomizer/Atomizer.php
@@ -16,14 +16,14 @@
use Spiral\Reactor\Partial\Source;
/**
- * Atomizer provides ability to convert given AbstractTables and their changes into set of
- * migration commands.
+ * Atomizer provides ability to convert given AbstractTables and their changes
+ * into set of migration commands.
*/
final class Atomizer
{
-
/** @var AbstractTable[] */
protected $tables = [];
+
/** @var RendererInterface */
private $renderer;
diff --git a/src/Config/MigrationConfig.php b/src/Config/MigrationConfig.php
index 2e0151d..dc8cf13 100644
--- a/src/Config/MigrationConfig.php
+++ b/src/Config/MigrationConfig.php
@@ -15,8 +15,20 @@
final class MigrationConfig extends InjectableConfig
{
+ /**
+ * @internal This is an internal config section name. Please, do not use
+ * this constant.
+ */
public const CONFIG = 'migration';
+ /**
+ * @param array{directory?: string|null, table?: string|null, safe?: bool|null} $config
+ */
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+ }
+
/**
* Migrations directory.
*
diff --git a/src/FileRepository.php b/src/FileRepository.php
index ac8c4a3..cfd481a 100644
--- a/src/FileRepository.php
+++ b/src/FileRepository.php
@@ -17,6 +17,7 @@
use Spiral\Files\FilesInterface;
use Spiral\Migrations\Config\MigrationConfig;
use Spiral\Migrations\Exception\RepositoryException;
+use Spiral\Migrations\Migration\State;
use Spiral\Tokenizer\Reflection\ReflectionFile;
/**
diff --git a/src/Migration.php b/src/Migration.php
index 7830cb8..03a4eb6 100644
--- a/src/Migration.php
+++ b/src/Migration.php
@@ -14,6 +14,9 @@
use Spiral\Database\Database;
use Spiral\Database\DatabaseInterface;
use Spiral\Migrations\Exception\MigrationException;
+use Spiral\Migrations\Migration\DefinitionInterface;
+use Spiral\Migrations\Migration\ProvidesSyncStateInterface;
+use Spiral\Migrations\Migration\State;
/**
* Simple migration class with shortcut for database and blueprint instances.
@@ -30,7 +33,7 @@ abstract class Migration implements MigrationInterface
private $capsule;
/**
- * {@inheritdoc}
+ * {@inheritDoc}
*/
public function getDatabase(): ?string
{
@@ -40,7 +43,7 @@ public function getDatabase(): ?string
/**
* {@inheritdoc}
*/
- public function withCapsule(CapsuleInterface $capsule): MigrationInterface
+ public function withCapsule(CapsuleInterface $capsule): DefinitionInterface
{
$migration = clone $this;
$migration->capsule = $capsule;
@@ -51,7 +54,7 @@ public function withCapsule(CapsuleInterface $capsule): MigrationInterface
/**
* {@inheritdoc}
*/
- public function withState(State $state): MigrationInterface
+ public function withState(State $state): ProvidesSyncStateInterface
{
$migration = clone $this;
$migration->state = $state;
diff --git a/src/Migration/DefinitionInterface.php b/src/Migration/DefinitionInterface.php
new file mode 100644
index 0000000..e9d514b
--- /dev/null
+++ b/src/Migration/DefinitionInterface.php
@@ -0,0 +1,50 @@
+name = $name;
+ $this->status = $status;
+ $this->createdAt = $createdAt;
+ $this->executedAt = $executedAt;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Migration status.
+ *
+ * @return int
+ */
+ public function getStatus(): int
+ {
+ return $this->status;
+ }
+
+ /**
+ * Get migration creation time.
+ *
+ * @return \DateTimeInterface
+ */
+ public function getTimeCreated(): \DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * Get migration execution time if any.
+ *
+ * @return \DateTimeInterface|null
+ */
+ public function getTimeExecuted(): ?\DateTimeInterface
+ {
+ return $this->executedAt;
+ }
+
+ /**
+ * @param StatusEnum $status
+ * @param \DateTimeInterface|null $executedAt
+ * @return State
+ */
+ public function withStatus(int $status, \DateTimeInterface $executedAt = null): State
+ {
+ $state = clone $this;
+ $state->status = $status;
+ $state->executedAt = $executedAt;
+
+ return $state;
+ }
+}
diff --git a/src/Migration/Status.php b/src/Migration/Status.php
new file mode 100644
index 0000000..2940e24
--- /dev/null
+++ b/src/Migration/Status.php
@@ -0,0 +1,42 @@
+dbal->getDatabases() as $db) {
+ $databases = $this->getDatabases();
+
+ foreach ($databases as $db) {
if (!$this->checkMigrationTableStructure($db)) {
return false;
}
}
- return !$this->isRestoreMigrationDataRequired();
+ return !$this->isRestoreMigrationDataRequired($databases);
}
/**
- * Configure all related databases with migration table.
+ * {@inheritDoc}
*/
public function configure(): void
{
@@ -93,39 +89,60 @@ public function configure(): void
return;
}
- foreach ($this->dbal->getDatabases() as $db) {
- $schema = $db->table($this->config->getTable())->getSchema();
-
- // Schema update will automatically sync all needed data
- $schema->primary('id');
- $schema->string('migration', 191)->nullable(false);
- $schema->datetime('time_executed')->datetime();
- $schema->datetime('created_at')->datetime();
- $schema->index(['migration', 'created_at'])
- ->unique(true);
+ $databases = $this->getDatabases();
- if ($schema->hasIndex(['migration'])) {
- $schema->dropIndex(['migration']);
- }
-
- $schema->save();
+ foreach ($databases as $database) {
+ $this->createMigrationTable($database);
}
- if ($this->isRestoreMigrationDataRequired()) {
+ if ($this->isRestoreMigrationDataRequired($databases)) {
$this->restoreMigrationData();
}
}
+ /**
+ * Get all databases for which there are migrations.
+ *
+ * @return array
+ */
+ private function getDatabases(): array
+ {
+ $result = [];
+
+ foreach ($this->repository->getMigrations() as $migration) {
+ $database = $this->dbal->database($migration->getDatabase());
+
+ if (! isset($result[$database->getName()])) {
+ $result[$database->getName()] = $database;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Create migration table inside given database
+ *
+ * @param Database $database
+ */
+ private function createMigrationTable(Database $database): void
+ {
+ $table = new MigrationsTable($database, $this->config->getTable());
+ $table->actualize();
+ }
+
/**
* Get every available migration with valid meta information.
*
* @return MigrationInterface[]
+ * @throws \Exception
*/
public function getMigrations(): array
{
$result = [];
+
foreach ($this->repository->getMigrations() as $migration) {
- //Populating migration state and execution time (if any)
+ // Populating migration state and execution time (if any)
$result[] = $migration->withState($this->resolveState($migration));
}
@@ -133,22 +150,18 @@ public function getMigrations(): array
}
/**
- * Execute one migration and return it's instance.
- *
- * @param CapsuleInterface $capsule
- *
- * @return null|MigrationInterface
- *
- * @throws MigrationException
+ * {@inheritDoc}
*/
public function run(CapsuleInterface $capsule = null): ?MigrationInterface
{
if (!$this->isConfigured()) {
- throw new MigrationException('Unable to run migration, Migrator not configured');
+ $this->configure();
}
foreach ($this->getMigrations() as $migration) {
- if ($migration->getState()->getStatus() !== State::STATUS_PENDING) {
+ $state = $migration->getState();
+
+ if ($state->getStatus() !== Status::STATUS_PENDING) {
continue;
}
@@ -162,7 +175,7 @@ static function () use ($migration, $capsule): void {
$this->migrationTable($migration->getDatabase())->insertOne(
[
- 'migration' => $migration->getState()->getName(),
+ 'migration' => $state->getName(),
'time_executed' => new \DateTime('now'),
'created_at' => $this->getMigrationCreatedAtForDb($migration),
]
@@ -170,17 +183,18 @@ static function () use ($migration, $capsule): void {
return $migration->withState($this->resolveState($migration));
} catch (\Throwable $exception) {
+ $state = $migration->getState();
throw new MigrationException(
\sprintf(
'Error in the migration (%s) occurred: %s',
\sprintf(
'%s (%s)',
- $migration->getState()->getName(),
- $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT)
+ $state->getName(),
+ $state->getTimeCreated()->format(self::DB_DATE_FORMAT)
),
$exception->getMessage()
),
- $exception->getCode(),
+ (int)$exception->getCode(),
$exception
);
}
@@ -190,22 +204,19 @@ static function () use ($migration, $capsule): void {
}
/**
- * Rollback last migration and return it's instance.
- *
- * @param CapsuleInterface $capsule
- * @return null|MigrationInterface
- *
+ * @param CapsuleInterface|null $capsule
+ * @return MigrationInterface|null
* @throws \Throwable
*/
public function rollback(CapsuleInterface $capsule = null): ?MigrationInterface
{
if (!$this->isConfigured()) {
- throw new MigrationException('Unable to run migration, Migrator not configured');
+ $this->configure();
}
/** @var MigrationInterface $migration */
- foreach (array_reverse($this->getMigrations()) as $migration) {
- if ($migration->getState()->getStatus() !== State::STATUS_EXECUTED) {
+ foreach (\array_reverse($this->getMigrations()) as $migration) {
+ if ($migration->getState()->getStatus() !== Status::STATUS_EXECUTED) {
continue;
}
@@ -235,19 +246,20 @@ static function () use ($migration, $capsule): void {
*
* @param MigrationInterface $migration
* @return State
+ * @throws \Exception
*/
- protected function resolveState(MigrationInterface $migration): State
+ private function resolveState(MigrationInterface $migration): State
{
$db = $this->dbal->database($migration->getDatabase());
$data = $this->fetchMigrationData($migration);
if (empty($data['time_executed'])) {
- return $migration->getState()->withStatus(State::STATUS_PENDING);
+ return $migration->getState()->withStatus(Status::STATUS_PENDING);
}
return $migration->getState()->withStatus(
- State::STATUS_EXECUTED,
+ Status::STATUS_EXECUTED,
new \DateTimeImmutable($data['time_executed'], $db->getDriver()->getTimezone())
);
}
@@ -258,30 +270,20 @@ protected function resolveState(MigrationInterface $migration): State
* @param string|null $database
* @return Table
*/
- protected function migrationTable(string $database = null): Table
+ private function migrationTable(string $database = null): Table
{
return $this->dbal->database($database)->table($this->config->getTable());
}
- protected function checkMigrationTableStructure(Database $db): bool
+ /**
+ * @param Database $db
+ * @return bool
+ */
+ private function checkMigrationTableStructure(Database $db): bool
{
- if (!$db->hasTable($this->config->getTable())) {
- return false;
- }
-
- $table = $db->table($this->config->getTable())->getSchema();
-
- foreach (self::MIGRATION_TABLE_FIELDS_LIST as $field) {
- if (!$table->hasColumn($field)) {
- return false;
- }
- }
+ $table = new MigrationsTable($db, $this->config->getTable());
- if (!$table->hasIndex(['migration', 'created_at'])) {
- return false;
- }
-
- return true;
+ return $table->isPresent();
}
/**
@@ -291,7 +293,7 @@ protected function checkMigrationTableStructure(Database $db): bool
*
* @return array|null
*/
- protected function fetchMigrationData(MigrationInterface $migration): ?array
+ private function fetchMigrationData(MigrationInterface $migration): ?array
{
$migrationData = $this->migrationTable($migration->getDatabase())
->select('id', 'time_executed', 'created_at')
@@ -307,7 +309,17 @@ protected function fetchMigrationData(MigrationInterface $migration): ?array
return is_array($migrationData) ? $migrationData : [];
}
- protected function restoreMigrationData(): void
+ /**
+ * This method updates the state of the empty (null) "created_at" fields for
+ * each entry in the migration table within the
+ * issue {@link https://github.com/spiral/migrations/issues/13}.
+ *
+ * TODO It is worth noting that this method works in an extremely suboptimal
+ * way and requires optimizations.
+ *
+ * @return void
+ */
+ private function restoreMigrationData(): void
{
foreach ($this->repository->getMigrations() as $migration) {
$migrationData = $this->migrationTable($migration->getDatabase())
@@ -333,13 +345,17 @@ protected function restoreMigrationData(): void
}
/**
- * Check if some data modification required
+ * Check if some data modification required.
+ *
+ * This method checks for empty (null) "created_at" fields created within
+ * the issue {@link https://github.com/spiral/migrations/issues/13}.
*
+ * @param iterable $databases
* @return bool
*/
- protected function isRestoreMigrationDataRequired(): bool
+ private function isRestoreMigrationDataRequired(iterable $databases): bool
{
- foreach ($this->dbal->getDatabases() as $db) {
+ foreach ($databases as $db) {
$table = $db->table($this->config->getTable());
if (
@@ -354,14 +370,26 @@ protected function isRestoreMigrationDataRequired(): bool
return false;
}
- protected function getMigrationCreatedAtForDb(MigrationInterface $migration): \DateTimeInterface
+ /**
+ * Creates a new date object based on the database timezone and the
+ * migration creation date.
+ *
+ * @param MigrationInterface $migration
+ * @return \DateTimeInterface
+ */
+ private function getMigrationCreatedAtForDb(MigrationInterface $migration): \DateTimeInterface
{
$db = $this->dbal->database($migration->getDatabase());
- return \DateTimeImmutable::createFromFormat(
- self::DB_DATE_FORMAT,
- $migration->getState()->getTimeCreated()->format(self::DB_DATE_FORMAT),
- $db->getDriver()->getTimezone()
- );
+ $createdAt = $migration->getState()
+ ->getTimeCreated()
+ ->format(self::DB_DATE_FORMAT)
+ ;
+
+ $timezone = $db->getDriver()
+ ->getTimezone()
+ ;
+
+ return \DateTimeImmutable::createFromFormat(self::DB_DATE_FORMAT, $createdAt, $timezone);
}
}
diff --git a/src/Migrator/MigrationsTable.php b/src/Migrator/MigrationsTable.php
new file mode 100644
index 0000000..11a8e94
--- /dev/null
+++ b/src/Migrator/MigrationsTable.php
@@ -0,0 +1,166 @@
+
+ */
+ private const MIGRATION_TABLE_FIELDS = [
+ 'id',
+ 'migration',
+ 'time_executed',
+ 'created_at'
+ ];
+
+ /**
+ * List of indices in the migration table. An implementation is specified in
+ * the {@see MigrationsTable::actualize()} method.
+ *
+ * @var array
+ */
+ private const MIGRATION_TABLE_INDICES = [
+ 'migration',
+ 'created_at'
+ ];
+
+ /**
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var AbstractTable
+ */
+ private $schema;
+
+ /**
+ * @param Database $db
+ * @param string $name
+ */
+ public function __construct(Database $db, string $name)
+ {
+ $this->db = $db;
+ $this->name = $name;
+
+ $table = $db->table($name);
+ $this->schema = $table->getSchema();
+ }
+
+ /**
+ * Schema update will automatically sync all needed data.
+ *
+ * Please note that if you change this migration, you will also need to
+ * change the list of fields in this migration specified in
+ * the {@see MigrationsTable::MIGRATION_TABLE_FIELDS}
+ * and {@see MigrationsTable::MIGRATION_TABLE_INDICES} constants.
+ *
+ * @return void
+ */
+ public function actualize(): void
+ {
+ $this->schema->primary('id');
+ $this->schema->string('migration', 191)->nullable(false);
+ $this->schema->datetime('time_executed')->datetime();
+ $this->schema->datetime('created_at')->datetime();
+
+ $this->schema->index(['migration', 'created_at'])
+ ->unique(true);
+
+ if ($this->schema->hasIndex(['migration'])) {
+ $this->schema->dropIndex(['migration']);
+ }
+
+ $this->schema->save();
+ }
+
+ /**
+ * Returns {@see true} if the migration table in the database is up to date
+ * or {@see false} instead.
+ *
+ * @return bool
+ */
+ public function isPresent(): bool
+ {
+ if (!$this->isTableExists()) {
+ return false;
+ }
+
+ if (!$this->isNecessaryColumnsExists()) {
+ return false;
+ }
+
+ if (!$this->isNecessaryIndicesExists()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns {@see true} if the migration's table exists or {@see false}
+ * instead.
+ *
+ * @return bool
+ */
+ private function isTableExists(): bool
+ {
+ return $this->db->hasTable($this->name);
+ }
+
+ /**
+ * Returns {@see true} if all migration's fields is present or {@see false}
+ * otherwise.
+ *
+ * @return bool
+ */
+ private function isNecessaryColumnsExists(): bool
+ {
+ foreach (self::MIGRATION_TABLE_FIELDS as $field) {
+ if (!$this->schema->hasColumn($field)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns {@see true} if all migration's indices is present or {@see false}
+ * otherwise.
+ *
+ * @return bool
+ */
+ private function isNecessaryIndicesExists(): bool
+ {
+ return $this->schema->hasIndex(self::MIGRATION_TABLE_INDICES);
+ }
+}
diff --git a/src/MigratorInterface.php b/src/MigratorInterface.php
new file mode 100644
index 0000000..3bcd226
--- /dev/null
+++ b/src/MigratorInterface.php
@@ -0,0 +1,43 @@
+name = $name;
- $this->status = $status;
- $this->timeCreated = $timeCreated;
- $this->timeExecuted = $timeExecuted;
- }
+ public const STATUS_UNDEFINED = Status::STATUS_UNDEFINED;
/**
- * @return string
+ * @deprecated Please use {@see Status::STATUS_PENDING} instead.
+ * @var StatusEnum
*/
- public function getName(): string
- {
- return $this->name;
- }
+ public const STATUS_PENDING = Status::STATUS_PENDING;
/**
- * Migration status.
- *
- * @return int
+ * @deprecated Please use {@see Status::STATUS_EXECUTED} instead.
+ * @var StatusEnum
*/
- public function getStatus(): int
- {
- return $this->status;
- }
-
- /**
- * Get migration creation time.
- *
- * @return \DateTimeInterface
- */
- public function getTimeCreated(): \DateTimeInterface
- {
- return $this->timeCreated;
- }
-
- /**
- * Get migration execution time if any.
- *
- * @return \DateTimeInterface|null
- */
- public function getTimeExecuted(): ?\DateTimeInterface
- {
- return $this->timeExecuted;
- }
-
- /**
- * @param int $status
- * @param \DateTimeInterface|null $timeExecuted
- *
- * @return State
- */
- public function withStatus(int $status, \DateTimeInterface $timeExecuted = null): State
- {
- $state = clone $this;
- $state->status = $status;
- $state->timeExecuted = $timeExecuted;
-
- return $state;
- }
+ public const STATUS_EXECUTED = Status::STATUS_EXECUTED;
}
diff --git a/src/TableBlueprint.php b/src/TableBlueprint.php
index e762e3a..1eb0e95 100644
--- a/src/TableBlueprint.php
+++ b/src/TableBlueprint.php
@@ -22,7 +22,7 @@
final class TableBlueprint
{
/** @var CapsuleInterface */
- private $capsule = null;
+ private $capsule;
/** @var bool */
private $executed = false;
diff --git a/tests/Migrations/AtomizerTest.php b/tests/Migrations/AtomizerTest.php
index 5f8bb59..fbf5966 100644
--- a/tests/Migrations/AtomizerTest.php
+++ b/tests/Migrations/AtomizerTest.php
@@ -12,7 +12,7 @@
namespace Spiral\Migrations\Tests;
use Spiral\Migrations\Migration;
-use Spiral\Migrations\State;
+use Spiral\Migrations\Migration\State;
abstract class AtomizerTest extends BaseTest
{
@@ -30,7 +30,7 @@ public function testCreateAndDiff(): void
$migration = $this->migrator->run();
$this->assertInstanceOf(Migration::class, $migration);
- $this->assertSame(State::STATUS_EXECUTED, $migration->getState()->getStatus());
+ $this->assertSame(Migration\Status::STATUS_EXECUTED, $migration->getState()->getStatus());
$this->assertInstanceOf(\DateTimeInterface::class, $migration->getState()->getTimeCreated());
$this->assertInstanceOf(\DateTimeInterface::class, $migration->getState()->getTimeExecuted());
diff --git a/tests/Migrations/BaseTest.php b/tests/Migrations/BaseTest.php
index 09959b4..478c959 100644
--- a/tests/Migrations/BaseTest.php
+++ b/tests/Migrations/BaseTest.php
@@ -72,7 +72,7 @@ public function setUp(): void
echo "\n\n-------- BEGIN: " . $this->getName() . " --------------\n\n";
}
- $this->container = $container = new Container();
+ $this->container = new Container();
$this->dbal = $this->getDBAL($this->container);
$this->migrationConfig = new MigrationConfig(static::CONFIG);
diff --git a/tests/Migrations/MigratorTest.php b/tests/Migrations/MigratorTest.php
index fcb376d..412b48c 100644
--- a/tests/Migrations/MigratorTest.php
+++ b/tests/Migrations/MigratorTest.php
@@ -13,7 +13,7 @@
use Spiral\Migrations\Capsule;
use Spiral\Migrations\Exception\MigrationException;
-use Spiral\Migrations\State;
+use Spiral\Migrations\Migration\State;
abstract class MigratorTest extends BaseTest
{
@@ -45,25 +45,42 @@ static function ($migration) {
$this->assertSame(['A1', 'A2', 'A3', 'A4', 'B1', 'B2', 'B3', 'C'], $classes);
}
- public function testIsConfigured(): void
+ private function generateMigration(string $file, string $class): string
{
- $this->assertFalse($this->migrator->isConfigured());
+ $out = __DIR__ . '/../files/' . $file;
+
+ file_put_contents($out, sprintf(file_get_contents(__DIR__ . '/../files/migration.stub'), $class));
+
+ return $out;
+ }
+ public function testDoNotCreateTableWithoutMigrations(): void
+ {
$this->migrator->configure();
+
+ $this->assertFalse($this->db->hasTable('migrations'));
+ }
+
+ public function testIsConfiguredWithoutMigrations(): void
+ {
$this->assertTrue($this->migrator->isConfigured());
}
- public function testConfigure(): void
+ public function testIsConfiguredWithMigrations(): void
{
+ $this->generateMigration('20200909.024119_333_333_migration_1.php', 'A3');
+
$this->assertFalse($this->migrator->isConfigured());
$this->migrator->configure();
- $this->assertTrue($this->db->hasTable('migrations'));
+ $this->assertTrue($this->migrator->isConfigured());
}
//no errors expected
public function testConfigureTwice(): void
{
+ $this->generateMigration('20200909.024119_333_333_migration_1.php', 'A3');
+
$this->assertFalse($this->migrator->isConfigured());
$this->migrator->configure();
@@ -74,6 +91,8 @@ public function testConfigureTwice(): void
public function testConfiguredTableStructure(): void
{
+ $this->generateMigration('20200909.024119_333_333_migration_1.php', 'A3');
+
$this->migrator->configure();
$table = $this->db->table('migrations');
@@ -102,22 +121,6 @@ public function testConfig(): void
$this->assertSame($this->migrationConfig, $this->migrator->getConfig());
}
- public function testRunUnconfigured(): void
- {
- $this->expectException(MigrationException::class);
- $this->expectExceptionMessage("Unable to run migration, Migrator not configured");
-
- $this->migrator->run();
- }
-
- public function testRollbackUnconfigured(): void
- {
- $this->expectException(MigrationException::class);
- $this->expectExceptionMessage("Unable to run migration, Migrator not configured");
-
- $this->migrator->rollback();
- }
-
public function testCapsule(): void
{
$capsule = new Capsule($this->db);
@@ -134,9 +137,7 @@ public function testCapsuleException(): void
$this->expectException(\Spiral\Migrations\Exception\CapsuleException::class);
$capsule = new Capsule($this->db);
- $capsule->execute([
- $this
- ]);
+ $capsule->execute([$this]);
}
public function testNoState(): void
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 3105bfd..9c726a3 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -2,7 +2,7 @@ version: "3"
services:
sqlserver:
- image: microsoft/mssql-server-linux
+ image: mcr.microsoft.com/mssql/server:2019-latest
ports:
- "11433:1433"
environment:
diff --git a/tests/files/.gitignore b/tests/files/.gitignore
new file mode 100644
index 0000000..cde8069
--- /dev/null
+++ b/tests/files/.gitignore
@@ -0,0 +1 @@
+*.php