diff --git a/migrations/2015_02_24_000000_create_access_tokens_table.php b/migrations/2015_02_24_000000_create_access_tokens_table.php index ee2a0be58..6eb3ddd1f 100644 --- a/migrations/2015_02_24_000000_create_access_tokens_table.php +++ b/migrations/2015_02_24_000000_create_access_tokens_table.php @@ -1,8 +1,7 @@ schema->create('activity', function (Blueprint $table) { - $table->increments('id'); - $table->integer('user_id')->unsigned(); - $table->string('type', 100); - $table->integer('subject_id')->unsigned()->nullable(); - $table->binary('data')->nullable(); - $table->dateTime('time'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - $this->schema->drop('activity'); - } -} diff --git a/migrations/2015_02_24_000000_create_config_table.php b/migrations/2015_02_24_000000_create_config_table.php index 784338bc6..71af72ace 100644 --- a/migrations/2015_02_24_000000_create_config_table.php +++ b/migrations/2015_02_24_000000_create_config_table.php @@ -1,8 +1,7 @@ schema->create('config', function (Blueprint $table) { - $table->string('key', 100)->primary(); $table->binary('value')->nullable(); }); diff --git a/migrations/2015_02_24_000000_create_discussions_table.php b/migrations/2015_02_24_000000_create_discussions_table.php index af8c5df81..76ba86f42 100644 --- a/migrations/2015_02_24_000000_create_discussions_table.php +++ b/migrations/2015_02_24_000000_create_discussions_table.php @@ -1,8 +1,7 @@ schema->create('discussions', function (Blueprint $table) { - $table->increments('id'); $table->string('title', 200); $table->integer('comments_count')->unsigned()->default(0); diff --git a/migrations/2015_02_24_000000_create_email_tokens_table.php b/migrations/2015_02_24_000000_create_email_tokens_table.php index 284e50a6f..4963883e8 100644 --- a/migrations/2015_02_24_000000_create_email_tokens_table.php +++ b/migrations/2015_02_24_000000_create_email_tokens_table.php @@ -1,8 +1,7 @@ schema->create('notifications', function (Blueprint $table) { - $table->increments('id'); $table->integer('user_id')->unsigned(); $table->integer('sender_id')->unsigned()->nullable(); diff --git a/migrations/2015_02_24_000000_create_password_tokens_table.php b/migrations/2015_02_24_000000_create_password_tokens_table.php index 8f50aa06f..a79ffe1e7 100644 --- a/migrations/2015_02_24_000000_create_password_tokens_table.php +++ b/migrations/2015_02_24_000000_create_password_tokens_table.php @@ -1,8 +1,7 @@ schema->create('permissions', function (Blueprint $table) { - $table->integer('group_id')->unsigned(); $table->string('permission', 100); $table->primary(['group_id', 'permission']); diff --git a/migrations/2015_02_24_000000_create_posts_table.php b/migrations/2015_02_24_000000_create_posts_table.php index 6df6f3839..c3d879b96 100644 --- a/migrations/2015_02_24_000000_create_posts_table.php +++ b/migrations/2015_02_24_000000_create_posts_table.php @@ -1,8 +1,7 @@ schema->create('users_groups', function (Blueprint $table) { - $table->integer('user_id')->unsigned(); $table->integer('group_id')->unsigned(); $table->primary(['user_id', 'group_id']); diff --git a/migrations/2015_02_24_000000_create_users_table.php b/migrations/2015_02_24_000000_create_users_table.php index 0af2fe3e1..04ae5e4f4 100644 --- a/migrations/2015_02_24_000000_create_users_table.php +++ b/migrations/2015_02_24_000000_create_users_table.php @@ -1,8 +1,7 @@ schema->create('test', function (Blueprint $table) { + $table->increments('id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $this->schema->drop('test'); + } +} diff --git a/src/Console/ConsoleServiceProvider.php b/src/Console/ConsoleServiceProvider.php deleted file mode 100644 index e0662f1f2..000000000 --- a/src/Console/ConsoleServiceProvider.php +++ /dev/null @@ -1,23 +0,0 @@ -commands('Flarum\Console\InstallCommand'); - $this->commands('Flarum\Console\SeedCommand'); - $this->commands('Flarum\Console\ImportCommand'); - $this->commands('Flarum\Console\GenerateExtensionCommand'); - } - - public function register() - { - } -} diff --git a/src/Console/GenerateExtensionCommand.php b/src/Console/GenerateExtensionCommand.php index 3c8403beb..501aa2e71 100644 --- a/src/Console/GenerateExtensionCommand.php +++ b/src/Console/GenerateExtensionCommand.php @@ -1,37 +1,28 @@ container = $container; - $this->app = $app; + parent::__construct(); + } + + protected function configure() + { + $this + ->setName('generate:extension') + ->setDescription("Generate a Flarum extension skeleton."); } /** @@ -39,7 +30,7 @@ class GenerateExtensionCommand extends Command * * @return mixed */ - public function fire() + protected function fire() { do { $vendor = $this->ask('Vendor name:'); diff --git a/src/Console/SeedCommand.php b/src/Console/SeedCommand.php deleted file mode 100644 index 1a01c0e22..000000000 --- a/src/Console/SeedCommand.php +++ /dev/null @@ -1,58 +0,0 @@ -call('db:seed', ['--class' => 'Flarum\Core\Seeders\UsersTableSeeder']); - $this->call('db:seed', ['--class' => 'Flarum\Core\Seeders\DiscussionsTableSeeder']); - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - // ['example', InputArgument::REQUIRED, 'An example argument.'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - // ['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null], - ]; - } -} diff --git a/src/Console/UpgradeCommand.php b/src/Console/UpgradeCommand.php new file mode 100644 index 000000000..b5abee6dd --- /dev/null +++ b/src/Console/UpgradeCommand.php @@ -0,0 +1,56 @@ +container = $container; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setName('upgrade') + ->setDescription("Run Flarum's upgrade script"); + } + + /** + * @inheritdoc + */ + protected function fire() + { + $this->info('Upgrading Flarum...'); + + $this->upgrade(); + + $this->info('DONE.'); + } + + protected function upgrade() + { + $this->container->bind('Illuminate\Database\Schema\Builder', function($container) { + return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); + }); + + $migrator = $this->container->make('Flarum\Migrations\Migrator'); + + $migrator->run(base_path('core/migrations')); + + foreach ($migrator->getNotes() as $note) { + $this->info($note); + } + } +} diff --git a/src/Core/DatabaseServiceProvider.php b/src/Core/DatabaseServiceProvider.php index 64cacc201..32421de1b 100644 --- a/src/Core/DatabaseServiceProvider.php +++ b/src/Core/DatabaseServiceProvider.php @@ -6,6 +6,7 @@ use Illuminate\Database\ConnectionResolver; use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\Eloquent\Model; use PDO; +use Flarum\Migrations\DatabaseMigrationRepository; class DatabaseServiceProvider extends ServiceProvider { @@ -44,5 +45,10 @@ class DatabaseServiceProvider extends ServiceProvider Model::setEventDispatcher($this->app->make('events')); }); } + + + $this->app->singleton('Flarum\Migrations\MigrationRepositoryInterface', function ($app) { + return new DatabaseMigrationRepository($app['db'], 'migrations'); + }); } } diff --git a/src/Install/Console/DataFromUser.php b/src/Install/Console/DataFromUser.php index b19f39767..722cf5b56 100644 --- a/src/Install/Console/DataFromUser.php +++ b/src/Install/Console/DataFromUser.php @@ -43,6 +43,35 @@ class DataFromUser implements ProvidesData ]; } + public function getSettings() + { + $baseUrl = rtrim($this->ask('Base URL:'), '/'); + $title = $this->ask('Forum title:'); + + return [ + 'admin_url' => $baseUrl . '/admin', + 'allow_post_editing' => 'reply', + 'allow_renaming' => '10', + 'allow_sign_up' => '1', + 'api_url' => $baseUrl . '/api', + 'base_url' => $baseUrl, + 'custom_less' => '', + 'default_locale' => 'en', + 'default_route' => '/all', + 'extensions_enabled' => '[]', + 'forum_title' => $title, + 'forum_description' => '', + 'mail_driver' => 'mail', + 'mail_from' => 'noreply@' . preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), + 'theme_colored_header' => '0', + 'theme_dark_mode' => '0', + 'theme_primary_color' => '#29415E', + 'theme_secondary_color' => '#29415E', + 'welcome_message' => 'This is beta software and you should not use it in production.', + 'welcome_title' => 'Welcome to ' . $title, + ]; + } + protected function ask($question, $default = null) { $question = new Question("$question ", $default); diff --git a/src/Install/Console/DefaultData.php b/src/Install/Console/DefaultData.php index bf5e97765..7bce9edc9 100644 --- a/src/Install/Console/DefaultData.php +++ b/src/Install/Console/DefaultData.php @@ -7,7 +7,7 @@ class DefaultData implements ProvidesData return [ 'driver' => 'mysql', 'host' => 'localhost', - 'database' => 'flarum', + 'database' => 'flarum_console', 'username' => 'root', 'password' => 'root', 'prefix' => '', @@ -22,4 +22,30 @@ class DefaultData implements ProvidesData 'email' => 'admin@example.com', ]; } + + public function getSettings() + { + return [ + 'admin_url' => 'http://flarum.dev/admin', + 'allow_post_editing' => 'reply', + 'allow_renaming' => '10', + 'allow_sign_up' => '1', + 'api_url' => 'http://flarum.dev/api', + 'base_url' => 'http://flarum.dev', + 'custom_less' => '', + 'default_locale' => 'en', + 'default_route' => '/all', + 'extensions_enabled' => '[]', + 'forum_title' => 'Development Forum', + 'forum_description' => '', + 'mail_driver' => 'mail', + 'mail_from' => 'noreply@flarum.dev', + 'theme_colored_header' => '0', + 'theme_dark_mode' => '0', + 'theme_primary_color' => '#29415E', + 'theme_secondary_color' => '#29415E', + 'welcome_message' => 'This is beta software and you should not use it in production.', + 'welcome_title' => 'Welcome to Development Forum', + ]; + } } diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index fa569bbe0..f33e0eeb2 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -1,6 +1,10 @@ container = $container; - $this->container->bind('Illuminate\Database\Schema\Builder', function($container) { - return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); - }); - parent::__construct(); } @@ -72,7 +72,19 @@ class InstallCommand extends Command $this->runMigrations(); + $this->writeSettings(); + + $this->container->register('Flarum\Core\CoreServiceProvider'); + + $resolver = $this->container->make('Illuminate\Database\ConnectionResolverInterface'); + Model::setConnectionResolver($resolver); + Model::setEventDispatcher($this->container->make('events')); + + $this->seedGroups(); + $this->seedPermissions(); + $this->createAdminUser(); + } protected function storeConfiguration() @@ -87,8 +99,8 @@ class InstallCommand extends Command 'database' => $dbConfig['database'], 'username' => $dbConfig['username'], 'password' => $dbConfig['password'], - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'prefix' => $dbConfig['prefix'], 'strict' => false ], @@ -105,24 +117,80 @@ class InstallCommand extends Command protected function runMigrations() { - $migrationDir = base_path('core/migrations'); + $this->container->bind('Illuminate\Database\Schema\Builder', function($container) { + return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); + }); - $files = glob("$migrationDir/*_*.php") or []; - sort($files); + $migrator = $this->container->make('Flarum\Migrations\Migrator'); + $migrator->getRepository()->createRepository(); - foreach ($files as $file) { - require $file; + $migrator->run(base_path('core/migrations')); - $migrationClass = studly_case(substr(basename($file), 18)); - $migrationClass = str_replace('.php', '', $migrationClass); - $migration = $this->container->make($migrationClass); - - $this->info("Migrating $migrationClass"); - - $migration->up(); + foreach ($migrator->getNotes() as $note) { + $this->info($note); } } + protected function writeSettings() + { + $data = $this->dataSource->getSettings(); + $settings = $this->container->make('Flarum\Core\Settings\SettingsRepository'); + + $this->info('Writing default settings'); + + foreach ($data as $k => $v) { + $settings->set($k, $v); + } + } + + protected function seedGroups() + { + Group::unguard(); + + $groups = [ + ['Admin', 'Admins', '#B72A2A', 'wrench'], + ['Guest', 'Guests', null, null], + ['Member', 'Members', null, null], + ['Mod', 'Mods', '#80349E', 'bolt'] + ]; + + foreach ($groups as $group) { + Group::create([ + 'name_singular' => $group[0], + 'name_plural' => $group[1], + 'color' => $group[2], + 'icon' => $group[3] + ]); + } + } + + protected function seedPermissions() + { + $permissions = [ + // Guests can view the forum + [2, 'forum.view'], + + // Members can create and reply to discussions + [3, 'forum.startDiscussion'], + [3, 'discussion.reply'], + + // Moderators can edit + delete stuff + [4, 'discussion.delete'], + [4, 'discussion.deletePosts'], + [4, 'discussion.editPosts'], + [4, 'discussion.rename'], + ]; + + foreach ($permissions as &$permission) { + $permission = [ + 'group_id' => $permission[0], + 'permission' => $permission[1] + ]; + } + + Permission::insert($permissions); + } + protected function createAdminUser() { $admin = $this->dataSource->getAdminUser(); @@ -130,7 +198,14 @@ class InstallCommand extends Command $this->info('Creating admin user '.$admin['username']); - $db->table('users')->insert($admin); + User::unguard(); + + $user = new User($admin); + $user->is_activated = 1; + $user->join_time = time(); + $user->save(); + + $user->groups()->sync([1]); } /** diff --git a/src/Install/Console/ProvidesData.php b/src/Install/Console/ProvidesData.php index 6eaf4b3c8..83220a536 100644 --- a/src/Install/Console/ProvidesData.php +++ b/src/Install/Console/ProvidesData.php @@ -5,4 +5,6 @@ interface ProvidesData public function getDatabaseConfiguration(); public function getAdminUser(); + + public function getSettings(); } diff --git a/src/Migrations/DatabaseMigrationRepository.php b/src/Migrations/DatabaseMigrationRepository.php new file mode 100755 index 000000000..6438110f6 --- /dev/null +++ b/src/Migrations/DatabaseMigrationRepository.php @@ -0,0 +1,157 @@ +table = $table; + $this->resolver = $resolver; + } + + /** + * Get the ran migrations. + * + * @return array + */ + public function getRan($extension = null) + { + return $this->table() + ->where('extension', $extension) + ->orderBy('migration', 'asc') + ->lists('migration'); + } + + /** + * Log that a migration was run. + * + * @param string $file + * @param string $extension + * @return void + */ + public function log($file, $extension = null) + { + $record = ['migration' => $file, 'extension' => $extension]; + + $this->table()->insert($record); + } + + /** + * Remove a migration from the log. + * + * @param string $file + * @param string $extension + * @return void + */ + public function delete($file, $extension = null) + { + $query = $this->table()->where('migration', $file); + + if (is_null($extension)) { + $query->whereNull('extension'); + } else { + $query->where('extension', $extension); + } + + $query->delete(); + } + + /** + * Create the migration repository data store. + * + * @return void + */ + public function createRepository() + { + $schema = $this->getConnection()->getSchemaBuilder(); + + $schema->create($this->table, function ($table) { + $table->string('migration'); + $table->string('extension')->nullable(); + }); + } + + /** + * Determine if the migration repository exists. + * + * @return bool + */ + public function repositoryExists() + { + $schema = $this->getConnection()->getSchemaBuilder(); + + return $schema->hasTable($this->table); + } + + /** + * Get a query builder for the migration table. + * + * @return \Illuminate\Database\Query\Builder + */ + protected function table() + { + return $this->getConnection()->table($this->table); + } + + /** + * Get the connection resolver instance. + * + * @return \Illuminate\Database\ConnectionResolverInterface + */ + public function getConnectionResolver() + { + return $this->resolver; + } + + /** + * Resolve the database connection instance. + * + * @return \Illuminate\Database\Connection + */ + public function getConnection() + { + return $this->resolver->connection($this->connection); + } + + /** + * Set the information source to gather data. + * + * @param string $name + * @return void + */ + public function setSource($name) + { + $this->connection = $name; + } +} diff --git a/src/Install/Migration.php b/src/Migrations/Migration.php similarity index 86% rename from src/Install/Migration.php rename to src/Migrations/Migration.php index a038191ae..8706625ae 100644 --- a/src/Install/Migration.php +++ b/src/Migrations/Migration.php @@ -1,4 +1,4 @@ -files = $files; + $this->resolver = $resolver; + $this->repository = $repository; + } + + /** + * Run the outstanding migrations at a given path. + * + * @param string $path + * @param string $extension + * @return void + */ + public function run($path, $extension = null) + { + $this->notes = []; + + $files = $this->getMigrationFiles($path); + + $ran = $this->repository->getRan($extension); + + $migrations = array_diff($files, $ran); + + $this->requireFiles($path, $migrations); + + $this->runMigrationList($migrations, $extension); + } + + /** + * Run an array of migrations. + * + * @param array $migrations + * @param bool $pretend + * @return void + */ + public function runMigrationList($migrations, $extension) + { + // First we will just make sure that there are any migrations to run. If there + // aren't, we will just make a note of it to the developer so they're aware + // that all of the migrations have been run against this database system. + if (count($migrations) == 0) { + $this->note('Nothing to migrate.'); + + return; + } + + // Once we have the array of migrations, we will spin through them and run the + // migrations "up" so the changes are made to the databases. We'll then log + // that the migration was run so we don't repeat it next time we execute. + foreach ($migrations as $file) { + $this->runUp($file, $extension); + } + } + + /** + * Run "up" a migration instance. + * + * @param string $file + * @param string $extension + * @return void + */ + protected function runUp($file, $extension) + { + // First we will resolve a "real" instance of the migration class from this + // migration file name. Once we have the instances we can run the actual + // command such as "up" or "down", or we can just simulate the action. + $migration = $this->resolve($file); + + $migration->up(); + + // Once we have run a migrations class, we will log that it was run in this + // repository so that we don't try to run it next time we do a migration + // in the application. A migration repository keeps the migrate order. + $this->repository->log($file, $extension); + + $this->note("Migrated: $file"); + } + + /** + * Rolls all of the currently applied migrations back. + * + * @param bool $pretend + * @return int + */ + public function reset($path, $extension = null) + { + $this->notes = []; + + $migrations = array_reverse($this->repository->getRan($extension)); + + $this->requireFiles($path, $migrations); + + $count = count($migrations); + + if ($count === 0) { + $this->note('Nothing to rollback.'); + } else { + foreach ($migrations as $migration) { + $this->runDown($migration, $extension); + } + } + + return $count; + } + + /** + * Run "down" a migration instance. + * + * @param string $file + * @param string $extension + * @return void + */ + protected function runDown($file, $extension = null) + { + // First we will get the file name of the migration so we can resolve out an + // instance of the migration. Once we get an instance we can either run a + // pretend execution of the migration or we can run the real migration. + $instance = $this->resolve($file); + + $instance->down(); + + // Once we have successfully run the migration "down" we will remove it from + // the migration repository so it will be considered to have not been run + // by the application then will be able to fire by any later operation. + $this->repository->delete($file, $extension); + + $this->note("Rolled back: $file"); + } + + /** + * Get all of the migration files in a given path. + * + * @param string $path + * @return array + */ + public function getMigrationFiles($path) + { + $files = $this->files->glob($path.'/*_*.php'); + + // Once we have the array of files in the directory we will just remove the + // extension and take the basename of the file which is all we need when + // finding the migrations that haven't been run against the databases. + if ($files === false) { + return []; + } + + $files = array_map(function ($file) { + return str_replace('.php', '', basename($file)); + }, $files); + + // Once we have all of the formatted file names we will sort them and since + // they all start with a timestamp this should give us the migrations in + // the order they were actually created by the application developers. + sort($files); + + return $files; + } + + /** + * Require in all the migration files in a given path. + * + * @param string $path + * @param array $files + * @return void + */ + public function requireFiles($path, array $files) + { + foreach ($files as $file) { + $this->files->requireOnce($path.'/'.$file.'.php'); + } + } + + /** + * Resolve a migration instance from a file. + * + * @param string $file + * @return object + */ + public function resolve($file) + { + $file = implode('_', array_slice(explode('_', $file), 4)); + + $class = Str::studly($file); + + return app()->make($class); + } + + /** + * Raise a note event for the migrator. + * + * @param string $message + * @return void + */ + protected function note($message) + { + $this->notes[] = $message; + } + + /** + * Get the notes for the last operation. + * + * @return array + */ + public function getNotes() + { + return $this->notes; + } + + /** + * Resolve the database connection instance. + * + * @param string $connection + * @return \Illuminate\Database\Connection + */ + public function resolveConnection($connection) + { + return $this->resolver->connection($connection); + } + + /** + * Set the default connection name. + * + * @param string $name + * @return void + */ + public function setConnection($name) + { + if (!is_null($name)) { + $this->resolver->setDefaultConnection($name); + } + + $this->repository->setSource($name); + + $this->connection = $name; + } + + /** + * Get the migration repository instance. + * + * @return \Illuminate\Database\Migrations\MigrationRepositoryInterface + */ + public function getRepository() + { + return $this->repository; + } + + /** + * Determine if the migration repository exists. + * + * @return bool + */ + public function repositoryExists() + { + return $this->repository->repositoryExists(); + } + + /** + * Get the file system instance. + * + * @return \Illuminate\Filesystem\Filesystem + */ + public function getFilesystem() + { + return $this->files; + } +} diff --git a/src/Support/ExtensionManager.php b/src/Support/ExtensionManager.php index 96acb24e2..198b15d5f 100644 --- a/src/Support/ExtensionManager.php +++ b/src/Support/ExtensionManager.php @@ -3,6 +3,7 @@ use Flarum\Support\ServiceProvider; use Flarum\Core\Settings\SettingsRepository; use Illuminate\Contracts\Container\Container; +use Flarum\Migrations\Migrator; class ExtensionManager { @@ -43,8 +44,7 @@ class ExtensionManager $class->install(); - // run migrations - // vendor publish + $this->migrate($extension); $this->setEnabled($enabled); } @@ -69,7 +69,24 @@ class ExtensionManager $class->uninstall(); - // run migrations + $this->migrate($extension, false); + } + + protected function migrate($extension, $up = true) + { + $migrationDir = base_path('../extensions/' . $extension . '/migrations'); + + $this->app->bind('Illuminate\Database\Schema\Builder', function($container) { + return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); + }); + + $migrator = $this->app->make('Flarum\Migrations\Migrator'); + + if ($up) { + $migrator->run($migrationDir, $extension); + } else { + $migrator->reset($migrationDir, $extension); + } } protected function getEnabled()