diff --git a/README.md b/README.md index 7b7faab6..da359983 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Documentation can be found here: https://tenancy.samuelstancl.me/docs/v2/ The repository with the documentation source code can be found here: [stancl/tenancy-docs](https://github.com/stancl/tenancy-docs). +### [Need help?](https://github.com/stancl/tenancy/blob/2.x/SUPPORT.md) + ### Credits - Created by [Samuel Štancl](https://github.com/stancl) diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..8f782e51 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,7 @@ +# Get Support + +If you need help with implementing the package, you can: +- Open an [issue here on GitHub](https://github.com/stancl/tenancy/issues/new?assignees=stancl&labels=support&template=support-question.md&title=) +- Message me (`@stancl`) on the [Unofficial Laravel Discord](https://discord.gg/zGVGFAd), in the `#stancl_tenancy` channel +- Contact me on Telegram: [@samuelstancl](https://t.me/samuelstancl) +- Send me an email: [samuel.stancl@gmail.com](mailto:samuel.stancl@gmail.com) diff --git a/assets/config.php b/assets/config.php index 5e285da3..9155dae4 100644 --- a/assets/config.php +++ b/assets/config.php @@ -44,6 +44,7 @@ return [ 'filesystem' => [ // https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/ 'suffix_base' => 'tenant', 'suffix_storage_path' => true, // Note: Disabling this will likely break local disk tenancy. Only disable this if you're using an external file storage service like S3. + 'asset_helper_tenancy' => true, // should asset() be automatically tenant-aware. You may want to disable this if you use tools like Horizon. // Disks which should be suffixed with the suffix_base + tenant id. 'disks' => [ 'local', @@ -93,11 +94,16 @@ return [ // 'paypal_api_key' => 'services.paypal.api_key', ], 'home_url' => '/app', + 'create_database' => true, 'queue_database_creation' => false, 'migrate_after_creation' => false, // run migrations after creating a tenant + 'migration_parameters' => [ + // '--force' => true, // force database migrations + ], 'seed_after_migration' => false, // should the seeder run after automatic migration 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', // root seeder class to run after automatic migrations, e.g.: 'DatabaseSeeder' + // '--force' => true, // force database seeder ], 'queue_database_deletion' => false, 'delete_database_after_tenant_deletion' => false, // delete the tenant's database after deleting the tenant diff --git a/src/Commands/Install.php b/src/Commands/Install.php index c1012d94..426bf1d3 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -33,7 +33,7 @@ class Install extends Command $this->callSilent('vendor:publish', [ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', '--tag' => 'config', - ]); + ]); $this->info('✔️ Created config/tenancy.php'); $newKernel = str_replace( diff --git a/src/Contracts/Future/CanSetConnection.php b/src/Contracts/Future/CanSetConnection.php new file mode 100644 index 00000000..3c4d527b --- /dev/null +++ b/src/Contracts/Future/CanSetConnection.php @@ -0,0 +1,13 @@ +getDriver($this->getBaseConnection($tenant->getConnectionName())); + $driver = $this->getDriver($this->getBaseConnection($connectionName = $tenant->getConnectionName())); $databaseManagers = $this->app['config']['tenancy.database_managers']; @@ -236,7 +237,13 @@ class DatabaseManager throw new DatabaseManagerNotRegisteredException($driver); } - return $this->app[$databaseManagers[$driver]]; + $databaseManager = $this->app[$databaseManagers[$driver]]; + + if ($connectionName !== 'tenant' && $databaseManager instanceof CanSetConnection) { + $databaseManager->setConnection($connectionName); + } + + return $databaseManager; } /** diff --git a/src/Jobs/QueuedTenantDatabaseMigrator.php b/src/Jobs/QueuedTenantDatabaseMigrator.php index 5ea12656..c71696cc 100644 --- a/src/Jobs/QueuedTenantDatabaseMigrator.php +++ b/src/Jobs/QueuedTenantDatabaseMigrator.php @@ -19,9 +19,13 @@ class QueuedTenantDatabaseMigrator implements ShouldQueue /** @var string */ protected $tenantId; - public function __construct(Tenant $tenant) + /** @var array */ + protected $migrationParameters = []; + + public function __construct(Tenant $tenant, $migrationParameters = []) { $this->tenantId = $tenant->id; + $this->migrationParameters = $migrationParameters; } /** @@ -33,6 +37,6 @@ class QueuedTenantDatabaseMigrator implements ShouldQueue { Artisan::call('tenants:migrate', [ '--tenants' => [$this->tenantId], - ]); + ] + $this->migrationParameters); } } diff --git a/src/Middleware/PreventAccessFromTenantDomains.php b/src/Middleware/PreventAccessFromTenantDomains.php index 92fda549..654babd5 100644 --- a/src/Middleware/PreventAccessFromTenantDomains.php +++ b/src/Middleware/PreventAccessFromTenantDomains.php @@ -55,7 +55,7 @@ class PreventAccessFromTenantDomains // groups have a `tenancy` middleware group inside them $middlewareGroups = Router::getMiddlewareGroups(); foreach ($route->gatherMiddleware() as $inner) { - if (isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) { + if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) { return true; } } diff --git a/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php b/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php index 5d417f8b..35ea5b2b 100644 --- a/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php @@ -4,7 +4,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenancyBootstrappers; -use Illuminate\Contracts\Foundation\Application; +use Illuminate\Filesystem\FilesystemAdapter; +use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Tenant; @@ -43,23 +44,27 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper } // asset() - if ($this->originalPaths['asset_url']) { - $this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix"; - $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); - } else { - $this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); + if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) { + if ($this->originalPaths['asset_url']) { + $this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix"; + $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); + } else { + $this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => ''])); + } } // Storage facade foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - $this->originalPaths['disks'][$disk] = Storage::disk($disk)->getAdapter()->getPathPrefix(); + /** @var FilesystemAdapter $filesystemDisk */ + $filesystemDisk = Storage::disk($disk); + $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix(); if ($root = str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) { - Storage::disk($disk)->getAdapter()->setPathPrefix($root); + $filesystemDisk->getAdapter()->setPathPrefix($root); } else { $root = $this->app['config']["filesystems.disks.{$disk}.root"]; - Storage::disk($disk)->getAdapter()->setPathPrefix($root . "/{$suffix}"); + $filesystemDisk->getAdapter()->setPathPrefix($root . "/{$suffix}"); } } } @@ -75,7 +80,10 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper // Storage facade foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - Storage::disk($disk)->getAdapter()->setPathPrefix($this->originalPaths['disks'][$disk]); + /** @var FilesystemAdapter $filesystemDisk */ + $filesystemDisk = Storage::disk($disk); + + $filesystemDisk->getAdapter()->setPathPrefix($this->originalPaths['disks'][$disk]); } } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 604d8a09..b5c41c73 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -109,11 +109,21 @@ class TenancyServiceProvider extends ServiceProvider // Queue tenancy $this->app['events']->listen(\Illuminate\Queue\Events\JobProcessing::class, function ($event) { - if (array_key_exists('tenant_id', $event->job->payload())) { - if (! tenancy()->initialized) { // dispatchNow - tenancy()->initialize(tenancy()->find($event->job->payload()['tenant_id'])); - } + $tenantId = $event->job->payload()['tenant_id'] ?? null; + + // The job is not tenant-aware + if (! $tenantId) { + return; } + + // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + if (tenancy()->initialized && tenant('id') === $tenantId) { + return; + } + + // Tenancy was either not initialized, or initialized for a different tenant. + // Therefore, we initialize it for the correct tenant. + tenancy()->initById($tenantId); }); } } diff --git a/src/Tenant.php b/src/Tenant.php index 8e6a900b..38ac7261 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -41,7 +41,7 @@ class Tenant implements ArrayAccess /** @var Repository */ protected $config; - /** @var StorageDriver */ + /** @var StorageDriver|CanDeleteKeys */ protected $storage; /** @var TenantManager */ @@ -233,8 +233,6 @@ class Tenant implements ArrayAccess $this->manager->createTenant($this); } - $this->persisted = true; - return $this; } diff --git a/src/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/TenantDatabaseManagers/MySQLDatabaseManager.php index 548f628d..f6c4ef96 100644 --- a/src/TenantDatabaseManagers/MySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/MySQLDatabaseManager.php @@ -5,34 +5,46 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenantDatabaseManagers; use Illuminate\Contracts\Config\Repository; -use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager; +use Illuminate\Database\Connection; +use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -class MySQLDatabaseManager implements TenantDatabaseManager +class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { - /** @var \Illuminate\Database\Connection */ - protected $database; + /** @var string */ + protected $connection; - public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager) + public function __construct(Repository $config) { - $this->database = $databaseManager->connection($config['tenancy.database_manager_connections.mysql']); + $this->connection = $config->get('tenancy.database_manager_connections.mysql'); + } + + protected function database(): Connection + { + return DB::connection($this->connection); + } + + public function setConnection(string $connection): void + { + $this->connection = $connection; } public function createDatabase(string $name): bool { - $charset = $this->database->getConfig('charset'); - $collation = $this->database->getConfig('collation'); + $charset = $this->database()->getConfig('charset'); + $collation = $this->database()->getConfig('collation'); - return $this->database->statement("CREATE DATABASE `$name` CHARACTER SET `$charset` COLLATE `$collation`"); + return $this->database()->statement("CREATE DATABASE `$name` CHARACTER SET `$charset` COLLATE `$collation`"); } public function deleteDatabase(string $name): bool { - return $this->database->statement("DROP DATABASE `$name`"); + return $this->database()->statement("DROP DATABASE `$name`"); } public function databaseExists(string $name): bool { - return (bool) $this->database->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); + return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); } } diff --git a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php index 9fe67297..fc21668e 100644 --- a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php @@ -5,31 +5,43 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenantDatabaseManagers; use Illuminate\Contracts\Config\Repository; -use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager; +use Illuminate\Database\Connection; +use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -class PostgreSQLDatabaseManager implements TenantDatabaseManager +class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { - /** @var \Illuminate\Database\Connection */ - protected $database; + /** @var string */ + protected $connection; - public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager) + public function __construct(Repository $config) { - $this->database = $databaseManager->connection($config['tenancy.database_manager_connections.pgsql']); + $this->connection = $config->get('tenancy.database_manager_connections.pgsql'); + } + + protected function database(): Connection + { + return DB::connection($this->connection); + } + + public function setConnection(string $connection): void + { + $this->connection = $connection; } public function createDatabase(string $name): bool { - return $this->database->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); + return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); } public function deleteDatabase(string $name): bool { - return $this->database->statement("DROP DATABASE \"$name\""); + return $this->database()->statement("DROP DATABASE \"$name\""); } public function databaseExists(string $name): bool { - return (bool) $this->database->select("SELECT datname FROM pg_database WHERE datname = '$name'"); + return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'"); } } diff --git a/src/TenantManager.php b/src/TenantManager.php index abf3b766..d0d8b428 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -73,16 +73,18 @@ class TenantManager $this->storage->createTenant($tenant); + $tenant->persisted = true; + /** @var \Illuminate\Contracts\Queue\ShouldQueue[]|callable[] $afterCreating */ $afterCreating = []; if ($this->shouldMigrateAfterCreation()) { $afterCreating[] = $this->databaseCreationQueued() - ? new QueuedTenantDatabaseMigrator($tenant) + ? new QueuedTenantDatabaseMigrator($tenant, $this->getMigrationParameters()) : function () use ($tenant) { $this->artisan->call('tenants:migrate', [ '--tenants' => [$tenant['id']], - ]); + ] + $this->getMigrationParameters()); }; } @@ -96,7 +98,9 @@ class TenantManager }; } - $this->database->createDatabase($tenant, $afterCreating); + if ($this->shouldCreateDatabase($tenant)) { + $this->database->createDatabase($tenant, $afterCreating); + } $this->event('tenant.created', $tenant); @@ -376,6 +380,15 @@ class TenantManager return array_diff_key($this->app['config']['tenancy.bootstrappers'], array_flip($except)); } + public function shouldCreateDatabase(Tenant $tenant): bool + { + if (array_key_exists('_tenancy_create_database', $tenant->data)) { + return $tenant->data['_tenancy_create_database']; + } + + return $this->app['config']['tenancy.create_database'] ?? true; + } + public function shouldMigrateAfterCreation(): bool { return $this->app['config']['tenancy.migrate_after_creation'] ?? false; @@ -401,6 +414,11 @@ class TenantManager return $this->app['config']['tenancy.seeder_parameters'] ?? []; } + public function getMigrationParameters() + { + return $this->app['config']['tenancy.migration_parameters'] ?? []; + } + /** * Add an event listener. * diff --git a/src/Traits/DealsWithMigrations.php b/src/Traits/DealsWithMigrations.php index a51bfa57..f730cf07 100644 --- a/src/Traits/DealsWithMigrations.php +++ b/src/Traits/DealsWithMigrations.php @@ -12,6 +12,6 @@ trait DealsWithMigrations return parent::getMigrationPaths(); } - return [config('tenancy.migrations_directory', database_path('migrations/tenant'))]; + return config('tenancy.migration_paths', [config('tenancy.migrations_directory') ?? database_path('migrations/tenant')]); } } diff --git a/src/helpers.php b/src/helpers.php index 101d43df..58aa8ccb 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -18,14 +18,19 @@ if (! function_exists('tenancy')) { } if (! function_exists('tenant')) { - /** @return Tenant|mixed */ + /** + * Get a key from the current tenant's storage. + * + * @param string|null $key + * @return Tenant|mixed + */ function tenant($key = null) { - if (! is_null($key)) { - return optional(app(Tenant::class))->get($key) ?? null; + if (is_null($key)) { + return app(Tenant::class); } - return app(Tenant::class); + return optional(app(Tenant::class))->get($key) ?? null; } } @@ -52,7 +57,7 @@ if (! function_exists('global_cache')) { } if (! function_exists('tenant_route')) { - function tenant_route(string $route, array $parameters = [], string $domain = null): string + function tenant_route($route, $parameters = [], string $domain = null) { $domain = $domain ?? request()->getHost(); diff --git a/src/routes.php b/src/routes.php index 7f1a5310..093f5de4 100644 --- a/src/routes.php +++ b/src/routes.php @@ -3,7 +3,7 @@ declare(strict_types=1); Route::middleware(['tenancy'])->group(function () { - Route::get('/tenancy/assets/{path}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') + Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') ->where('path', '(.*)') ->name('stancl.tenancy.asset'); }); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 071f929c..6bcf6fd9 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -18,7 +18,7 @@ class CommandsTest extends TestCase { parent::setUp(); - config(['tenancy.migrations_directory' => database_path('../migrations')]); + config(['tenancy.migration_paths', [database_path('../migrations')]]); } /** @test */ diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 248e7703..b9bbabad 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -68,4 +68,20 @@ class TenantAssetTest extends TestCase $this->assertSame($original, global_asset('foobar')); } + + /** @test */ + public function asset_helper_tenancy_can_be_disabled() + { + $original = asset('foo'); + + config([ + 'app.asset_url' => null, + 'tenancy.filesystem.asset_helper_tenancy' => false, + ]); + + Tenant::create('foo.localhost'); + tenancy()->init('foo.localhost'); + + $this->assertSame($original, asset('foo')); + } } diff --git a/tests/TenantManagerEventsTest.php b/tests/TenantManagerEventsTest.php index eca5077f..4dde735c 100644 --- a/tests/TenantManagerEventsTest.php +++ b/tests/TenantManagerEventsTest.php @@ -121,4 +121,18 @@ class TenantManagerEventsTest extends TestCase tenancy()->init('abc.localhost'); $this->assertSame('tenant', \DB::connection()->getConfig()['name']); } + + /** @test */ + public function tenant_is_persisted_before_the_created_hook_is_called() + { + $was_persisted = false; + + Tenancy::eventListener('tenant.created', function ($tenancy, $tenant) use (&$was_persisted) { + $was_persisted = $tenant->persisted; + }); + + Tenant::new()->save(); + + $this->assertTrue($was_persisted); + } } diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php index c3df859c..8e78a607 100644 --- a/tests/TenantManagerTest.php +++ b/tests/TenantManagerTest.php @@ -345,4 +345,38 @@ class TenantManagerTest extends TestCase $this->assertArrayHasKey('foo', $tenant->data); $this->assertArrayHasKey('abc123', $tenant->data); } + + /** @test */ + public function database_creation_can_be_disabled() + { + config(['tenancy.create_database' => false]); + + tenancy()->hook('database.creating', function () { + $this->fail(); + }); + + $tenant = Tenant::new()->save(); + + $this->assertTrue(true); + } + + /** @test */ + public function database_creation_can_be_disabled_for_specific_tenants() + { + config(['tenancy.create_database' => true]); + + tenancy()->hook('database.creating', function () { + $this->assertTrue(true); + }); + + $tenant = Tenant::new()->save(); + + tenancy()->hook('database.creating', function () { + $this->fail(); + }); + + $tenant2 = Tenant::new()->withData([ + '_tenancy_create_database' => false, + ])->save(); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2621d0a9..257964d1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -97,7 +97,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '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'), + 'tenancy.migration_paths' => [database_path('../migrations')], 'tenancy.storage_drivers.db.connection' => 'central', 'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class, 'queue.connections.central' => [