1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 06:04:03 +00:00

POC with no polish

Signed-off-by: michael lundbøl <michael.lundboel@gmail.com>
This commit is contained in:
michael lundbøl 2020-03-31 21:18:19 +02:00
parent 5dc80473d3
commit 43ee1c6596
No known key found for this signature in database
GPG key ID: 213C976E2CFD1CAF
12 changed files with 226 additions and 34 deletions

View file

@ -31,8 +31,7 @@ return [
'database' => [ 'database' => [
'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection. Set to null to use the default connection. 'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection. Set to null to use the default connection.
'prefix' => 'tenant', 'prefix' => 'tenant',
'suffix' => '', 'suffix' => ''
'separate_by' => 'database', // database or schema (only supported by pgsql)
], ],
'redis' => [ 'redis' => [
'prefix_base' => 'tenant', 'prefix_base' => 'tenant',

View file

@ -0,0 +1,11 @@
<?php
namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Tenant;
interface ManagesDatabaseUsers extends CanSetConnection
{
public function createDatabaseUser(string $databaseName, Tenant $tenant): void;
}

View file

@ -4,15 +4,18 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts; namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Tenant;
interface TenantDatabaseManager interface TenantDatabaseManager
{ {
/** /**
* Create a database. * Create a database.
* *
* @param string $name Name of the database. * @param string $name Name of the database.
* @param Tenant $tenant
* @return bool * @return bool
*/ */
public function createDatabase(string $name): bool; public function createDatabase(string $name, Tenant $tenant): bool;
/** /**
* Delete a database. * Delete a database.
@ -29,4 +32,14 @@ interface TenantDatabaseManager
* @return bool * @return bool
*/ */
public function databaseExists(string $name): bool; public function databaseExists(string $name): bool;
/**
* Override the base connection
*
* @param Tenant $tenant
* @param array $baseConfiguration
* @return array
*/
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array;
} }

View file

@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
@ -58,7 +59,7 @@ class DatabaseManager
*/ */
public function connect(Tenant $tenant) public function connect(Tenant $tenant)
{ {
$this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName()); $this->createTenantConnection($tenant);
$this->setDefaultConnection($tenant->getConnectionName()); $this->setDefaultConnection($tenant->getConnectionName());
$this->switchConnection($tenant->getConnectionName()); $this->switchConnection($tenant->getConnectionName());
} }
@ -90,21 +91,21 @@ class DatabaseManager
/** /**
* Create the tenant database connection. * Create the tenant database connection.
* *
* @param string $databaseName * @param Tenant $tenant
* @param string $connectionName
* @return void * @return void
* @throws DatabaseManagerNotRegisteredException
*/ */
public function createTenantConnection($databaseName, $connectionName) public function createTenantConnection(Tenant $tenant)
{ {
// Create the database connection. $configuration = $this->getTenantDatabaseManager($tenant)
$based_on = $this->getBaseConnection($connectionName); ->createDatabaseConnection(
$this->app['config']["database.connections.$connectionName"] = $this->app['config']['database.connections.' . $based_on]; $tenant,
$this->getBaseConnectionConfiguration($tenant)
);
// Change database name. $connectionName = $tenant->getConnectionName();
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
$separateBy = $this->separateBy($connectionName);
$this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName; $this->app['config']->set("database.connections.{$connectionName}", $configuration);
} }
/** /**
@ -188,9 +189,9 @@ class DatabaseManager
} }
} }
QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database); QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database, $tenant);
} else { } else {
$manager->createDatabase($database); $manager->createDatabase($database, $tenant);
foreach ($afterCreating as $item) { foreach ($afterCreating as $item) {
if (is_object($item) && ! $item instanceof Closure) { if (is_object($item) && ! $item instanceof Closure) {
$item->handle($tenant); $item->handle($tenant);
@ -253,18 +254,15 @@ class DatabaseManager
} }
/** /**
* What key on the connection config should be used to separate tenants. * Get the connection base configuration for a tenant
* *
* @param string $connectionName * @param Tenant $tenant
* @return string * @return array
*/ */
public function separateBy(string $connectionName): string protected function getBaseConnectionConfiguration(Tenant $tenant): array
{ {
if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql' $basedOn = $this->getBaseConnection($tenant->getConnectionName());
&& $this->app['config']['tenancy.database.separate_by'] === 'schema') {
return 'schema';
}
return 'database'; return $this->app['config']->get("database.connections.{$basedOn}");
} }
} }

