diff --git a/assets/config.php b/assets/config.php index 5624143c..46b4c6e1 100644 --- a/assets/config.php +++ b/assets/config.php @@ -100,7 +100,7 @@ return [ * The connection that will be used as a template for the dynamically created tenant connection. * Set to null to use the default connection. */ - 'based_on' => null, + 'template_connection' => null, /** * Tenant database names are created like this: @@ -108,8 +108,6 @@ return [ */ 'prefix' => 'tenant', 'suffix' => '', - - 'separate_by' => 'database', // database or schema (only supported by pgsql) ], /** @@ -197,9 +195,14 @@ return [ 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, /** - * Disable the pgsql manager above, enable the one below, and set the - * tenancy.database.separate_by config key to 'schema' if you would - * like to separate tenant DBs by schemas rather than databases. + * Use this database manager for MySQL to have a DB user created for each tenant database. + * You can customize the grants given to these users by changing the $grants property. + */ + // 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class, + + /** + * Disable the pgsql manager above, and enable the one below if you + * want to separate tenant DBs by schemas rather than databases. */ // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], @@ -252,8 +255,8 @@ return [ */ 'migrate_after_creation' => false, 'migration_parameters' => [ - // '--force' => true, // Set this to true to be able to run migrations in production - // '--path' => [], // If you need to customize paths to tenant migrations + '--force' => true, // Set this to true to be able to run migrations in production + // '--path' => [database_path('migrations/tenant')], // If you need to customize paths to tenant migrations ], /** diff --git a/phpunit.xml b/phpunit.xml index d4f07804..a1c16a21 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,7 +28,7 @@ - + diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 279a371b..4859d047 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -45,6 +45,12 @@ class Migrate extends MigrateCommand */ public function handle() { + foreach (config('tenancy.migration_parameters') as $parameter => $value) { + if (! $this->input->hasParameterOption($parameter)) { + $this->input->setOption(ltrim($parameter, '-'), $value); + } + } + if (! $this->confirmToProceed()) { return; } @@ -52,8 +58,6 @@ class Migrate extends MigrateCommand tenancy()->all($this->option('tenants'))->each(function ($tenant) { $this->line("Tenant: {$tenant['id']}"); - $this->input->setOption('database', $tenant->getConnectionName()); - $tenant->run(function () { // Migrate parent::handle(); diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index bef3a09a..eb4b0f16 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -39,7 +39,7 @@ final class MigrateFresh extends Command $tenant->run(function ($tenant) { $this->info('Dropping tables.'); $this->call('db:wipe', array_filter([ - '--database' => $tenant->getConnectionName(), + '--database' => 'tenant', '--force' => true, ])); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index eee99ddd..0a85a10e 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -45,6 +45,12 @@ class Rollback extends RollbackCommand */ public function handle() { + foreach (config('tenancy.migration_parameters') as $parameter => $value) { + if (! $this->input->hasParameterOption($parameter)) { + $this->input->setOption(ltrim($parameter, '-'), $value); + } + } + if (! $this->confirmToProceed()) { return; } @@ -52,8 +58,6 @@ class Rollback extends RollbackCommand tenancy()->all($this->option('tenants'))->each(function ($tenant) { $this->line("Tenant: {$tenant['id']}"); - $this->input->setOption('database', $tenant->getConnectionName()); - $tenant->run(function () { // Rollback parent::handle(); diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 36f59dd0..acc697e0 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -56,8 +56,6 @@ class Seed extends SeedCommand tenancy()->all($this->option('tenants'))->each(function ($tenant) { $this->line("Tenant: {$tenant['id']}"); - $this->input->setOption('database', $tenant->getConnectionName()); - $tenant->run(function () { // Seed parent::handle(); diff --git a/src/Contracts/ManagesDatabaseUsers.php b/src/Contracts/ManagesDatabaseUsers.php new file mode 100644 index 00000000..6de28a80 --- /dev/null +++ b/src/Contracts/ManagesDatabaseUsers.php @@ -0,0 +1,16 @@ +id . config('tenancy.database.suffix'); + }; + } + + public function __construct(Tenant $tenant) + { + static::__constructStatic(); + + $this->tenant = $tenant; + } + + public static function generateDatabaseNamesUsing(callable $databaseNameGenerator): void + { + static::$databaseNameGenerator = $databaseNameGenerator; + } + + public static function generateUsernamesUsing(callable $usernameGenerator): void + { + static::$usernameGenerator = $usernameGenerator; + } + + public static function generatePasswordsUsing(callable $passwordGenerator): void + { + static::$passwordGenerator = $passwordGenerator; + } + + public function getName(): ?string + { + return $this->tenant->data['_tenancy_db_name'] ?? (static::$databaseNameGenerator)($this->tenant); + } + + public function getUsername(): ?string + { + return $this->tenant->data['_tenancy_db_username'] ?? null; + } + + public function getPassword(): ?string + { + return $this->tenant->data['_tenancy_db_password'] ?? null; + } + + public function makeCredentials(): void + { + $this->tenant->data['_tenancy_db_name'] = $this->getName() ?? (static::$databaseNameGenerator)($this->tenant); + + if ($this->manager() instanceof ManagesDatabaseUsers) { + $this->tenant->data['_tenancy_db_username'] = $this->getUsername() ?? (static::$usernameGenerator)($this->tenant); + $this->tenant->data['_tenancy_db_password'] = $this->getPassword() ?? (static::$passwordGenerator)($this->tenant); + } + } + + public function getTemplateConnectionName(): string + { + return $this->tenant->data['_tenancy_db_connection'] + ?? config('tenancy.database.template_connection') + ?? DatabaseManager::$originalDefaultConnectionName; + } + + /** + * Tenant's own database connection config. + */ + public function connection(): array + { + $template = $this->getTemplateConnectionName(); + + $templateConnection = config("database.connections.{$template}"); + + $databaseName = $this->getName(); + if (($manager = $this->manager()) instanceof ModifiesDatabaseNameForConnection) { + /** @var ModifiesDatabaseNameForConnection $manager */ + $databaseName = $manager->getDatabaseNameForConnection($databaseName); + } + + return array_merge($templateConnection, $this->tenantConfig(), [ + $this->manager()->getSeparator() => $databaseName, + ]); + } + + /** + * Additional config for the database connection, specific to this tenant. + */ + public function tenantConfig(): array + { + $dbConfig = array_filter(array_keys($this->tenant->data), function ($key) { + return Str::startsWith($key, '_tenancy_db_'); + }); + + // Remove DB name because we set that separately + if (($pos = array_search('_tenancy_db_name', $dbConfig)) !== false) { + unset($dbConfig[$pos]); + } + + // Remove DB connection because that's not used inside the array + if (($pos = array_search('_tenancy_db_connection', $dbConfig)) !== false) { + unset($dbConfig[$pos]); + } + + return array_reduce($dbConfig, function ($config, $key) { + return array_merge($config, [ + Str::substr($key, strlen('_tenancy_db_')) => $this->tenant[$key], + ]); + }, []); + } + + /** + * Get the TenantDatabaseManager for this tenant's connection. + */ + public function manager(): TenantDatabaseManager + { + $driver = config("database.connections.{$this->getTemplateConnectionName()}.driver"); + + $databaseManagers = config('tenancy.database_managers'); + + if (! array_key_exists($driver, $databaseManagers)) { + throw new DatabaseManagerNotRegisteredException($driver); + } + + /** @var TenantDatabaseManager $databaseManager */ + $databaseManager = app($databaseManagers[$driver]); + + if ($databaseManager instanceof CanSetConnection) { + $databaseManager->setConnection($this->getTemplateConnectionName()); + } + + return $databaseManager; + } +} diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 0004ea69..c83a7eb8 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -8,18 +8,19 @@ use Closure; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Foundation\Application; -use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; -use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; +/** + * @internal Class is subject to breaking changes in minor and patch versions. + */ class DatabaseManager { /** @var string */ - public $originalDefaultConnectionName; + public static $originalDefaultConnectionName; /** @var Application */ protected $app; @@ -34,14 +35,11 @@ class DatabaseManager { $this->app = $app; $this->database = $database; - $this->originalDefaultConnectionName = $app['config']['database.default']; + static::$originalDefaultConnectionName = $app['config']['database.default']; } /** * Set the TenantManager instance, used to dispatch tenancy events. - * - * @param TenantManager $tenantManager - * @return self */ public function withTenantManager(TenantManager $tenantManager): self { @@ -52,35 +50,28 @@ class DatabaseManager /** * Connect to a tenant's database. - * - * @param Tenant $tenant - * @return void */ public function connect(Tenant $tenant) { - $this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName()); - $this->setDefaultConnection($tenant->getConnectionName()); - $this->switchConnection($tenant->getConnectionName()); + $this->createTenantConnection($tenant); + $this->setDefaultConnection('tenant'); + $this->switchConnection('tenant'); } /** * Reconnect to the default non-tenant connection. - * - * @return void */ public function reconnect() { - // Opposite order to connect() because we don't - // want to ever purge the central connection - $this->switchConnection($this->originalDefaultConnectionName); - $this->setDefaultConnection($this->originalDefaultConnectionName); + if ($this->tenancy->initialized) { + $this->database->purge('tenant'); + } + $this->setDefaultConnection(static::$originalDefaultConnectionName); + $this->switchConnection(static::$originalDefaultConnectionName); } /** * Change the default database connection config. - * - * @param string $connection - * @return void */ public function setDefaultConnection(string $connection) { @@ -89,57 +80,17 @@ class DatabaseManager /** * Create the tenant database connection. - * - * @param string $databaseName - * @param string $connectionName - * @return void */ - public function createTenantConnection($databaseName, $connectionName) + public function createTenantConnection(Tenant $tenant) { - // Create the database connection. - $based_on = $this->getBaseConnection($connectionName); - $this->app['config']["database.connections.$connectionName"] = $this->app['config']['database.connections.' . $based_on]; - - // Change database name. - $databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName; - $separateBy = $this->separateBy($connectionName); - - $this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName; - } - - /** - * Get the name of the connection that $connectionName should be based on. - * - * @param string $connectionName - * @return string - */ - public function getBaseConnection(string $connectionName): string - { - return ($connectionName !== 'tenant' ? $connectionName : null) // 'tenant' is not a specific connection, it's the default - ?? $this->app['config']['tenancy.database.based_on'] - ?? $this->originalDefaultConnectionName; // tenancy.database.based_on === null => use the default connection - } - - /** - * Get the driver of a database connection. - * - * @param string $connectionName - * @return string|null - */ - public function getDriver(string $connectionName): ?string - { - return $this->app['config']["database.connections.$connectionName.driver"]; + $this->app['config']['database.connections.tenant'] = $tenant->database()->connection(); } /** * Switch the application's connection. - * - * @param string $connection - * @return void */ public function switchConnection(string $connection) { - $this->database->purge(); $this->database->reconnect($connection); $this->database->setDefaultConnection($connection); } @@ -147,15 +98,13 @@ class DatabaseManager /** * Check if a tenant can be created. * - * @param Tenant $tenant - * @return void * @throws TenantCannotBeCreatedException * @throws DatabaseManagerNotRegisteredException * @throws TenantDatabaseAlreadyExistsException */ public function ensureTenantCanBeCreated(Tenant $tenant): void { - if ($this->getTenantDatabaseManager($tenant)->databaseExists($database = $tenant->getDatabaseName())) { + if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) { throw new TenantDatabaseAlreadyExistsException($database); } } @@ -170,101 +119,66 @@ class DatabaseManager */ public function createDatabase(Tenant $tenant, array $afterCreating = []) { - $database = $tenant->getDatabaseName(); - $manager = $this->getTenantDatabaseManager($tenant); - $afterCreating = array_merge( $afterCreating, - $this->tenancy->event('database.creating', $database, $tenant) + $this->tenancy->event('database.creating', $tenant->database()->getName(), $tenant) ); if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { - $chain = []; - foreach ($afterCreating as $item) { - if (is_string($item) && class_exists($item)) { - $chain[] = new $item($tenant); // Classes are instantiated and given $tenant - } elseif ($item instanceof ShouldQueue) { - $chain[] = $item; - } - } - - QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database); + $this->createDatabaseAsynchronously($tenant, $afterCreating); } else { - $manager->createDatabase($database); - foreach ($afterCreating as $item) { - if (is_object($item) && ! $item instanceof Closure) { - $item->handle($tenant); - } else { - $item($tenant); - } + $this->createDatabaseSynchronously($tenant, $afterCreating); + } + + $this->tenancy->event('database.created', $tenant->database()->getName(), $tenant); + } + + protected function createDatabaseAsynchronously(Tenant $tenant, array $afterCreating) + { + $chain = []; + foreach ($afterCreating as $item) { + if (is_string($item) && class_exists($item)) { + $chain[] = new $item($tenant); // Classes are instantiated and given $tenant + } elseif ($item instanceof ShouldQueue) { + $chain[] = $item; } } - $this->tenancy->event('database.created', $database, $tenant); + QueuedTenantDatabaseCreator::withChain($chain)->dispatch($tenant->database()->manager(), $tenant); + } + + protected function createDatabaseSynchronously(Tenant $tenant, array $afterCreating) + { + $manager = $tenant->database()->manager(); + $manager->createDatabase($tenant); + + foreach ($afterCreating as $item) { + if (is_object($item) && ! $item instanceof Closure) { + $item->handle($tenant); + } else { + $item($tenant); + } + } } /** * Delete a tenant's database. * - * @param Tenant $tenant - * @return void * @throws DatabaseManagerNotRegisteredException */ public function deleteDatabase(Tenant $tenant) { - $database = $tenant->getDatabaseName(); - $manager = $this->getTenantDatabaseManager($tenant); + $database = $tenant->database()->getName(); + $manager = $tenant->database()->manager(); $this->tenancy->event('database.deleting', $database, $tenant); if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) { - QueuedTenantDatabaseDeleter::dispatch($manager, $database); + QueuedTenantDatabaseDeleter::dispatch($manager, $tenant); } else { - $manager->deleteDatabase($database); + $manager->deleteDatabase($tenant); } $this->tenancy->event('database.deleted', $database, $tenant); } - - /** - * Get the TenantDatabaseManager for a tenant's database connection. - * - * @param Tenant $tenant - * @return TenantDatabaseManager - * @throws DatabaseManagerNotRegisteredException - */ - public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager - { - $driver = $this->getDriver($this->getBaseConnection($connectionName = $tenant->getConnectionName())); - - $databaseManagers = $this->app['config']['tenancy.database_managers']; - - if (! array_key_exists($driver, $databaseManagers)) { - throw new DatabaseManagerNotRegisteredException($driver); - } - - $databaseManager = $this->app[$databaseManagers[$driver]]; - - if ($connectionName !== 'tenant' && $databaseManager instanceof CanSetConnection) { - $databaseManager->setConnection($connectionName); - } - - return $databaseManager; - } - - /** - * What key on the connection config should be used to separate tenants. - * - * @param string $connectionName - * @return string - */ - public function separateBy(string $connectionName): string - { - if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql' - && $this->app['config']['tenancy.database.separate_by'] === 'schema') { - return 'schema'; - } - - return 'database'; - } } diff --git a/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php b/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php new file mode 100644 index 00000000..f84e39ec --- /dev/null +++ b/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php @@ -0,0 +1,25 @@ +user} already exists."; + } + + public function __construct(string $user) + { + parent::__construct(); + + $this->user = $user; + } +} diff --git a/src/Jobs/QueuedTenantDatabaseCreator.php b/src/Jobs/QueuedTenantDatabaseCreator.php index bd03fc55..066580b9 100644 --- a/src/Jobs/QueuedTenantDatabaseCreator.php +++ b/src/Jobs/QueuedTenantDatabaseCreator.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class QueuedTenantDatabaseCreator implements ShouldQueue { @@ -18,20 +19,13 @@ class QueuedTenantDatabaseCreator implements ShouldQueue /** @var TenantDatabaseManager */ protected $databaseManager; - /** @var string */ - protected $databaseName; + /** @var Tenant */ + protected $tenant; - /** - * Create a new job instance. - * - * @param TenantDatabaseManager $databaseManager - * @param string $databaseName - * @return void - */ - public function __construct(TenantDatabaseManager $databaseManager, string $databaseName) + public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant) { $this->databaseManager = $databaseManager; - $this->databaseName = $databaseName; + $this->tenant = $tenant; } /** @@ -41,6 +35,6 @@ class QueuedTenantDatabaseCreator implements ShouldQueue */ public function handle() { - $this->databaseManager->createDatabase($this->databaseName); + $this->databaseManager->createDatabase($this->tenant); } } diff --git a/src/Jobs/QueuedTenantDatabaseDeleter.php b/src/Jobs/QueuedTenantDatabaseDeleter.php index 7d395579..dbd79d21 100644 --- a/src/Jobs/QueuedTenantDatabaseDeleter.php +++ b/src/Jobs/QueuedTenantDatabaseDeleter.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class QueuedTenantDatabaseDeleter implements ShouldQueue { @@ -18,20 +19,13 @@ class QueuedTenantDatabaseDeleter implements ShouldQueue /** @var TenantDatabaseManager */ protected $databaseManager; - /** @var string */ - protected $databaseName; + /** @var Tenant */ + protected $tenant; - /** - * Create a new job instance. - * - * @param TenantDatabaseManager $databaseManager - * @param string $databaseName - * @return void - */ - public function __construct(TenantDatabaseManager $databaseManager, string $databaseName) + public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant) { $this->databaseManager = $databaseManager; - $this->databaseName = $databaseName; + $this->tenant = $tenant; } /** @@ -41,6 +35,6 @@ class QueuedTenantDatabaseDeleter implements ShouldQueue */ public function handle() { - $this->databaseManager->deleteDatabase($this->databaseName); + $this->databaseManager->deleteDatabase($this->tenant); } } diff --git a/src/StorageDrivers/Database/DatabaseStorageDriver.php b/src/StorageDrivers/Database/DatabaseStorageDriver.php index 1303ba38..5381dae0 100644 --- a/src/StorageDrivers/Database/DatabaseStorageDriver.php +++ b/src/StorageDrivers/Database/DatabaseStorageDriver.php @@ -21,22 +21,22 @@ use Stancl\Tenancy\Tenant; class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAnyKey { /** @var Application */ - protected $app; + public $app; /** @var Connection */ - protected $centralDatabase; + public $centralDatabase; /** @var TenantRepository */ - protected $tenants; + public $tenants; /** @var DomainRepository */ - protected $domains; + public $domains; /** @var CachedTenantResolver */ - protected $cache; + public $cache; /** @var Tenant The default tenant. */ - protected $tenant; + public $tenant; public function __construct(Application $app, ConfigRepository $config, CachedTenantResolver $cache) { @@ -59,7 +59,7 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn public static function getCentralConnectionName(): string { - return config('tenancy.storage_drivers.db.connection') ?? app(DatabaseManager::class)->originalDefaultConnectionName; + return config('tenancy.storage_drivers.db.connection') ?? DatabaseManager::$originalDefaultConnectionName; } public function findByDomain(string $domain): Tenant diff --git a/src/StorageDrivers/Database/DomainRepository.php b/src/StorageDrivers/Database/DomainRepository.php index 4e21b9ad..3e77396f 100644 --- a/src/StorageDrivers/Database/DomainRepository.php +++ b/src/StorageDrivers/Database/DomainRepository.php @@ -50,8 +50,6 @@ class DomainRepository extends Repository public function getTable(ConfigRepository $config) { - return $config->get('tenancy.storage_drivers.db.table_names.DomainModel') // legacy - ?? $config->get('tenancy.storage_drivers.db.table_names.domains') - ?? 'domains'; + return $config->get('tenancy.storage_drivers.db.table_names.domains') ?? 'domains'; } } diff --git a/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php b/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php index 8920f44a..bc9cc9b4 100644 --- a/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php @@ -21,8 +21,8 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper public function start(Tenant $tenant) { - $database = $tenant->getDatabaseName(); - if (! $this->database->getTenantDatabaseManager($tenant)->databaseExists($database)) { + $database = $tenant->database()->getName(); + if (! $tenant->database()->manager()->databaseExists($database)) { throw new TenantDatabaseDoesNotExistException($database); } diff --git a/src/Tenant.php b/src/Tenant.php index 38ac7261..4a9824f8 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -19,6 +19,7 @@ use Stancl\Tenancy\Exceptions\TenantStorageException; /** * @internal Class is subject to breaking changes in minor and patch versions. */ +// todo make this class serializable class Tenant implements ArrayAccess { use Traits\HasArrayAccess, @@ -91,8 +92,7 @@ class Tenant implements ArrayAccess } /** - * DO NOT CALL THIS METHOD FROM USERLAND. Used by storage - * drivers to create persisted instances of Tenant. + * Used by storage drivers to create persisted instances of Tenant. * * @param array $data * @return self @@ -230,6 +230,7 @@ class Tenant implements ArrayAccess if ($this->persisted) { $this->manager->updateTenant($this); } else { + $this->database()->makeCredentials(); $this->manager->createTenant($this); } @@ -272,23 +273,13 @@ class Tenant implements ArrayAccess } /** - * Get the tenant's database's name. + * Get database config. * - * @return string + * @return DatabaseConfig */ - public function getDatabaseName(): string + public function database(): DatabaseConfig { - return $this->data['_tenancy_db_name'] ?? ($this->config->get('tenancy.database.prefix') . $this->id . $this->config->get('tenancy.database.suffix')); - } - - /** - * Get the tenant's database connection's name. - * - * @return string - */ - public function getConnectionName(): string - { - return $this->data['_tenancy_db_connection'] ?? 'tenant'; + return new DatabaseConfig($this); } /** diff --git a/src/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/TenantDatabaseManagers/MySQLDatabaseManager.php index f6c4ef96..2685b527 100644 --- a/src/TenantDatabaseManagers/MySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/MySQLDatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -20,6 +21,11 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection $this->connection = $config->get('tenancy.database_manager_connections.mysql'); } + public function getSeparator(): string + { + return 'database'; + } + protected function database(): Connection { return DB::connection($this->connection); @@ -30,17 +36,18 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(Tenant $tenant): bool { + $database = $tenant->database()->getName(); $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 `{$database}` CHARACTER SET `$charset` COLLATE `$collation`"); } - public function deleteDatabase(string $name): bool + public function deleteDatabase(Tenant $tenant): bool { - return $this->database()->statement("DROP DATABASE `$name`"); + return $this->database()->statement("DROP DATABASE `{$tenant->database()->getName()}`"); } public function databaseExists(string $name): bool diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php new file mode 100644 index 00000000..3ce51568 --- /dev/null +++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -0,0 +1,62 @@ +getName(); + $username = $databaseConfig->getUsername(); + $hostname = $databaseConfig->connection()['host']; + $password = $databaseConfig->getPassword(); + + if ($this->userExists($username)) { + throw new TenantDatabaseUserAlreadyExistsException($username); + } + + $this->database()->statement("CREATE USER `{$username}`@`{$hostname}` IDENTIFIED BY '{$password}'"); + + $grants = implode(', ', static::$grants); + + if ($this->isVersion8()) { // MySQL 8+ + $grantQuery = "GRANT $grants ON `$database`.* TO `$username`@`$hostname`"; + } else { // MySQL 5.7 + $grantQuery = "GRANT $grants ON `$database`.* TO `$username`@`$hostname` IDENTIFIED BY '$password'"; + } + + return $this->database()->statement($grantQuery); + } + + protected function isVersion8(): bool + { + $version = $this->database()->select($this->database()->raw('select version()'))[0]->{'version()'}; + + return version_compare($version, '8.0.0') >= 0; + } + + public function deleteUser(DatabaseConfig $databaseConfig): bool + { + return $this->database()->statement("DROP USER IF EXISTS '{$databaseConfig->getUsername()}'"); + } + + public function userExists(string $username): bool + { + return (bool) $this->database()->select("SELECT count(*) FROM mysql.user WHERE user = '$username'")[0]->{'count(*)'}; + } +} diff --git a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php index fc21668e..f189e232 100644 --- a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -20,6 +21,11 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti $this->connection = $config->get('tenancy.database_manager_connections.pgsql'); } + public function getSeparator(): string + { + return 'database'; + } + protected function database(): Connection { return DB::connection($this->connection); @@ -30,14 +36,14 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(Tenant $tenant): bool { - return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); + return $this->database()->statement("CREATE DATABASE \"{$tenant->database()->getName()}\" WITH TEMPLATE=template0"); } - public function deleteDatabase(string $name): bool + public function deleteDatabase(Tenant $tenant): bool { - return $this->database()->statement("DROP DATABASE \"$name\""); + return $this->database()->statement("DROP DATABASE \"{$tenant->database()->getName()}\""); } public function databaseExists(string $name): bool diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php index a93ed901..819daf09 100644 --- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection { @@ -20,6 +21,11 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection $this->connection = $config->get('tenancy.database_manager_connections.pgsql'); } + public function getSeparator(): string + { + return 'schema'; + } + protected function database(): Connection { return DB::connection($this->connection); @@ -30,14 +36,14 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(Tenant $tenant): bool { - return $this->database()->statement("CREATE SCHEMA \"$name\""); + return $this->database()->statement("CREATE SCHEMA \"{$tenant->database()->getName()}\""); } - public function deleteDatabase(string $name): bool + public function deleteDatabase(Tenant $tenant): bool { - return $this->database()->statement("DROP SCHEMA \"$name\""); + return $this->database()->statement("DROP SCHEMA \"{$tenant->database()->getName()}\""); } public function databaseExists(string $name): bool diff --git a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php index 5d681ded..19407ce9 100644 --- a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -4,23 +4,30 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenantDatabaseManagers; +use Stancl\Tenancy\Contracts\ModifiesDatabaseNameForConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; -class SQLiteDatabaseManager implements TenantDatabaseManager +class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNameForConnection { - public function createDatabase(string $name): bool + public function getSeparator(): string + { + return 'database'; + } + + public function createDatabase(Tenant $tenant): bool { try { - return fclose(fopen(database_path($name), 'w')); + return fclose(fopen(database_path($tenant->database()->getName()), 'w')); } catch (\Throwable $th) { return false; } } - public function deleteDatabase(string $name): bool + public function deleteDatabase(Tenant $tenant): bool { try { - return unlink(database_path($name)); + return unlink(database_path($tenant->database()->getName())); } catch (\Throwable $th) { return false; } @@ -30,4 +37,9 @@ class SQLiteDatabaseManager implements TenantDatabaseManager { return file_exists(database_path($name)); } + + public function getDatabaseNameForConnection(string $original): string + { + return database_path($original); + } } diff --git a/src/Traits/CreatesDatabaseUsers.php b/src/Traits/CreatesDatabaseUsers.php new file mode 100644 index 00000000..40d97e30 --- /dev/null +++ b/src/Traits/CreatesDatabaseUsers.php @@ -0,0 +1,28 @@ +database()->transaction(function () use ($tenant) { + parent::createDatabase($tenant); + + return $this->createUser($tenant->database()); + }); + } + + public function deleteDatabase(Tenant $tenant): bool + { + return $this->database()->transaction(function () use ($tenant) { + parent::deleteDatabase($tenant); + + return $this->deleteUser($tenant->database()); + }); + } +} diff --git a/src/Traits/DealsWithMigrations.php b/src/Traits/DealsWithMigrations.php index f730cf07..edbae934 100644 --- a/src/Traits/DealsWithMigrations.php +++ b/src/Traits/DealsWithMigrations.php @@ -12,6 +12,6 @@ trait DealsWithMigrations return parent::getMigrationPaths(); } - return config('tenancy.migration_paths', [config('tenancy.migrations_directory') ?? database_path('migrations/tenant')]); + return database_path('migrations/tenant'); } } diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php index 77e0c357..13e88916 100644 --- a/tests/CacheManagerTest.php +++ b/tests/CacheManagerTest.php @@ -8,11 +8,10 @@ use Stancl\Tenancy\Tenant; class CacheManagerTest extends TestCase { - public $autoInitTenancy = false; - /** @test */ public function default_tag_is_automatically_applied() { + $this->createTenant(); $this->initTenancy(); $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); } @@ -20,6 +19,7 @@ class CacheManagerTest extends TestCase /** @test */ public function tags_are_merged_when_array_is_passed() { + $this->createTenant(); $this->initTenancy(); $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); @@ -28,6 +28,7 @@ class CacheManagerTest extends TestCase /** @test */ public function tags_are_merged_when_string_is_passed() { + $this->createTenant(); $this->initTenancy(); $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); @@ -36,6 +37,7 @@ class CacheManagerTest extends TestCase /** @test */ public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method() { + $this->createTenant(); $this->initTenancy(); $this->expectException(\Exception::class); cache()->tags(); @@ -44,6 +46,7 @@ class CacheManagerTest extends TestCase /** @test */ public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method() { + $this->createTenant(); $this->initTenancy(); $this->expectException(\Exception::class); cache()->tags(1, 2); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 39bb46e2..e1522b1e 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -12,15 +12,9 @@ use Stancl\Tenancy\Tests\Etc\ExampleSeeder; class CommandsTest extends TestCase { + public $autoCreateTenant = true; public $autoInitTenancy = false; - public function setUp(): void - { - parent::setUp(); - - config(['tenancy.migration_paths', [database_path('../migrations')]]); - } - /** @test */ public function migrate_command_doesnt_change_the_db_connection() { @@ -173,6 +167,7 @@ class CommandsTest extends TestCase $tenant = tenancy()->all()[1]; // a tenant is autocreated prior to this $data = $tenant->data; unset($data['id']); + unset($data['_tenancy_db_name']); $this->assertSame(['plan' => 'free', 'email' => 'foo@test.local'], $data); $this->assertSame(['aaa.localhost', 'bbb.localhost'], $tenant->domains); diff --git a/tests/DataSeparationTest.php b/tests/DataSeparationTest.php index a6240d12..8fe21c66 100644 --- a/tests/DataSeparationTest.php +++ b/tests/DataSeparationTest.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Tests\Etc\User; class DataSeparationTest extends TestCase { @@ -151,8 +152,3 @@ class DataSeparationTest extends TestCase $this->assertFalse(Storage::disk('public')->exists('abc')); } } - -class User extends \Illuminate\Database\Eloquent\Model -{ - protected $guarded = []; -} diff --git a/tests/DatabaseManagerTest.php b/tests/DatabaseManagerTest.php index 9195ef69..1bb29663 100644 --- a/tests/DatabaseManagerTest.php +++ b/tests/DatabaseManagerTest.php @@ -5,16 +5,16 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Stancl\Tenancy\DatabaseManager; +use Stancl\Tenancy\Tenant; class DatabaseManagerTest extends TestCase { - public $autoInitTenancy = false; - /** @test */ public function reconnect_method_works() { $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - tenancy()->init('test.localhost'); + $this->createTenant(); + $this->initTenancy(); app(\Stancl\Tenancy\DatabaseManager::class)->reconnect(); $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); @@ -25,22 +25,30 @@ class DatabaseManagerTest extends TestCase /** @test */ public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() { + if (file_exists(database_path('foodb'))) { + unlink(database_path('foodb')); // cleanup + } config(['database.connections.fooconn.driver' => 'sqlite']); - app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn'); + $tenant = Tenant::new()->withData([ + '_tenancy_db_name' => 'foodb', + '_tenancy_db_connection' => 'fooconn', + ])->save(); + app(DatabaseManager::class)->createTenantConnection($tenant); - $this->assertSame(config('database.connections.fooconn.database'), database_path('foodb')); + $this->assertSame(config('database.connections.tenant.database'), database_path('foodb')); } /** @test */ - public function the_default_db_is_used_when_based_on_is_null() + public function the_default_db_is_used_when_template_connection_is_null() { - $this->assertSame('sqlite', config('database.default')); + $this->assertSame('central', config('database.default')); config([ - 'database.connections.sqlite.foo' => 'bar', - 'tenancy.database.based_on' => null, + 'database.connections.central.foo' => 'bar', + 'tenancy.database.template_connection' => null, ]); - tenancy()->init('test.localhost'); + $this->createTenant(); + $this->initTenancy(); $this->assertSame('tenant', config('database.default')); $this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo')); diff --git a/tests/DatabaseSchemaManagerTest.php b/tests/DatabaseSchemaManagerTest.php index 5f9589e0..7643b5b2 100644 --- a/tests/DatabaseSchemaManagerTest.php +++ b/tests/DatabaseSchemaManagerTest.php @@ -6,9 +6,11 @@ namespace Stancl\Tenancy\Tests; use Illuminate\Support\Str; use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Tests\Etc\User; class DatabaseSchemaManagerTest extends TestCase { + public $autoCreateTenant = true; public $autoInitTenancy = false; protected function getEnvironmentSetUp($app) @@ -17,11 +19,11 @@ class DatabaseSchemaManagerTest extends TestCase $app['config']->set([ 'database.default' => 'pgsql', + 'tenancy.storage_drivers.db.connection' => 'pgsql', 'database.connections.pgsql.database' => 'main', 'database.connections.pgsql.schema' => 'public', - 'tenancy.database.based_on' => null, + 'tenancy.database.template_connection' => null, 'tenancy.database.suffix' => '', - 'tenancy.database.separate_by' => 'schema', 'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, ]); } @@ -41,20 +43,18 @@ class DatabaseSchemaManagerTest extends TestCase } /** @test */ - public function the_default_db_is_used_when_based_on_is_null() + public function the_default_db_is_used_when_template_connection_is_null() { - config(['database.default' => 'pgsql']); - $this->assertSame('pgsql', config('database.default')); config([ 'database.connections.pgsql.foo' => 'bar', - 'tenancy.database.based_on' => null, + 'tenancy.database.template_connection' => null, ]); tenancy()->init('test.localhost'); $this->assertSame('tenant', config('database.default')); - $this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo')); + $this->assertSame('bar', config('database.connections.tenant.foo')); } /** @test */ @@ -63,7 +63,7 @@ class DatabaseSchemaManagerTest extends TestCase $tenant = tenancy()->create(['schema.localhost']); tenancy()->init('schema.localhost'); - $this->assertSame($tenant->getDatabaseName(), config('database.connections.' . config('database.default') . '.schema')); + $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema')); } /** @test */ @@ -96,6 +96,7 @@ class DatabaseSchemaManagerTest extends TestCase $tenant1 = Tenant::create('tenant1.localhost'); $tenant2 = Tenant::create('tenant2.localhost'); + \Artisan::call('tenants:migrate', [ '--tenants' => [$tenant1['id'], $tenant2['id']], ]); diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php new file mode 100644 index 00000000..999d4839 --- /dev/null +++ b/tests/DatabaseUsersTest.php @@ -0,0 +1,113 @@ + PermissionControlledMySQLDatabaseManager::class, + 'tenancy.database.suffix' => '', + 'tenancy.database.template_connection' => 'mysql', + ]); + } + + /** @test */ + public function users_are_created_when_permission_controlled_mysql_manager_is_used() + { + $tenant = Tenant::new()->withData([ + 'id' => 'foo' . Str::random(10), + ]); + $tenant->database()->makeCredentials(); + + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant->database()->manager(); + $this->assertFalse($manager->userExists($tenant->database()->getUsername())); + + $tenant->save(); + + $this->assertTrue($manager->userExists($tenant->database()->getUsername())); + } + + /** @test */ + public function a_tenants_database_cannot_be_created_when_the_user_already_exists() + { + $username = 'foo' . Str::random(8); + $tenant = Tenant::new()->withData([ + '_tenancy_db_username' => $username, + ])->save(); + + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant->database()->manager(); + $this->assertTrue($manager->userExists($tenant->database()->getUsername())); + $this->assertTrue($manager->databaseExists($tenant->database()->getName())); + + $this->expectException(TenantDatabaseUserAlreadyExistsException::class); + $tenant2 = Tenant::new()->withData([ + '_tenancy_db_username' => $username, + ])->save(); + + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant2->database()->manager(); + // database was not created because of DB transaction + $this->assertFalse($manager->databaseExists($tenant2->database()->getName())); + } + + /** @test */ + public function correct_grants_are_given_to_users() + { + PermissionControlledMySQLDatabaseManager::$grants = [ + 'ALTER', 'ALTER ROUTINE', 'CREATE', + ]; + + $tenant = Tenant::new()->withData([ + '_tenancy_db_username' => $user = 'user' . Str::random(8), + ])->save(); + + $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`{$tenant->database()->connection()['host']}`")[1]; + $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@mysql"}); // @mysql because that's the hostname within the docker network + } + + /** @test */ + public function having_existing_databases_without_users_and_switching_to_permission_controlled_mysql_manager_doesnt_break_existing_dbs() + { + config([ + 'tenancy.database_managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.suffix' => '', + 'tenancy.database.template_connection' => 'mysql', + ]); + + $tenant = Tenant::new()->withData([ + 'id' => 'foo' . Str::random(10), + ])->save(); + + $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager); + + $tenant = Tenant::new()->withData([ + 'id' => 'foo' . Str::random(10), + ])->save(); + + tenancy()->initialize($tenant); // check if everything works + tenancy()->end(); + + config(['tenancy.database_managers.mysql' => PermissionControlledMySQLDatabaseManager::class]); + + tenancy()->initialize($tenant); // check if everything works + + $this->assertTrue($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager); + $this->assertSame('root', config('database.connections.tenant.username')); + } +} diff --git a/tests/Etc/User.php b/tests/Etc/User.php new file mode 100644 index 00000000..1d228cc9 --- /dev/null +++ b/tests/Etc/User.php @@ -0,0 +1,10 @@ + $id]); $tenant->foo = 'bar'; $tenant->save(); - $this->assertEquals(['id' => $id, 'foo' => 'bar'], $tenant->data); - $this->assertEquals(['id' => $id, 'foo' => 'bar'], tenancy()->find($id)->data); + $this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], $tenant->data); + $this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], tenancy()->find($id)->data); $tenant->addDomains('abc.localhost'); $tenant->save(); @@ -102,6 +102,7 @@ class TenantClassTest extends TestCase $data = tenancy()->all()->first()->data; unset($data['id']); + unset($data['_tenancy_db_name']); $this->assertSame(['foo' => 'bar'], $data); } @@ -173,4 +174,54 @@ class TenantClassTest extends TestCase return $tenant->id; })); } + + /** @test */ + public function extra_config_is_merged_into_the_connection_config_array() + { + $tenant = Tenant::new()->withData([ + '_tenancy_db_link' => 'foo', + '_tenancy_db_name' => 'dbname', + '_tenancy_db_username' => 'usernamefoo', + '_tenancy_db_password' => 'passwordfoo', + '_tenancy_db_connection' => 'mysql', + ]); + + config(['database.connections.mysql' => [ + 'driver' => 'mysql', + 'url' => null, + 'host' => 'mysql', + 'port' => '3306', + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => [], + ]]); + + $this->assertEquals([ + 'database' => 'dbname', + 'username' => 'usernamefoo', + 'password' => 'passwordfoo', + 'link' => 'foo', + + 'driver' => 'mysql', + 'url' => null, + 'host' => 'mysql', + 'port' => '3306', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => [], + ], $tenant->database()->connection()); + } } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 89c0bbd7..7c4ff514 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -9,6 +9,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; +use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; @@ -27,25 +28,46 @@ class TenantDatabaseManagerTest extends TestCase $this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.'); } + config()->set([ + "tenancy.database_managers.$driver" => $databaseManager, + ]); + $name = 'db' . $this->randomString(); + $tenant = Tenant::new()->withData([ + '_tenancy_db_name' => $name, + '_tenancy_db_connection' => $driver, + ]); + $this->assertFalse(app($databaseManager)->databaseExists($name)); - app($databaseManager)->createDatabase($name); + $tenant->save(); // generate credentials & create DB $this->assertTrue(app($databaseManager)->databaseExists($name)); - app($databaseManager)->deleteDatabase($name); + app($databaseManager)->deleteDatabase($tenant); $this->assertFalse(app($databaseManager)->databaseExists($name)); } /** @test */ public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db() { - $this->assertSame('sqlite', config('database.default')); + $this->assertSame('central', config('database.default')); $database = 'db' . $this->randomString(); - app(MySQLDatabaseManager::class)->createDatabase($database); + $tenant = Tenant::new()->withData([ + '_tenancy_db_name' => $database, + '_tenancy_db_connection' => 'mysql', + ]); + + $this->assertFalse(app(MySQLDatabaseManager::class)->databaseExists($database)); + $tenant->save(); // create DB $this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($database)); - $database = 'db2' . $this->randomString(); - app(PostgreSQLDatabaseManager::class)->createDatabase($database); + $database = 'db' . $this->randomString(); + $tenant = Tenant::new()->withData([ + '_tenancy_db_name' => $database, + '_tenancy_db_connection' => 'pgsql', + ]); + + $this->assertFalse(app(PostgreSQLDatabaseManager::class)->databaseExists($database)); + $tenant->save(); // create DB $this->assertTrue(app(PostgreSQLDatabaseManager::class)->databaseExists($database)); } @@ -59,16 +81,25 @@ class TenantDatabaseManagerTest extends TestCase $this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.'); } - config()->set('database.default', $driver); + config()->set([ + 'database.default' => $driver, + "tenancy.database_managers.$driver" => $databaseManager, + ]); $name = 'db' . $this->randomString(); + $tenant = Tenant::new()->withData([ + '_tenancy_db_name' => $name, + '_tenancy_db_connection' => $driver, + ]); + $tenant->database()->makeCredentials(); + $this->assertFalse(app($databaseManager)->databaseExists($name)); - $job = new QueuedTenantDatabaseCreator(app($databaseManager), $name); + $job = new QueuedTenantDatabaseCreator(app($databaseManager), $tenant); $job->handle(); $this->assertTrue(app($databaseManager)->databaseExists($name)); - $job = new QueuedTenantDatabaseDeleter(app($databaseManager), $name); + $job = new QueuedTenantDatabaseDeleter(app($databaseManager), $tenant); $job->handle(); $this->assertFalse(app($databaseManager)->databaseExists($name)); } @@ -77,6 +108,7 @@ class TenantDatabaseManagerTest extends TestCase { return [ ['mysql', MySQLDatabaseManager::class], + ['mysql', PermissionControlledMySQLDatabaseManager::class], ['sqlite', SQLiteDatabaseManager::class], ['pgsql', PostgreSQLDatabaseManager::class], ['pgsql', PostgreSQLSchemaManager::class], diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php index 8e78a607..3198123b 100644 --- a/tests/TenantManagerTest.php +++ b/tests/TenantManagerTest.php @@ -167,6 +167,7 @@ class TenantManagerTest extends TestCase $tenant_data = $tenant->data; unset($tenant_data['id']); + unset($tenant_data['_tenancy_db_name']); $this->assertSame($data, $tenant_data); } @@ -180,7 +181,7 @@ class TenantManagerTest extends TestCase '_tenancy_db_name' => $database, ]); - $this->assertSame($database, $tenant->getDatabaseName()); + $this->assertSame($database, $tenant->database()->getName()); } /** @test */ diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 568746cb..65354129 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -9,6 +9,9 @@ use Stancl\Tenancy\Tenant; class TenantStorageTest extends TestCase { + public $autoCreateTenant = true; + public $autoInitTenancy = true; + /** @test */ public function deleting_a_tenant_works() { diff --git a/tests/TestCase.php b/tests/TestCase.php index d94164f7..918024e7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,8 +9,8 @@ use Stancl\Tenancy\Tenant; abstract class TestCase extends \Orchestra\Testbench\TestCase { - public $autoCreateTenant = true; - public $autoInitTenancy = true; + public $autoCreateTenant = false; + public $autoInitTenancy = false; /** * Setup the test environment. @@ -24,12 +24,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('tenancy')->flushdb(); Redis::connection('cache')->flushdb(); - $originalConnection = config('database.default'); - $this->loadMigrationsFrom([ - '--path' => realpath(__DIR__ . '/../assets/migrations'), - '--database' => 'central', + file_put_contents(database_path('central.sqlite'), ''); + $this->artisan('migrate:fresh', [ + '--force' => true, + '--path' => __DIR__ . '/../assets/migrations', + '--realpath' => true, ]); - config(['database.default' => $originalConnection]); // fix issue caused by loadMigrationsFrom if ($this->autoCreateTenant) { $this->createTenant(); @@ -62,9 +62,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); } - fclose(fopen(database_path('central.sqlite'), 'w')); - $app['config']->set([ + 'database.default' => 'central', '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', @@ -80,9 +79,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'database.connections.central' => [ 'driver' => 'sqlite', 'database' => database_path('central.sqlite'), + // 'database' => ':memory:', ], 'tenancy.database' => [ - 'based_on' => 'sqlite', + 'template_connection' => 'central', 'prefix' => 'tenant', 'suffix' => '.sqlite', ], @@ -97,7 +97,11 @@ 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.migration_paths' => [database_path('../migrations')], + 'tenancy.migration_parameters' => [ + '--path' => [database_path('../migrations')], + '--realpath' => true, + '--force' => true, + ], 'tenancy.storage_drivers.db.connection' => 'central', 'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class, 'queue.connections.central' => [