diff --git a/README.md b/README.md index 31f03153..79a6dca4 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ You won't have to change a thing in your application's code.\* * [Artisan commands](#artisan-commands) - [`tenants:list`](#-tenants-list-) - [`tenants:migrate`, `tenants:rollback`, `tenants:seed`](#-tenants-migrate----tenants-rollback----tenants-seed-) + - [Running your commands for tenants](#running-your-commands-for-tenants) + [Tenant migrations](#tenant-migrations) * [Testing](#testing) - [Tips](#tips) @@ -487,6 +488,7 @@ Available commands for the "tenants" namespace: tenants:list List tenants. tenants:migrate Run migrations for tenant(s) tenants:rollback Rollback migrations for tenant(s). + tenants:run Run a command for tenant(s). tenants:seed Seed tenant database(s). ``` @@ -509,6 +511,18 @@ Tenant: 8075a580-1cb8-11e9-8822-49c5d8f8ff23 (laravel.localhost) Database seeding completed successfully. ``` +### Running your commands for tenants + +You can use the `tenants:run` command to run your own commands for tenants. + +If your command's signature were `email:send {user} {--queue} {--subject} {body}`, you would run this command like this: + +``` +$ artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..." +``` + +The `=` separates the argument/option name from its value, but you can still use `=` in the argument's value. + ### Tenant migrations Tenant migrations are located in `database/migrations/tenant`, so you should move your tenant migrations there. @@ -530,6 +544,9 @@ To do this automatically, you can make this part of your `TestCase::setUp()` met ## HTTPS certificates +
+Click to expand/collapse + HTTPS certificates are very easy to deal with if you use the `yourclient1.yourapp.com`, `yourclient2.yourapp.com` model. You can use a wildcard HTTPS certificate. If you use the model where second level domains are used, there are multiple ways you can solve this. @@ -562,6 +579,8 @@ Creating this config dynamically from PHP is not easy, but is probably feasible. However, you still need to reload nginx configuration to apply the changes to configuration. This is problematic and I'm not sure if there is a simple and secure way to do this from PHP. +
+ # Development ## Running tests diff --git a/src/Commands/Run.php b/src/Commands/Run.php new file mode 100644 index 00000000..d3e00861 --- /dev/null +++ b/src/Commands/Run.php @@ -0,0 +1,66 @@ +initialized) { + $previous_tenants_domain = tenant('domain'); + } + + tenant()->all($this->option('tenants'))->each(function ($tenant) { + $this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})"); + tenancy()->init($tenant['domain']); + + $callback = function ($prefix = '') { + return function ($arguments, $argument) use ($prefix) { + [$key, $value] = explode('=', $argument, 2); + $arguments[$prefix . $key] = $value; + + return $arguments; + }; + }; + + // Turns ['foo=bar', 'abc=xyz=zzz'] into ['foo' => 'bar', 'abc' => 'xyz=zzz'] + $arguments = array_reduce($this->option('argument'), $callback(), []); + + // Turns ['foo=bar', 'abc=xyz=zzz'] into ['--foo' => 'bar', '--abc' => 'xyz=zzz'] + $options = array_reduce($this->option('option'), $callback('--'), []); + + // Run command + $this->call($this->argument('commandname'), array_merge($arguments, $options)); + + tenancy()->end(); + }); + + if ($tenancy_was_initialized) { + tenancy()->init($previous_tenants_domain); + } + } +} diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 2300bee8..18e6774e 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -2,6 +2,7 @@ namespace Stancl\Tenancy; +use Stancl\Tenancy\Commands\Run; use Stancl\Tenancy\Commands\Seed; use Illuminate\Cache\CacheManager; use Stancl\Tenancy\Commands\Migrate; @@ -24,9 +25,10 @@ class TenancyServiceProvider extends ServiceProvider { if ($this->app->runningInConsole()) { $this->commands([ + Run::class, + Seed::class, Migrate::class, Rollback::class, - Seed::class, TenantList::class, ]); } diff --git a/test b/test index 710b1824..ae14fb61 100755 --- a/test +++ b/test @@ -1,26 +1,11 @@ -#!/usr/bin/env python3 -from os import system -import argparse +#!/bin/bash -system('docker-compose up -d') - -parser = argparse.ArgumentParser() -parser.add_argument("--variants", default='1,2,3', - help="Comma-separated values. Which test variants should be run.") -args, other = parser.parse_known_args() - -variants = args.variants.split(',') - -for variant in variants: - filename_base = "phpunit_var_" + variant - with open('phpunit.xml', 'r') as inp, open(filename_base + '.xml', 'w') as out: - out.write(inp.read().replace('"STANCL_TENANCY_TEST_VARIANT" value="1"', - '"STANCL_TENANCY_TEST_VARIANT" value="%s"' % variant)) - - print("Test variant: %s\n" % variant) - - system('docker-compose exec test vendor/bin/phpunit --configuration "%s" --coverage-php %s %s' - % (filename_base + '.xml', 'coverage/' + filename_base + '.cov', ' '.join(other))) - -# todo delete folder contents first? -system("docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/") +# for development +docker-compose up -d +printf "Variant 1\n\n" +TENANCY_TEST_REDIS_TENANCY=1 TENANCY_TEST_REDIS_CLIENT=phpredis docker-compose exec test vendor/bin/phpunit --coverage-php coverage/1.cov "$@" +printf "Variant 2\n\n" +TENANCY_TEST_REDIS_TENANCY=0 TENANCY_TEST_REDIS_CLIENT=predis docker-compose exec test vendor/bin/phpunit --coverage-php coverage/2.cov "$@" +printf "Variant 3\n\n" +TENANCY_TEST_REDIS_TENANCY=1 TENANCY_TEST_REDIS_CLIENT=phpredis TENANCY_TEST_STORAGE_DRIVER=db docker-compose exec test vendor/bin/phpunit --coverage-php coverage/3.cov "$@" +docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/ diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 3998719d..d26b71c1 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -76,7 +76,7 @@ class CommandsTest extends TestCase } /** @test */ - public function database_connection_is_switched_to_default_after_migrating_or_seeding_or_rolling_back() + public function database_connection_is_switched_to_default() { $originalDBName = DB::connection()->getDatabaseName(); @@ -88,13 +88,29 @@ class CommandsTest extends TestCase Artisan::call('tenants:rollback'); $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); + + $this->run_commands_works(); + $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); } /** @test */ - public function database_connection_is_switched_to_default_after_migrating_or_seeding_or_rolling_back_when_tenancy_has_been_initialized() + public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized() { tenancy()->init('localhost'); - $this->database_connection_is_switched_to_default_after_migrating_or_seeding_or_rolling_back(); + $this->database_connection_is_switched_to_default(); + } + + /** @test */ + public function run_commands_works() + { + $uuid = tenant()->create('run.localhost')['uuid']; + + Artisan::call('tenants:migrate', ['--tenants' => $uuid]); + + $this->artisan("tenants:run foo --tenants=$uuid --argument='a=foo' --option='b=bar' --option='c=xyz'") + ->expectsOutput("User's name is Test command") + ->expectsOutput('foo') + ->expectsOutput('xyz'); } } diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php new file mode 100644 index 00000000..a0d82a84 --- /dev/null +++ b/tests/Etc/ConsoleKernel.php @@ -0,0 +1,17 @@ + 999, + 'name' => 'Test command', + 'email' => 'test@command.com', + 'password' => bcrypt('password'), + ]); + + $this->line("User's name is " . User::find(999)->name); + $this->line($this->argument('a')); + $this->line($this->option('c')); + } +} + +class User extends \Illuminate\Database\Eloquent\Model +{ + protected $guarded = []; +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 0fd30934..944943b4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,7 +26,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Artisan::call('migrate:fresh', [ '--path' => __DIR__ . '/../src/assets/migrations' ]); - dd(Artisan::output()); + // dd(Artisan::output()); // $this->loadLaravelMigrations(); // $this->loadMigrationsFrom(__DIR__ . '/../src/assets/migrations'); @@ -62,7 +62,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase } $app['config']->set([ - 'database.redis.client' => 'phpredis', 'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.options.prefix' => 'foo', @@ -88,33 +87,22 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'public', 's3', ], - 'tenancy.redis.tenancy' => true, + 'tenancy.redis.tenancy' => env('TENANCY_TEST_REDIS_TENANCY', true), + 'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'), 'tenancy.redis.prefixed_connections' => ['default'], 'tenancy.migrations_directory' => database_path('../migrations'), ]); - switch ((string) env('STANCL_TENANCY_TEST_VARIANT', '1')) { - case '3': - $app['config']->set([ - 'tenancy.redis.tenancy' => true, - 'database.redis.client' => 'phpredis', - 'tenancy.storage_driver' => DatabaseStorageDriver::class, - ]); - tenancy()->setStorageDriver(DatabaseStorageDriver::class); - - break; - case '2': - $app['config']->set([ - 'tenancy.redis.tenancy' => false, - 'database.redis.client' => 'predis', - ]); - break; - default: - $app['config']->set([ - 'tenancy.redis.tenancy' => true, - 'database.redis.client' => 'phpredis', - ]); + if (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') { + $app['config']->set([ + 'tenancy.redis.tenancy' => true, + 'database.redis.client' => 'phpredis', + 'tenancy.storage_driver' => DatabaseStorageDriver::class, + ]); + + tenancy()->setStorageDriver(DatabaseStorageDriver::class); } + } protected function getPackageProviders($app) @@ -141,6 +129,17 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase $app->singleton('Illuminate\Contracts\Http\Kernel', Etc\HttpKernel::class); } + /** + * Resolve application Console Kernel implementation. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function resolveApplicationConsoleKernel($app) + { + $app->singleton('Illuminate\Contracts\Console\Kernel', Etc\ConsoleKernel::class); + } + public function randomString(int $length = 10) { return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);