View file

@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class QueuedTenantDatabaseCreator implements ShouldQueue class QueuedTenantDatabaseCreator implements ShouldQueue
{ {
@ -21,17 +22,21 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
/** @var string */ /** @var string */
protected $databaseName; protected $databaseName;
/** @var Tenant */
public $tenant;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @param TenantDatabaseManager $databaseManager * @param TenantDatabaseManager $databaseManager
* @param string $databaseName * @param string $databaseName
* @return void * @param Tenant $tenant
*/ */
public function __construct(TenantDatabaseManager $databaseManager, string $databaseName) public function __construct(TenantDatabaseManager $databaseManager, string $databaseName, Tenant $tenant)
{ {
$this->databaseManager = $databaseManager; $this->databaseManager = $databaseManager;
$this->databaseName = $databaseName; $this->databaseName = $databaseName;
$this->tenant = $tenant;
} }
/** /**
@ -41,6 +46,6 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
$this->databaseManager->createDatabase($this->databaseName); $this->databaseManager->createDatabase($this->databaseName, $this->tenant);
} }
} }

View file

@ -15,6 +15,7 @@ use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
use Stancl\Tenancy\Exceptions\NotImplementedException; use Stancl\Tenancy\Exceptions\NotImplementedException;
use Stancl\Tenancy\Exceptions\TenantStorageException; use Stancl\Tenancy\Exceptions\TenantStorageException;
use Stancl\Tenancy\Traits\ProvidesDatabaseUser;
/** /**
* @internal Class is subject to breaking changes in minor and patch versions. * @internal Class is subject to breaking changes in minor and patch versions.
@ -22,7 +23,8 @@ use Stancl\Tenancy\Exceptions\TenantStorageException;
class Tenant implements ArrayAccess class Tenant implements ArrayAccess
{ {
use Traits\HasArrayAccess, use Traits\HasArrayAccess,
ForwardsCalls; ForwardsCalls,
ProvidesDatabaseUser;
/** /**
* Tenant data. A "cache" of tenant storage. * Tenant data. A "cache" of tenant storage.

View file

@ -9,6 +9,7 @@ use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{ {
@ -30,7 +31,7 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $connection; $this->connection = $connection;
} }
public function createDatabase(string $name): bool public function createDatabase(string $name, Tenant $tenant): bool
{ {
$charset = $this->database()->getConfig('charset'); $charset = $this->database()->getConfig('charset');
$collation = $this->database()->getConfig('collation'); $collation = $this->database()->getConfig('collation');
@ -47,4 +48,14 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{ {
return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
} }
/**
* @inheritDoc
*/
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array
{
return array_replace_recursive($baseConfiguration, [
'database' => $tenant->getDatabaseName(),
]);
}
} }

View file

@ -0,0 +1,64 @@
<?php
namespace Stancl\Tenancy\TenantDatabaseManagers;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\Str;
use Stancl\Tenancy\Contracts\DatabaseUserGenerator;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\Tenant;
class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager implements ManagesDatabaseUsers
{
public function createDatabase(string $name, Tenant $tenant): bool
{
parent::createDatabase($name, $tenant);
$this->createDatabaseUser($name, $tenant);
return true;
}
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array
{
return array_replace_recursive(
parent::createDatabaseConnection($tenant, $baseConfiguration),
array_filter([
'host' => $tenant->getDatabaseHost(),
'username' => $tenant->getDatabaseUsername(),
'password' => $tenant->getDatabasePassword(),
'port' => $tenant->getDatabasePort(),
'url' => $tenant->getDatabaseUrl()
])
);
}
public function createDatabaseUser(string $databaseName, Tenant $tenant): void
{
$username = $tenant->generateDatabaseUsername();
$password = $tenant->generateDatabasePassword();
$appHost = $tenant->getDatabaseHost() ?? $this->getBaseConfigurationFor('host');
$grants = implode(', ', $tenant->getDatabaseGrants());
$this->database()->statement(
"CREATE USER '$username'@'$appHost' IDENTIFIED BY '$password"
);
$this->database()->statement(
"GRANT $grants ON $databaseName.* TO '$username'@'$appHost' IDENTIFIED BY '$password'"
);
$tenant->withData([
'_tenancy_db_username' => $username,
'_tenancy_db_password' => $password,
'_tenancy_db_host' => $appHost,
'_tenancy_db_link' => $tenant->getDatabaseLink()
])->save();
}
private function getBaseConfigurationFor(string $key)
{
return $this->database()->getConfig($key);
}
}

