Added basic database migration functionality
parent
e44ba84561
commit
235266ec53
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Engelsystem\Application;
|
||||
use Engelsystem\Database\Migration\Migrate;
|
||||
use Engelsystem\Database\Migration\MigrationServiceProvider;
|
||||
|
||||
require_once __DIR__ . '/../includes/application.php';
|
||||
|
||||
/** @var $loader ClassLoader */
|
||||
$baseDir = __DIR__ . '/../db/migrations';
|
||||
|
||||
/** @var Application $app */
|
||||
$app = app();
|
||||
$app->register(MigrationServiceProvider::class);
|
||||
|
||||
/** @var Migrate $migration */
|
||||
$migration = $app->get('db.migration');
|
||||
$migration->setOutput(function ($text) { echo $text . PHP_EOL; });
|
||||
$migration->run($baseDir, Migrate::UP);
|
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Database\Migration;
|
||||
|
||||
use Engelsystem\Application;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Migrate
|
||||
{
|
||||
const UP = 'up';
|
||||
const DOWN = 'down';
|
||||
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
/** @var SchemaBuilder */
|
||||
protected $scheme;
|
||||
|
||||
/** @var callable */
|
||||
protected $output;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'migrations';
|
||||
|
||||
/**
|
||||
* Migrate constructor
|
||||
*
|
||||
* @param SchemaBuilder $scheme
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(SchemaBuilder $scheme, Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->scheme = $scheme;
|
||||
$this->output = function () { };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a migration
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $type (up|down)
|
||||
* @param bool $oneStep
|
||||
*/
|
||||
public function run($path, $type = self::UP, $oneStep = false)
|
||||
{
|
||||
$this->initMigration();
|
||||
$migrations = $this->getMigrations($path);
|
||||
$migrated = $this->getMigrated();
|
||||
|
||||
if ($type == self::DOWN) {
|
||||
$migrations = array_reverse($migrations, true);
|
||||
}
|
||||
|
||||
foreach ($migrations as $file => $migration) {
|
||||
if (
|
||||
($type == self::UP && $migrated->contains('migration', $migration))
|
||||
|| ($type == self::DOWN && !$migrated->contains('migration', $migration))
|
||||
) {
|
||||
call_user_func($this->output, 'Skipping ' . $migration);
|
||||
continue;
|
||||
}
|
||||
|
||||
call_user_func($this->output, 'Migrating ' . $migration . ' (' . $type . ')');
|
||||
|
||||
$this->migrate($file, $migration, $type);
|
||||
$this->setMigrated($migration, $type);
|
||||
|
||||
if ($oneStep) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all migrated migrations
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
protected function getMigrated()
|
||||
{
|
||||
return $this->getTableQuery()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a migration
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $migration
|
||||
* @param string $type (up|down)
|
||||
*/
|
||||
protected function migrate($file, $migration, $type = self::UP)
|
||||
{
|
||||
require_once $file;
|
||||
|
||||
$className = Str::studly(preg_replace('/\d+_/', '', $migration));
|
||||
/** @var Migration $class */
|
||||
$class = $this->app->make($className);
|
||||
|
||||
if (method_exists($class, $type)) {
|
||||
$class->{$type}();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a migration to migrated
|
||||
*
|
||||
* @param string $migration
|
||||
* @param string $type (up|down)
|
||||
*/
|
||||
protected function setMigrated($migration, $type = self::UP)
|
||||
{
|
||||
$table = $this->getTableQuery();
|
||||
|
||||
if ($type == self::DOWN) {
|
||||
$table->where(['migration' => $migration])->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
$table->insert(['migration' => $migration]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of migration files
|
||||
*
|
||||
* @param string $dir
|
||||
* @return array
|
||||
*/
|
||||
protected function getMigrations($dir)
|
||||
{
|
||||
$files = $this->getMigrationFiles($dir);
|
||||
|
||||
$migrations = [];
|
||||
foreach ($files as $dir) {
|
||||
$name = str_replace('.php', '', basename($dir));
|
||||
$migrations[$dir] = $name;
|
||||
}
|
||||
|
||||
asort($migrations);
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all migration files from the given directory
|
||||
*
|
||||
* @param string $dir
|
||||
* @return array
|
||||
*/
|
||||
protected function getMigrationFiles($dir)
|
||||
{
|
||||
return glob($dir . '/*_*.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup migration tables
|
||||
*/
|
||||
protected function initMigration()
|
||||
{
|
||||
if ($this->scheme->hasTable($this->table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scheme->create($this->table, function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('migration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Init a table query
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
protected function getTableQuery()
|
||||
{
|
||||
return $this->scheme->getConnection()->table($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the output function
|
||||
*
|
||||
* @param callable $output
|
||||
*/
|
||||
public function setOutput(callable $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Database\Migration;
|
||||
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
|
||||
abstract class Migration
|
||||
{
|
||||
/** @var SchemaBuilder */
|
||||
protected $schema;
|
||||
|
||||
public function __construct(SchemaBuilder $schemaBuilder)
|
||||
{
|
||||
$this->schema = $schemaBuilder;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Database\Migration;
|
||||
|
||||
use Engelsystem\Container\ServiceProvider;
|
||||
use Engelsystem\Database\Db;
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
|
||||
class MigrationServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
$schema = Db::connection()->getSchemaBuilder();
|
||||
$this->app->instance('db.scheme', $schema);
|
||||
$this->app->bind(SchemaBuilder::class, 'db.scheme');
|
||||
|
||||
$migration = $this->app->make(Migrate::class);
|
||||
$this->app->instance('db.migration', $migration);
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Database;
|
||||
|
||||
use Engelsystem\Application;
|
||||
use Engelsystem\Database\Migration\Migrate;
|
||||
use Illuminate\Database\Capsule\Manager as CapsuleManager;
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MigrateTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::__construct
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::run
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::getMigrations
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::setOutput
|
||||
*/
|
||||
public function testRun()
|
||||
{
|
||||
/** @var MockObject|Application $app */
|
||||
$app = $this->getMockBuilder(Application::class)
|
||||
->setMethods(['instance'])
|
||||
->getMock();
|
||||
/** @var MockObject|SchemaBuilder $builder */
|
||||
$builder = $this->getMockBuilder(SchemaBuilder::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var MockObject|Migrate $migration */
|
||||
$migration = $this->getMockBuilder(Migrate::class)
|
||||
->setConstructorArgs([$builder, $app])
|
||||
->setMethods(['initMigration', 'getMigrationFiles', 'getMigrated', 'migrate', 'setMigrated'])
|
||||
->getMock();
|
||||
|
||||
$migration->expects($this->atLeastOnce())
|
||||
->method('initMigration');
|
||||
$migration->expects($this->atLeastOnce())
|
||||
->method('getMigrationFiles')
|
||||
->willReturn([
|
||||
'foo/1234_01_23_123456_init_foo.php',
|
||||
'foo/9876_03_22_210000_random_hack.php',
|
||||
'foo/4567_11_01_000000_do_stuff.php',
|
||||
'foo/9999_99_99_999999_another_foo.php',
|
||||
]);
|
||||
$migration->expects($this->atLeastOnce())
|
||||
->method('getMigrated')
|
||||
->willReturn(new Collection([
|
||||
['id' => 1, 'migration' => '1234_01_23_123456_init_foo'],
|
||||
['id' => 2, 'migration' => '4567_11_01_000000_do_stuff'],
|
||||
]));
|
||||
$migration->expects($this->atLeastOnce())
|
||||
->method('migrate')
|
||||
->withConsecutive(
|
||||
['foo/9876_03_22_210000_random_hack.php', '9876_03_22_210000_random_hack', Migrate::UP],
|
||||
['foo/9999_99_99_999999_another_foo.php', '9999_99_99_999999_another_foo', Migrate::UP],
|
||||
['foo/9876_03_22_210000_random_hack.php', '9876_03_22_210000_random_hack', Migrate::UP],
|
||||
['foo/4567_11_01_000000_do_stuff.php', '4567_11_01_000000_do_stuff', Migrate::DOWN]
|
||||
);
|
||||
$migration->expects($this->atLeastOnce())
|
||||
->method('setMigrated')
|
||||
->withConsecutive(
|
||||
['9876_03_22_210000_random_hack', Migrate::UP],
|
||||
['9999_99_99_999999_another_foo', Migrate::UP],
|
||||
['9876_03_22_210000_random_hack', Migrate::UP],
|
||||
['4567_11_01_000000_do_stuff', Migrate::DOWN]
|
||||
);
|
||||
|
||||
$messages = [];
|
||||
$migration->setOutput(function ($text) use (&$messages) {
|
||||
$messages[] = $text;
|
||||
});
|
||||
|
||||
$migration->run('foo', Migrate::UP);
|
||||
|
||||
$this->assertCount(4, $messages);
|
||||
foreach (
|
||||
[
|
||||
'init_foo' => 'skipping',
|
||||
'do_stuff' => 'skipping',
|
||||
'random_hack' => 'migrating',
|
||||
'another_foo' => 'migrating',
|
||||
] as $value => $type
|
||||
) {
|
||||
$contains = false;
|
||||
foreach ($messages as $message) {
|
||||
if (!Str::contains(strtolower($message), $type) || !Str::contains(strtolower($message), $value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contains = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->assertTrue($contains, sprintf('Missing message "%s: %s"', $type, $value));
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
$migration->run('foo', Migrate::UP, true);
|
||||
$this->assertCount(3, $messages);
|
||||
|
||||
$migration->run('foo', Migrate::DOWN, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::getMigrated
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::migrate
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::setMigrated
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::getMigrationFiles
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::initMigration
|
||||
* @covers \Engelsystem\Database\Migration\Migrate::getTableQuery
|
||||
*/
|
||||
public function testRunIntegration()
|
||||
{
|
||||
$app = new Application();
|
||||
$dbManager = new CapsuleManager($app);
|
||||
$dbManager->addConnection(['driver' => 'sqlite', 'database' => ':memory:']);
|
||||
$dbManager->bootEloquent();
|
||||
$db = $dbManager->getConnection();
|
||||
$db->useDefaultSchemaGrammar();
|
||||
$scheme = $db->getSchemaBuilder();
|
||||
|
||||
$app->instance('scheme', $scheme);
|
||||
$app->bind(SchemaBuilder::class, 'scheme');
|
||||
|
||||
$migration = new Migrate($scheme, $app);
|
||||
|
||||
$messages = [];
|
||||
$migration->setOutput(function ($msg) use (&$messages) {
|
||||
$messages[] = $msg;
|
||||
});
|
||||
|
||||
$migration->run(__DIR__ . '/Stub', Migrate::UP);
|
||||
|
||||
$this->assertTrue($scheme->hasTable('migrations'));
|
||||
|
||||
$migrations = $db->table('migrations')->get();
|
||||
$this->assertCount(3, $migrations);
|
||||
|
||||
$this->assertTrue($migrations->contains('migration', '2001_04_11_123456_create_lorem_ipsum_table'));
|
||||
$this->assertTrue($migrations->contains('migration', '2017_12_24_053300_another_stuff'));
|
||||
$this->assertTrue($migrations->contains('migration', '2022_12_22_221222_add_some_feature'));
|
||||
|
||||
$this->assertTrue($scheme->hasTable('lorem_ipsum'));
|
||||
|
||||
$migration->run(__DIR__ . '/Stub', Migrate::DOWN, true);
|
||||
|
||||
$migrations = $db->table('migrations')->get();
|
||||
$this->assertCount(2, $migrations);
|
||||
|
||||
$migration->run(__DIR__ . '/Stub', Migrate::DOWN);
|
||||
|
||||
$migrations = $db->table('migrations')->get();
|
||||
$this->assertCount(0, $migrations);
|
||||
|
||||
$this->assertFalse($scheme->hasTable('lorem_ipsum'));
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Database\Migration;
|
||||
|
||||
use Engelsystem\Database\Db;
|
||||
use Engelsystem\Database\Migration\Migrate;
|
||||
use Engelsystem\Database\Migration\MigrationServiceProvider;
|
||||
use Engelsystem\Test\Unit\ServiceProviderTest;
|
||||
use Illuminate\Database\Capsule\Manager as CapsuleManager;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
use PHPUnit_Framework_MockObject_MockObject as MockObject;
|
||||
|
||||
class MigrationServiceProviderTest extends ServiceProviderTest
|
||||
{
|
||||
/**
|
||||
* @covers \Engelsystem\Database\Migration\MigrationServiceProvider::register()
|
||||
*/
|
||||
public function testRegister()
|
||||
{
|
||||
/** @var MockObject|Migrate $migration */
|
||||
$migration = $this->getMockBuilder(Migrate::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var MockObject|CapsuleManager $dbManager */
|
||||
$dbManager = $this->getMockBuilder(CapsuleManager::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var MockObject|Connection $dbConnection */
|
||||
$dbConnection = $this->getMockBuilder(Connection::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var MockObject|SchemaBuilder $schemaBuilder */
|
||||
$schemaBuilder = $this->getMockBuilder(SchemaBuilder::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$app = $this->getApp(['make', 'instance', 'bind']);
|
||||
|
||||
$app->expects($this->atLeastOnce())
|
||||
->method('instance')
|
||||
->withConsecutive(['db.scheme'], ['db.migration'])
|
||||
->willReturnOnConsecutiveCalls($schemaBuilder, $migration);
|
||||
|
||||
$this->setExpects($app, 'bind', [SchemaBuilder::class, 'db.scheme']);
|
||||
$this->setExpects($app, 'make', [Migrate::class], $migration);
|
||||
|
||||
$this->setExpects($dbConnection, 'getSchemaBuilder', null, $schemaBuilder);
|
||||
$this->setExpects($dbManager, 'getConnection', null, $dbConnection);
|
||||
Db::setDbManager($dbManager);
|
||||
|
||||
$serviceProvider = new MigrationServiceProvider($app);
|
||||
$serviceProvider->register();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Database;
|
||||
|
||||
use AnotherStuff;
|
||||
use Illuminate\Database\Schema\Builder as SchemaBuilder;
|
||||
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MigrationTest extends TestCase
|
||||
{
|
||||
public function testConstructor()
|
||||
{
|
||||
require_once __DIR__ . '/Stub/2017_12_24_053300_another_stuff.php';
|
||||
|
||||
/** @var MockBuilder|SchemaBuilder $schemaBuilder */
|
||||
$schemaBuilder = $this->getMockBuilder(SchemaBuilder::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$instance = new AnotherStuff($schemaBuilder);
|
||||
$this->assertAttributeEquals($schemaBuilder, 'schema', $instance);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateLoremIpsumTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('lorem_ipsum', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name')->unique();
|
||||
$table->string('email');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$this->schema->dropIfExists('lorem_ipsum');
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
|
||||
class AnotherStuff extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// nope
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// nope
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
|
||||
class AddSomeFeature extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// nope
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// nope
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue