Added basic database migration functionality

main
Igor Scheller 7 years ago
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);

@ -28,6 +28,7 @@ class DatabaseServiceProvider extends ServiceProvider
$capsule->setAsGlobal(); $capsule->setAsGlobal();
$capsule->bootEloquent(); $capsule->bootEloquent();
$capsule->getConnection()->useDefaultSchemaGrammar();
try { try {
$capsule->getConnection()->getPdo(); $capsule->getConnection()->getPdo();

@ -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);
}
}

@ -74,6 +74,7 @@ class DatabaseServiceProviderTest extends ServiceProviderTest
$this->setExpects($dbManager, 'setAsGlobal'); $this->setExpects($dbManager, 'setAsGlobal');
$this->setExpects($dbManager, 'bootEloquent'); $this->setExpects($dbManager, 'bootEloquent');
$this->setExpects($connection, 'useDefaultSchemaGrammar');
$connection->expects($this->once()) $connection->expects($this->once())
->method('getPdo') ->method('getPdo')
->willReturnCallback(function () use ($getPdoThrowException) { ->willReturnCallback(function () use ($getPdoThrowException) {

@ -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…
Cancel
Save