diff --git a/README.md b/README.md index 31f03153..f893f82c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Laravel 5.8](https://img.shields.io/badge/laravel-5.8-red.svg)](https://laravel.com) [![Latest Stable Version](https://poser.pugx.org/stancl/tenancy/version)](https://packagist.org/packages/stancl/tenancy) -[![Travis CI build](https://travis-ci.com/stancl/tenancy.svg?branch=master)](https://travis-ci.com/stancl/tenancy) -[![codecov](https://codecov.io/gh/stancl/tenancy/branch/master/graph/badge.svg)](https://codecov.io/gh/stancl/tenancy) +[![Travis CI build](https://travis-ci.com/stancl/tenancy.svg?branch=1.x)](https://travis-ci.com/stancl/tenancy) +[![codecov](https://codecov.io/gh/stancl/tenancy/branch/1.x/graph/badge.svg)](https://codecov.io/gh/stancl/tenancy) ### *A Laravel multi-database tenancy package that respects your code.* @@ -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) @@ -216,6 +217,32 @@ You can use the `tenancy()` and `tenant()` helpers to resolve `Stancl\Tenancy\Te ] ``` +You can also put data into the storage during the tenant creation process: + +```php +>>> tenant()->create('dev.localhost', [ + 'plan' => 'basic' +]) +=> [ + "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", + "domain" => "dev.localhost", + "plan" => "basic", + ] +``` + +If you want to specify the tenant's database name, set the `tenancy.database_name_key` configuration key to the name of the key that is used to specify the database name in the tenant storage. You must use a name that you won't use for storing other data, so it's recommended to avoid names like `database` and use names like `_stancl_tenancy_database_name` instead. Then just give the key a value during the tenant creation process: + +```php +>>> tenant()->create('example.com', [ + '_stancl_tenancy_database_name' => 'example_com' +]) +=> [ + "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", + "domain" => "example.com", + "_stancl_tenancy_database_name" => "example_com", + ] +``` + When you create a new tenant, you can [migrate](#tenant-migrations) their database like this: ```php @@ -487,6 +514,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 +537,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 +570,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 +605,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/composer.json b/composer.json index 7712229c..c5519935 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ }, "require-dev": { "vlucas/phpdotenv": "^3.3", - "psy/psysh": "@stable", "league/flysystem-aws-s3-v3": "~1.0", "laravel/telescope": "^2.0", "laravel/framework": "5.8.*", @@ -43,6 +42,7 @@ ], "aliases": { "Tenancy": "Stancl\\Tenancy\\TenancyFacade", + "Tenant": "Stancl\\Tenancy\\TenancyFacade", "GlobalCache": "Stancl\\Tenancy\\GlobalCacheFacade" } } diff --git a/src/Commands/Install.php b/src/Commands/Install.php new file mode 100644 index 00000000..de1a81b2 --- /dev/null +++ b/src/Commands/Install.php @@ -0,0 +1,76 @@ +comment('Installing stancl/tenancy...'); + $this->callSilent('vendor:publish', [ + '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', + '--tag' => 'config', + ]); + $this->info('✔️ Created config/tenancy.php'); + + file_put_contents(app_path('Http/Kernel.php'), str_replace( + 'protected $middlewarePriority = [', + "protected \$middlewarePriority = [\n \Stancl\Tenancy\Middleware\InitializeTenancy::class,", + file_get_contents(app_path('Http/Kernel.php')) + )); + $this->info('✔️ Set middleware priority'); + + file_put_contents(base_path('routes/tenant.php'), +"info('✔️ Created routes/tenant.php'); + + $this->line(''); + $this->line("This package lets you store data about tenants either in Redis or in a relational database like MySQL. If you're going to use the database storage, you need to create a tenants table."); + if ($this->confirm('Do you want to publish the default database migration?', true)) { + $this->callSilent('vendor:publish', [ + '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', + '--tag' => 'migrations', + ]); + $this->info('✔️ Created migration.'); + } + + $this->comment('✨️ stancl/tenancy installed successfully.'); + } +} 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/Commands/TenantList.php b/src/Commands/TenantList.php index 6a8fc400..a1d2b049 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -20,16 +20,6 @@ class TenantList extends Command */ protected $description = 'List tenants.'; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - /** * Execute the console command. * diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 47bee9fc..68e0cde0 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -6,7 +6,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; -class DatabaseManager +final class DatabaseManager { public $originalDefaultConnection; @@ -19,8 +19,7 @@ class DatabaseManager public function connect(string $database) { $this->createTenantConnection($database); - $this->database->setDefaultConnection('tenant'); - $this->database->reconnect('tenant'); + $this->useConnection('tenant'); } public function connectToTenant($tenant) @@ -105,4 +104,10 @@ class DatabaseManager $database_name = $this->getDriver() === 'sqlite' ? database_path($database_name) : $database_name; config()->set(['database.connections.tenant.database' => $database_name]); } + + public function useConnection(string $connection) + { + $this->database->setDefaultConnection($connection); + $this->database->reconnect($connection); + } } diff --git a/src/Exceptions/CannotChangeUuidOrDomainException.php b/src/Exceptions/CannotChangeUuidOrDomainException.php new file mode 100644 index 00000000..52817767 --- /dev/null +++ b/src/Exceptions/CannotChangeUuidOrDomainException.php @@ -0,0 +1,8 @@ +onFail = $onFail ?: function ($e) { + $this->onFail = $onFail ?? function ($e) { throw $e; }; } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 12005e9f..76218665 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -2,8 +2,10 @@ namespace Stancl\Tenancy; +use Stancl\Tenancy\Commands\Run; use Stancl\Tenancy\Commands\Seed; use Illuminate\Cache\CacheManager; +use Stancl\Tenancy\Commands\Install; use Stancl\Tenancy\Commands\Migrate; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Commands\Rollback; @@ -21,14 +23,14 @@ class TenancyServiceProvider extends ServiceProvider */ public function boot() { - if ($this->app->runningInConsole()) { - $this->commands([ - Migrate::class, - Rollback::class, - Seed::class, - TenantList::class, - ]); - } + $this->commands([ + Run::class, + Seed::class, + Install::class, + Migrate::class, + Rollback::class, + TenantList::class, + ]); $this->publishes([ __DIR__ . '/config/tenancy.php' => config_path('tenancy.php'), diff --git a/src/TenantManager.php b/src/TenantManager.php index b76a84a8..9d5531bb 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -5,8 +5,9 @@ namespace Stancl\Tenancy; use Stancl\Tenancy\Interfaces\StorageDriver; use Stancl\Tenancy\Traits\BootstrapsTenancy; use Illuminate\Contracts\Foundation\Application; +use Stancl\Tenancy\Exceptions\CannotChangeUuidOrDomainException; -class TenantManager +final class TenantManager { use BootstrapsTenancy; @@ -29,7 +30,7 @@ class TenantManager * * @var DatabaseManager */ - protected $database; + public $database; /** * Current tenant. @@ -70,7 +71,14 @@ class TenantManager return $tenant; } - public function create(string $domain = null): array + /** + * Create a tenant. + * + * @param string $domain + * @param array $data + * @return array + */ + public function create(string $domain = null, array $data = []): array { $domain = $domain ?: $this->currentDomain(); @@ -79,6 +87,13 @@ class TenantManager } $tenant = $this->jsonDecodeArrayValues($this->storage->createTenant($domain, (string) \Webpatser\Uuid\Uuid::generate(1, $domain))); + + if ($data) { + $this->put($data, null, $tenant['uuid']); + + $tenant = array_merge($tenant, $data); + } + $this->database->create($this->getDatabaseName($tenant)); return $tenant; @@ -168,6 +183,12 @@ class TenantManager { $tenant = $tenant ?: $this->tenant; + if ($key = $this->app['config']['tenancy.database_name_key']) { + if (isset($tenant[$key])) { + return $tenant[$key]; + } + } + return $this->app['config']['tenancy.database.prefix'] . $tenant['uuid'] . $this->app['config']['tenancy.database.suffix']; } @@ -254,6 +275,15 @@ class TenantManager */ public function put($key, $value = null, string $uuid = null) { + if (in_array($key, ['uuid', 'domain'], true) || ( + is_array($key) && ( + in_array('uuid', array_keys($key), true) || + in_array('domain', array_keys($key), true) + ) + )) { + throw new CannotChangeUuidOrDomainException; + } + if (\is_null($uuid)) { if (! isset($this->tenant['uuid'])) { throw new \Exception('No UUID supplied (and no tenant is currently identified).'); diff --git a/src/Traits/BootstrapsTenancy.php b/src/Traits/BootstrapsTenancy.php index 5f2b74d3..b7188968 100644 --- a/src/Traits/BootstrapsTenancy.php +++ b/src/Traits/BootstrapsTenancy.php @@ -9,6 +9,8 @@ use Stancl\Tenancy\Exceptions\PhpRedisNotInstalledException; trait BootstrapsTenancy { + use TenantManagerEvents; + public $originalSettings = []; /** * Was tenancy initialized/bootstrapped? @@ -19,26 +21,55 @@ trait BootstrapsTenancy public function bootstrap() { + $prevented = $this->event('bootstrapping'); $this->initialized = true; - $this->switchDatabaseConnection(); - if ($this->app['config']['tenancy.redis.tenancy']) { - $this->setPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + if (! $prevented->contains('database')) { + $this->switchDatabaseConnection(); } - $this->tagCache(); - $this->suffixFilesystemRootPaths(); + + if (! $prevented->contains('redis')) { + if ($this->app['config']['tenancy.redis.tenancy']) { + $this->setPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + } + } + + if (! $prevented->contains('cache')) { + $this->tagCache(); + } + + if (! $prevented->contains('filesystem')) { + $this->suffixFilesystemRootPaths(); + } + + $this->event('bootstrapped'); } public function end() { + $prevented = $this->event('ending'); + $this->initialized = false; - $this->disconnectDatabase(); - if ($this->app['config']['tenancy.redis.tenancy']) { - $this->resetPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + if (! $prevented->contains('database')) { + $this->disconnectDatabase(); } - $this->untagCache(); - $this->resetFileSystemRootPaths(); + + if (! $prevented->contains('redis')) { + if ($this->app['config']['tenancy.redis.tenancy']) { + $this->resetPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + } + } + + if (! $prevented->contains('cache')) { + $this->untagCache(); + } + + if (! $prevented->contains('filesystem')) { + $this->resetFileSystemRootPaths(); + } + + $this->event('ended'); } public function switchDatabaseConnection() diff --git a/src/Traits/TenantManagerEvents.php b/src/Traits/TenantManagerEvents.php new file mode 100644 index 00000000..416ebd72 --- /dev/null +++ b/src/Traits/TenantManagerEvents.php @@ -0,0 +1,85 @@ + [], + 'bootstrapped' => [], + 'ending' => [], + 'ended' => [], + ]; + + /** + * Register a listener that will be executed before tenancy is bootstrapped. + * + * @param callable $callback + * @return self + */ + public function bootstrapping(callable $callback) + { + $this->listeners['bootstrapping'][] = $callback; + + return $this; + } + + /** + * Register a listener that will be executed after tenancy is bootstrapped. + * + * @param callable $callback + * @return self + */ + public function bootstrapped(callable $callback) + { + $this->listeners['bootstrapped'][] = $callback; + + return $this; + } + + /** + * Register a listener that will be executed before tenancy is ended. + * + * @param callable $callback + * @return self + */ + public function ending(callable $callback) + { + $this->listeners['ending'][] = $callback; + + return $this; + } + + /** + * Register a listener that will be executed after tenancy is ended. + * + * @param callable $callback + * @return self + */ + public function ended(callable $callback) + { + $this->listeners['ended'][] = $callback; + + return $this; + } + + /** + * Fire an event. + * + * @param string $name Event name + * @return Collection Prevented events + */ + public function event(string $name): Collection + { + return array_reduce($this->listeners[$name], function ($prevents, $listener) { + return $prevents->merge($listener($this) ?? []); + }, collect([])); + } +} diff --git a/src/config/tenancy.php b/src/config/tenancy.php index c33f8f63..6ebd7251 100644 --- a/src/config/tenancy.php +++ b/src/config/tenancy.php @@ -43,4 +43,5 @@ return [ ], 'queue_database_creation' => false, 'queue_database_deletion' => false, + 'database_name_key' => null, ]; diff --git a/test b/test index 0d11eef3..ff55e60b 100755 --- a/test +++ b/test @@ -1,26 +1,10 @@ -#!/usr/bin/env python3 -from os import system -import argparse +#!/bin/bash +set -e -system('docker-compose up -d') - -parser = argparse.ArgumentParser() -parser.add_argument("--variants", default='1,2', - 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 "$@" +docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/ diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 3998719d..df90a21b 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,209 @@ 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'); + } + + /** @test */ + public function install_command_works() + { + if (! is_dir($dir = app_path('Http'))) { + mkdir($dir, 0777, true); + } + if (! is_dir($dir = base_path('routes'))) { + mkdir($dir, 0777, true); + } + + file_put_contents(app_path('Http/Kernel.php'), " [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + 'throttle:60,1', + 'bindings', + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected \$routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + + /** + * The priority-sorted list of middleware. + * + * This forces non-global middleware to always be in the given order. + * + * @var array + */ + protected \$middlewarePriority = [ + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\Authenticate::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Illuminate\Auth\Middleware\Authorize::class, + ]; +} +"); + + $this->artisan('tenancy:install') + ->expectsQuestion('Do you want to publish the default database migration?', 'yes'); + $this->assertFileExists(base_path('routes/tenant.php')); + $this->assertFileExists(base_path('config/tenancy.php')); + $this->assertSame(" [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + 'throttle:60,1', + 'bindings', + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected \$routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + + /** + * The priority-sorted list of middleware. + * + * This forces non-global middleware to always be in the given order. + * + * @var array + */ + protected \$middlewarePriority = [ + \Stancl\Tenancy\Middleware\InitializeTenancy::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\Authenticate::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Illuminate\Auth\Middleware\Authorize::class, + ]; +} +", \file_get_contents(app_path('Http/Kernel.php'))); } } 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/FacadeTest.php b/tests/FacadeTest.php index a218f0dd..45da854c 100644 --- a/tests/FacadeTest.php +++ b/tests/FacadeTest.php @@ -2,6 +2,7 @@ namespace Stancl\Tenancy\Tests; +use Tenant; use Tenancy; class FacadeTest extends TestCase @@ -15,4 +16,14 @@ class FacadeTest extends TestCase $this->assertSame('bar', Tenancy::get('foo')); $this->assertSame('xyz', Tenancy::get('abc')); } + + /** @test */ + public function tenant_manager_can_be_accessed_using_the_Tenant_facade() + { + tenancy()->put('foo', 'bar'); + Tenant::put('abc', 'xyz'); + + $this->assertSame('bar', Tenant::get('foo')); + $this->assertSame('xyz', Tenant::get('abc')); + } } diff --git a/tests/TenantManagerEventsTest.php b/tests/TenantManagerEventsTest.php new file mode 100644 index 00000000..2fcd74b1 --- /dev/null +++ b/tests/TenantManagerEventsTest.php @@ -0,0 +1,137 @@ +create('foo.localhost')['uuid']; + + Tenancy::bootstrapping(function ($tenantManager) use ($uuid) { + if ($tenantManager->tenant['uuid'] === $uuid) { + config(['tenancy.foo' => 'bar']); + } + }); + + $this->assertSame(null, config('tenancy.foo')); + tenancy()->init('foo.localhost'); + $this->assertSame('bar', config('tenancy.foo')); + } + + /** @test */ + public function bootstrapped_event_works() + { + $uuid = tenant()->create('foo.localhost')['uuid']; + + Tenancy::bootstrapped(function ($tenantManager) use ($uuid) { + if ($tenantManager->tenant['uuid'] === $uuid) { + config(['tenancy.foo' => 'bar']); + } + }); + + $this->assertSame(null, config('tenancy.foo')); + tenancy()->init('foo.localhost'); + $this->assertSame('bar', config('tenancy.foo')); + } + + /** @test */ + public function ending_event_works() + { + $uuid = tenant()->create('foo.localhost')['uuid']; + + Tenancy::ending(function ($tenantManager) use ($uuid) { + if ($tenantManager->tenant['uuid'] === $uuid) { + config(['tenancy.foo' => 'bar']); + } + }); + + $this->assertSame(null, config('tenancy.foo')); + tenancy()->init('foo.localhost'); + $this->assertSame(null, config('tenancy.foo')); + tenancy()->end(); + $this->assertSame('bar', config('tenancy.foo')); + } + + /** @test */ + public function ended_event_works() + { + $uuid = tenant()->create('foo.localhost')['uuid']; + + Tenancy::ended(function ($tenantManager) use ($uuid) { + if ($tenantManager->tenant['uuid'] === $uuid) { + config(['tenancy.foo' => 'bar']); + } + }); + + $this->assertSame(null, config('tenancy.foo')); + tenancy()->init('foo.localhost'); + $this->assertSame(null, config('tenancy.foo')); + tenancy()->end(); + $this->assertSame('bar', config('tenancy.foo')); + } + + /** @test */ + public function event_returns_a_collection() + { + // Note: The event() method should not be called by your code. + tenancy()->bootstrapping(function ($tenancy) { + return ['database']; + }); + tenancy()->bootstrapping(function ($tenancy) { + return ['redis', 'cache']; + }); + + $prevents = tenancy()->event('bootstrapping'); + $this->assertEquals(collect(['database', 'redis', 'cache']), $prevents); + } + + /** @test */ + public function database_can_be_reconnected_using_event_hooks() + { + config(['database.connections.tenantabc' => [ + 'driver' => 'sqlite', + 'database' => database_path('some_special_database.sqlite'), + ]]); + + $uuid = Tenant::create('abc.localhost')['uuid']; + + Tenancy::bootstrapping(function ($tenancy) use ($uuid) { + if ($tenancy->tenant['uuid'] === $uuid) { + $tenancy->database->useConnection('tenantabc'); + + return ['database']; + } + }); + + $this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']); + tenancy()->init('abc.localhost'); + $this->assertSame('tenantabc', \DB::connection()->getConfig()['name']); + } + + /** @test */ + public function database_cannot_be_reconnected_without_using_prevents() + { + config(['database.connections.tenantabc' => [ + 'driver' => 'sqlite', + 'database' => database_path('some_special_database.sqlite'), + ]]); + + $uuid = Tenant::create('abc.localhost')['uuid']; + + Tenancy::bootstrapping(function ($tenancy) use ($uuid) { + if ($tenancy->tenant['uuid'] === $uuid) { + $tenancy->database->useConnection('tenantabc'); + // return ['database']; + } + }); + + $this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']); + tenancy()->init('abc.localhost'); + $this->assertSame('tenant', \DB::connection()->getConfig()['name']); + } +} diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php index 050c729a..453730ea 100644 --- a/tests/TenantManagerTest.php +++ b/tests/TenantManagerTest.php @@ -4,6 +4,7 @@ namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; +use Stancl\Tenancy\Exceptions\CannotChangeUuidOrDomainException; class TenantManagerTest extends TestCase { @@ -194,4 +195,50 @@ class TenantManagerTest extends TestCase $tenant2 = tenant()->create('bar.localhost'); $this->assertEqualsCanonicalizing([$tenant1, $tenant2], tenant()->all()->toArray()); } + + /** @test */ + public function properites_can_be_passed_in_the_create_method() + { + $data = ['plan' => 'free', 'subscribed_until' => '2020-01-01']; + $tenant = tenant()->create('foo.localhost', $data); + + $tenant_data = $tenant; + unset($tenant_data['uuid']); + unset($tenant_data['domain']); + + $this->assertSame($data, $tenant_data); + } + + /** @test */ + public function database_name_can_be_passed_in_the_create_method() + { + $database = 'abc'; + config(['tenancy.database_name_key' => '_stancl_tenancy_database_name']); + + $tenant = tenant()->create('foo.localhost', [ + '_stancl_tenancy_database_name' => $database, + ]); + + $this->assertSame($database, tenant()->getDatabaseName($tenant)); + } + + /** @test */ + public function uuid_and_domain_cannot_be_changed() + { + $tenant = tenant()->create('foo.localhost'); + + $this->expectException(CannotChangeUuidOrDomainException::class); + tenant()->put('uuid', 'foo', $tenant['uuid']); + + $this->expectException(CannotChangeUuidOrDomainException::class); + tenant()->put(['uuid' => 'foo'], null, $tenant['uuid']); + + tenancy()->init('foo.localhost'); + + $this->expectException(CannotChangeUuidOrDomainException::class); + tenant()->put('uuid', 'foo'); + + $this->expectException(CannotChangeUuidOrDomainException::class); + tenant()->put(['uuid' => 'foo']); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 7cc55500..0183cc5a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -60,7 +60,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', @@ -86,24 +85,11 @@ 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 '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', - ]); - } } protected function getPackageProviders($app) @@ -115,6 +101,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { return [ 'Tenancy' => \Stancl\Tenancy\TenancyFacade::class, + 'Tenant' => \Stancl\Tenancy\TenancyFacade::class, 'GlobalCache' => \Stancl\Tenancy\GlobalCacheFacade::class, ]; } @@ -130,6 +117,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);