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' => [