View file

@ -9,6 +9,7 @@ use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{ {
@ -30,7 +31,7 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti
$this->connection = $connection; $this->connection = $connection;
} }
public function createDatabase(string $name): bool public function createDatabase(string $name, Tenant $tenant): bool
{ {
return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0");
} }
@ -44,4 +45,18 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti
{ {
return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'"); return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'");
} }
/**
* @inheritDoc
*/
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array
{
if ('pgsql' !== $baseConfiguration['driver']) {
throw new \Exception('Mismatching driver for tenant');
}
return array_replace_recursive($baseConfiguration, [
'database' => $tenant->getDatabaseName()
]);
}
} }

View file

@ -9,6 +9,7 @@ use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
{ {
@ -30,7 +31,7 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $connection; $this->connection = $connection;
} }
public function createDatabase(string $name): bool public function createDatabase(string $name, Tenant $tenant): bool
{ {
return $this->database()->statement("CREATE SCHEMA \"$name\""); return $this->database()->statement("CREATE SCHEMA \"$name\"");
} }
@ -44,4 +45,15 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
{ {
return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'"); return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'");
} }
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array
{
if ('pgsql' !== $baseConfiguration['driver']) {
throw new \Exception('Mismatching driver for tenant');
}
return array_replace_recursive($baseConfiguration, [
'schema' => $tenant->getDatabaseName()
]);
}
} }

View file

@ -5,10 +5,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenantDatabaseManagers; namespace Stancl\Tenancy\TenantDatabaseManagers;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class SQLiteDatabaseManager implements TenantDatabaseManager class SQLiteDatabaseManager implements TenantDatabaseManager
{ {
public function createDatabase(string $name): bool public function createDatabase(string $name, Tenant $tenant): bool
{ {
try { try {
return fclose(fopen(database_path($name), 'w')); return fclose(fopen(database_path($name), 'w'));
@ -30,4 +31,11 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
{ {
return file_exists(database_path($name)); return file_exists(database_path($name));
} }
public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array
{
return array_replace_recursive($baseConfiguration, [
'database' => database_path($tenant->getDatabaseName())
]);
}
} }

View file

@ -0,0 +1,54 @@
<?php
namespace Stancl\Tenancy\Traits;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
trait ProvidesDatabaseUser
{
public function getDatabaseHost(): ?string
{
return $this->data['_tenancy_db_host'];
}
public function getDatabaseUrl(): ?string
{
return $this->data['_tenancy_db_url'];
}
public function getDatabaseUsername(): string
{
return $this->data['_tenancy_db_username'];
}
public function getDatabasePassword(): string
{
return $this->data['_tenancy_db_password'];
}
public function getDatabasePort(): ?string
{
return $this->data['_tenancy_db_port'] ?? null;
}
public function getDatabaseGrants(): array
{
return $this->data['_tenancy_db_grants'] ?? $this->config['tenancy.database.grants'];
}
public function generateDatabaseUsername(): string
{
return Str::random(16);
}
public function generateDatabasePassword(): string
{
return Hash::make(Str::random(16));
}
public function getDatabaseLink(): ?string
{
return $this->data['_tenancy_db_link'] ?? null;
}
}