1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 08:24:05 +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' => [
'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',
'suffix' => '',
'separate_by' => 'database', // database or schema (only supported by pgsql)
'suffix' => ''
],
'redis' => [
'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;
use Stancl\Tenancy\Tenant;
interface TenantDatabaseManager
{
/**
* Create a database.
*
* @param string $name Name of the database.
* @param string $name Name of the database.
* @param Tenant $tenant
* @return bool
*/
public function createDatabase(string $name): bool;
public function createDatabase(string $name, Tenant $tenant): bool;
/**
* Delete a database.
@ -29,4 +32,14 @@ interface TenantDatabaseManager
* @return 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\Foundation\Application;
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
@ -58,7 +59,7 @@ class DatabaseManager
*/
public function connect(Tenant $tenant)
{
$this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName());
$this->createTenantConnection($tenant);
$this->setDefaultConnection($tenant->getConnectionName());
$this->switchConnection($tenant->getConnectionName());
}
@ -90,21 +91,21 @@ class DatabaseManager
/**
* Create the tenant database connection.
*
* @param string $databaseName
* @param string $connectionName
* @param Tenant $tenant
* @return void
* @throws DatabaseManagerNotRegisteredException
*/
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];
$configuration = $this->getTenantDatabaseManager($tenant)
->createDatabaseConnection(
$tenant,
$this->getBaseConnectionConfiguration($tenant)
);
// Change database name.
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
$separateBy = $this->separateBy($connectionName);
$connectionName = $tenant->getConnectionName();
$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 {
$manager->createDatabase($database);
$manager->createDatabase($database, $tenant);
foreach ($afterCreating as $item) {
if (is_object($item) && ! $item instanceof Closure) {
$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
* @return string
* @param Tenant $tenant
* @return array
*/
public function separateBy(string $connectionName): string
protected function getBaseConnectionConfiguration(Tenant $tenant): array
{
if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql'
&& $this->app['config']['tenancy.database.separate_by'] === 'schema') {
return 'schema';
}
$basedOn = $this->getBaseConnection($tenant->getConnectionName());
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\SerializesModels;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class QueuedTenantDatabaseCreator implements ShouldQueue
{
@ -21,17 +22,21 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
/** @var string */
protected $databaseName;
/** @var Tenant */
public $tenant;
/**
* Create a new job instance.
*
* @param TenantDatabaseManager $databaseManager
* @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->databaseName = $databaseName;
$this->tenant = $tenant;
}
/**
@ -41,6 +46,6 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
*/
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\Exceptions\NotImplementedException;
use Stancl\Tenancy\Exceptions\TenantStorageException;
use Stancl\Tenancy\Traits\ProvidesDatabaseUser;
/**
* @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
{
use Traits\HasArrayAccess,
ForwardsCalls;
ForwardsCalls,
ProvidesDatabaseUser;
/**
* Tenant data. A "cache" of tenant storage.

View file

@ -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
{
@ -30,7 +31,7 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $connection;
}
public function createDatabase(string $name): bool
public function createDatabase(string $name, Tenant $tenant): bool
{
$charset = $this->database()->getConfig('charset');
$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'");
}
/**
* @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 Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{
@ -30,7 +31,7 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti
$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");
}
@ -44,4 +45,18 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti
{
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 Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
{
@ -30,7 +31,7 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $connection;
}
public function createDatabase(string $name): bool
public function createDatabase(string $name, Tenant $tenant): bool
{
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'");
}
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;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
class SQLiteDatabaseManager implements TenantDatabaseManager
{
public function createDatabase(string $name): bool
public function createDatabase(string $name, Tenant $tenant): bool
{
try {
return fclose(fopen(database_path($name), 'w'));
@ -30,4 +31,11 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
{
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;
}
}