mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 19:34:04 +00:00
[2.2.0] [WIP] Add functionality (#206)
* TenantDatabaseDoesNotExistException * Apply fixes from StyleCI * User post-creation callbacks * Rename method * postCreationActions * pass $tenant as parameter * pass $tenant to async actions * WIP findBy() * findBy\* ForwardsCalls * Apply fixes from StyleCI * findBy DB storage driver * Redis SD TODO message * Apply fixes from StyleCI * Fix chained jobs * WIP event system * import str * instanceof closure check * findBy instead of find * Tenant -> Tenants * dots * Use DB hooks instead of a SC key * Don't allow callables for queue chain * CanDeleteKeys interface * Apply fixes from StyleCI * CanFindByAnyKey interface * Apply fixes from StyleCI * Ditch models for custom repositories * Resolve circular dependency * Apply fixes from StyleCI * Fix tests * Apply fixes from StyleCI * FutureTest * Prefix tenant events with 'tenant.' * Event listener arguments test
This commit is contained in:
parent
7389f44de9
commit
869ac32983
22 changed files with 654 additions and 254 deletions
|
|
@ -1,15 +1,14 @@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
StyleCI will flag code style violations in your pull requests.
|
||||||
StyleCI will automatically fix code style violations in your pull requests.
|
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
If you have Docker installed, simply run ./test. When you're done testing, run docker-compose down to shut down the containers.
|
If you have Docker installed, simply run ./fulltest. When you're done testing, run docker-compose down to shut down the containers.
|
||||||
|
|
||||||
### Without Docker
|
### Without Docker
|
||||||
If you run the tests of this package, please make sure you don't store anything in Redis @ 127.0.0.1:6379 db#14. The contents of this database are flushed everytime the tests are run.
|
If you run the tests of this package, please make sure you don't store anything in Redis @ 127.0.0.1:6379 db#14. The contents of this database are flushed everytime the tests are run.
|
||||||
|
|
||||||
Some tests are run only if the CI, TRAVIS and CONTINUOUS_INTEGRATION environment variables are set to true. This is to avoid things like bloating your MySQL instance with test databases.
|
Some tests are run only if the `CONTINUOUS_INTEGRATION` or `DOCKER` environment variables are set to true. This is to avoid things like bloating your MySQL instance with test databases.
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ return [
|
||||||
],
|
],
|
||||||
'connection' => null, // Your central database connection. Set to null to use the default connection.
|
'connection' => null, // Your central database connection. Set to null to use the default connection.
|
||||||
'table_names' => [
|
'table_names' => [
|
||||||
'TenantModel' => 'tenants',
|
'tenants' => 'tenants',
|
||||||
'DomainModel' => 'domains',
|
'domains' => 'domains',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'redis' => [
|
'redis' => [
|
||||||
|
|
@ -93,7 +93,7 @@ return [
|
||||||
'migrate_after_creation' => false, // run migrations after creating a tenant
|
'migrate_after_creation' => false, // run migrations after creating a tenant
|
||||||
'seed_after_migration' => false, // should the seeder run after automatic migration
|
'seed_after_migration' => false, // should the seeder run after automatic migration
|
||||||
'seeder_parameters' => [
|
'seeder_parameters' => [
|
||||||
'--class' => 'DatabaseSeeder', // root seeder class to run after automatic migrations, eg: 'DatabaseSeeder'
|
'--class' => 'DatabaseSeeder', // root seeder class to run after automatic migrations, e.g.: 'DatabaseSeeder'
|
||||||
],
|
],
|
||||||
'queue_database_deletion' => false,
|
'queue_database_deletion' => false,
|
||||||
'delete_database_after_tenant_deletion' => false, // delete the tenant's database after deleting the tenant
|
'delete_database_after_tenant_deletion' => false, // delete the tenant's database after deleting the tenant
|
||||||
|
|
|
||||||
21
src/Contracts/Future/CanDeleteKeys.php
Normal file
21
src/Contracts/Future/CanDeleteKeys.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Contracts\Future;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface will be part of the StorageDriver interface in 3.x.
|
||||||
|
*/
|
||||||
|
interface CanDeleteKeys
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Delete keys from the storage.
|
||||||
|
*
|
||||||
|
* @param string[] $keys
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function deleteMany(array $keys, Tenant $tenant = null): void;
|
||||||
|
}
|
||||||
24
src/Contracts/Future/CanFindByAnyKey.php
Normal file
24
src/Contracts/Future/CanFindByAnyKey.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Contracts\Future;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface *might* be part of the StorageDriver interface in 3.x.
|
||||||
|
*/
|
||||||
|
interface CanFindByAnyKey
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Find a tenant using an arbitrary key.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @return Tenant
|
||||||
|
* @throws TenantDoesNotExistException
|
||||||
|
*/
|
||||||
|
public function findBy(string $key, $value): Tenant;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy;
|
namespace Stancl\Tenancy;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
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\TenantDatabaseManager;
|
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||||
|
|
@ -23,6 +25,9 @@ class DatabaseManager
|
||||||
/** @var BaseDatabaseManager */
|
/** @var BaseDatabaseManager */
|
||||||
protected $database;
|
protected $database;
|
||||||
|
|
||||||
|
/** @var TenantManager */
|
||||||
|
protected $tenancy;
|
||||||
|
|
||||||
public function __construct(Application $app, BaseDatabaseManager $database)
|
public function __construct(Application $app, BaseDatabaseManager $database)
|
||||||
{
|
{
|
||||||
$this->app = $app;
|
$this->app = $app;
|
||||||
|
|
@ -30,6 +35,19 @@ class DatabaseManager
|
||||||
$this->originalDefaultConnectionName = $app['config']['database.default'];
|
$this->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
|
||||||
|
{
|
||||||
|
$this->tenancy = $tenantManager;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a tenant's database.
|
* Connect to a tenant's database.
|
||||||
*
|
*
|
||||||
|
|
@ -140,7 +158,7 @@ class DatabaseManager
|
||||||
* Create a database for a tenant.
|
* Create a database for a tenant.
|
||||||
*
|
*
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @param \Illuminate\Contracts\Queue\ShouldQueue[]|callable[] $afterCreating
|
* @param ShouldQueue[]|callable[] $afterCreating
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function createDatabase(Tenant $tenant, array $afterCreating = [])
|
public function createDatabase(Tenant $tenant, array $afterCreating = [])
|
||||||
|
|
@ -148,16 +166,36 @@ class DatabaseManager
|
||||||
$database = $tenant->getDatabaseName();
|
$database = $tenant->getDatabaseName();
|
||||||
$manager = $this->getTenantDatabaseManager($tenant);
|
$manager = $this->getTenantDatabaseManager($tenant);
|
||||||
|
|
||||||
|
$afterCreating = array_merge(
|
||||||
|
$afterCreating,
|
||||||
|
$this->tenancy->event('database.creating', $database, $tenant)
|
||||||
|
);
|
||||||
|
|
||||||
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
|
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
|
||||||
QueuedTenantDatabaseCreator::withChain($afterCreating)->dispatch($manager, $database);
|
$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);
|
||||||
} else {
|
} else {
|
||||||
$manager->createDatabase($database);
|
$manager->createDatabase($database);
|
||||||
foreach ($afterCreating as $callback) {
|
foreach ($afterCreating as $item) {
|
||||||
$callback();
|
if (is_object($item) && ! $item instanceof Closure) {
|
||||||
|
$item->handle($tenant);
|
||||||
|
} else {
|
||||||
|
$item($tenant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->tenancy->event('database.created', $database, $tenant);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a tenant's database.
|
* Delete a tenant's database.
|
||||||
*
|
*
|
||||||
|
|
@ -169,11 +207,15 @@ class DatabaseManager
|
||||||
$database = $tenant->getDatabaseName();
|
$database = $tenant->getDatabaseName();
|
||||||
$manager = $this->getTenantDatabaseManager($tenant);
|
$manager = $this->getTenantDatabaseManager($tenant);
|
||||||
|
|
||||||
|
$this->tenancy->event('database.deleting', $database, $tenant);
|
||||||
|
|
||||||
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
|
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
|
||||||
QueuedTenantDatabaseDeleter::dispatch($manager, $database);
|
QueuedTenantDatabaseDeleter::dispatch($manager, $database);
|
||||||
} else {
|
} else {
|
||||||
$manager->deleteDatabase($database);
|
$manager->deleteDatabase($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->tenancy->event('database.deleted', $database, $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -182,7 +224,7 @@ class DatabaseManager
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @return TenantDatabaseManager
|
* @return TenantDatabaseManager
|
||||||
*/
|
*/
|
||||||
protected function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
|
public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
|
||||||
{
|
{
|
||||||
$driver = $this->getDriver($this->getBaseConnection($tenant->getConnectionName()));
|
$driver = $this->getDriver($this->getBaseConnection($tenant->getConnectionName()));
|
||||||
|
|
||||||
|
|
|
||||||
15
src/Exceptions/NotImplementedException.php
Normal file
15
src/Exceptions/NotImplementedException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class NotImplementedException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($class, $method, $extra)
|
||||||
|
{
|
||||||
|
parent::__construct("The $class class does not implement the $method method. $extra");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Exceptions/TenantDatabaseDoesNotExistException.php
Normal file
15
src/Exceptions/TenantDatabaseDoesNotExistException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class TenantDatabaseDoesNotExistException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($database)
|
||||||
|
{
|
||||||
|
parent::__construct("Database $database does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,8 @@ use Exception;
|
||||||
|
|
||||||
class TenantDoesNotExistException extends Exception
|
class TenantDoesNotExistException extends Exception
|
||||||
{
|
{
|
||||||
public function __construct(string $id)
|
public function __construct(string $id, string $key = 'id')
|
||||||
{
|
{
|
||||||
$this->message = "Tenant with this id does not exist: $id";
|
$this->message = "Tenant with this $key does not exist: $id";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,51 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository as ConfigRepository;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
|
||||||
use Stancl\Tenancy\Contracts\StorageDriver;
|
use Stancl\Tenancy\Contracts\StorageDriver;
|
||||||
use Stancl\Tenancy\DatabaseManager;
|
use Stancl\Tenancy\DatabaseManager;
|
||||||
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
|
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
|
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
|
||||||
use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains;
|
|
||||||
use Stancl\Tenancy\StorageDrivers\Database\TenantModel as Tenants;
|
|
||||||
use Stancl\Tenancy\Tenant;
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
class DatabaseStorageDriver implements StorageDriver
|
class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAnyKey
|
||||||
{
|
{
|
||||||
/** @var Application */
|
/** @var Application */
|
||||||
protected $app;
|
protected $app;
|
||||||
|
|
||||||
/** @var \Illuminate\Database\Connection */
|
/** @var Connection */
|
||||||
protected $centralDatabase;
|
protected $centralDatabase;
|
||||||
|
|
||||||
|
/** @var TenantRepository */
|
||||||
|
protected $tenants;
|
||||||
|
|
||||||
|
/** @var DomainRepository */
|
||||||
|
protected $domains;
|
||||||
|
|
||||||
/** @var Tenant The default tenant. */
|
/** @var Tenant The default tenant. */
|
||||||
protected $tenant;
|
protected $tenant;
|
||||||
|
|
||||||
public function __construct(Application $app)
|
public function __construct(Application $app, ConfigRepository $config)
|
||||||
{
|
{
|
||||||
$this->app = $app;
|
$this->app = $app;
|
||||||
$this->centralDatabase = $this->getCentralConnection();
|
$this->centralDatabase = $this->getCentralConnection();
|
||||||
|
$this->tenants = new TenantRepository($config);
|
||||||
|
$this->domains = new DomainRepository($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the central database connection.
|
* Get the central database connection.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Connection
|
* @return Connection
|
||||||
*/
|
*/
|
||||||
public static function getCentralConnection(): \Illuminate\Database\Connection
|
public static function getCentralConnection(): Connection
|
||||||
{
|
{
|
||||||
return DB::connection(static::getCentralConnectionName());
|
return DB::connection(static::getCentralConnectionName());
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +60,7 @@ class DatabaseStorageDriver implements StorageDriver
|
||||||
|
|
||||||
public function findByDomain(string $domain): Tenant
|
public function findByDomain(string $domain): Tenant
|
||||||
{
|
{
|
||||||
$id = $this->getTenantIdByDomain($domain);
|
$id = $this->domains->getTenantIdByDomain($domain);
|
||||||
if (! $id) {
|
if (! $id) {
|
||||||
throw new TenantCouldNotBeIdentifiedException($domain);
|
throw new TenantCouldNotBeIdentifiedException($domain);
|
||||||
}
|
}
|
||||||
|
|
@ -60,30 +70,43 @@ class DatabaseStorageDriver implements StorageDriver
|
||||||
|
|
||||||
public function findById(string $id): Tenant
|
public function findById(string $id): Tenant
|
||||||
{
|
{
|
||||||
$tenant = Tenants::find($id);
|
$tenant = $this->tenants->find($id);
|
||||||
|
|
||||||
if (! $tenant) {
|
if (! $tenant) {
|
||||||
throw new TenantDoesNotExistException($id);
|
throw new TenantDoesNotExistException($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Tenant::fromStorage($tenant->decoded())
|
return Tenant::fromStorage($this->tenants->decodeData($tenant))
|
||||||
->withDomains($this->getTenantDomains($id));
|
->withDomains($this->domains->getTenantDomains($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTenantDomains($id)
|
/**
|
||||||
|
* Find a tenant using an arbitrary key.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @return Tenant
|
||||||
|
* @throws TenantDoesNotExistException
|
||||||
|
*/
|
||||||
|
public function findBy(string $key, $value): Tenant
|
||||||
{
|
{
|
||||||
return Domains::where('tenant_id', $id)->get()->map(function ($model) {
|
$tenant = $this->tenants->findBy($key, $value);
|
||||||
return $model->domain;
|
|
||||||
})->toArray();
|
if (! $tenant) {
|
||||||
|
throw new TenantDoesNotExistException($value, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tenant::fromStorage($this->tenants->decodeData($tenant))
|
||||||
|
->withDomains($this->domains->getTenantDomains($tenant['id']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
if (Tenants::find($tenant->id)) {
|
if ($this->tenants->exists($tenant)) {
|
||||||
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
|
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Domains::whereIn('domain', $tenant->domains)->exists()) {
|
if ($this->domains->occupied($tenant->domains)) {
|
||||||
throw new DomainsOccupiedByOtherTenantException;
|
throw new DomainsOccupiedByOtherTenantException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,53 +118,28 @@ class DatabaseStorageDriver implements StorageDriver
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTenantIdByDomain(string $domain): ?string
|
|
||||||
{
|
|
||||||
return Domains::where('domain', $domain)->first()->tenant_id ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createTenant(Tenant $tenant): void
|
public function createTenant(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->centralDatabase->transaction(function () use ($tenant) {
|
$this->centralDatabase->transaction(function () use ($tenant) {
|
||||||
Tenants::create(array_merge(Tenants::encodeData($tenant->data), [
|
$this->tenants->insert($tenant);
|
||||||
'id' => $tenant->id,
|
$this->domains->insertTenantDomains($tenant);
|
||||||
]))->toArray();
|
|
||||||
|
|
||||||
$domainData = [];
|
|
||||||
foreach ($tenant->domains as $domain) {
|
|
||||||
$domainData[] = ['domain' => $domain, 'tenant_id' => $tenant->id];
|
|
||||||
}
|
|
||||||
|
|
||||||
Domains::insert($domainData);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateTenant(Tenant $tenant): void
|
public function updateTenant(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->centralDatabase->transaction(function () use ($tenant) {
|
$this->centralDatabase->transaction(function () use ($tenant) {
|
||||||
Tenants::find($tenant->id)->putMany($tenant->data);
|
$this->tenants->updateTenant($tenant);
|
||||||
|
|
||||||
$original_domains = Domains::where('tenant_id', $tenant->id)->get()->map(function ($model) {
|
$this->domains->updateTenantDomains($tenant);
|
||||||
return $model->domain;
|
|
||||||
})->toArray();
|
|
||||||
$deleted_domains = array_diff($original_domains, $tenant->domains);
|
|
||||||
|
|
||||||
Domains::whereIn('domain', $deleted_domains)->delete();
|
|
||||||
|
|
||||||
foreach ($tenant->domains as $domain) {
|
|
||||||
Domains::firstOrCreate([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'domain' => $domain,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteTenant(Tenant $tenant): void
|
public function deleteTenant(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->centralDatabase->transaction(function () use ($tenant) {
|
$this->centralDatabase->transaction(function () use ($tenant) {
|
||||||
Tenants::find($tenant->id)->delete();
|
$this->tenants->where('id', $tenant->id)->delete();
|
||||||
Domains::where('tenant_id', $tenant->id)->delete();
|
$this->domains->where('tenant_id', $tenant->id)->delete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,8 +151,9 @@ class DatabaseStorageDriver implements StorageDriver
|
||||||
*/
|
*/
|
||||||
public function all(array $ids = []): array
|
public function all(array $ids = []): array
|
||||||
{
|
{
|
||||||
return Tenants::getAllTenants($ids)->map(function ($data) {
|
return $this->tenants->all($ids)->map(function ($data) {
|
||||||
return Tenant::fromStorage($data)->withDomains($this->getTenantDomains($data['id']));
|
return Tenant::fromStorage($data)
|
||||||
|
->withDomains($this->domains->getTenantDomains($data['id']));
|
||||||
})->toArray();
|
})->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,27 +169,26 @@ class DatabaseStorageDriver implements StorageDriver
|
||||||
|
|
||||||
public function get(string $key, Tenant $tenant = null)
|
public function get(string $key, Tenant $tenant = null)
|
||||||
{
|
{
|
||||||
$tenant = $tenant ?? $this->currentTenant();
|
return $this->tenants->get($key, $tenant ?? $this->currentTenant());
|
||||||
|
|
||||||
return Tenants::find($tenant->id)->get($key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMany(array $keys, Tenant $tenant = null): array
|
public function getMany(array $keys, Tenant $tenant = null): array
|
||||||
{
|
{
|
||||||
$tenant = $tenant ?? $this->currentTenant();
|
return $this->tenants->getMany($keys, $tenant ?? $this->currentTenant());
|
||||||
|
|
||||||
return Tenants::find($tenant->id)->getMany($keys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function put(string $key, $value, Tenant $tenant = null): void
|
public function put(string $key, $value, Tenant $tenant = null): void
|
||||||
{
|
{
|
||||||
$tenant = $tenant ?? $this->currentTenant();
|
$this->tenants->put($key, $value, $tenant ?? $this->currentTenant());
|
||||||
Tenants::find($tenant->id)->put($key, $value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function putMany(array $kvPairs, Tenant $tenant = null): void
|
public function putMany(array $kvPairs, Tenant $tenant = null): void
|
||||||
{
|
{
|
||||||
$tenant = $tenant ?? $this->currentTenant();
|
$this->tenants->putMany($kvPairs, $tenant ?? $this->currentTenant());
|
||||||
Tenants::find($tenant->id)->putMany($kvPairs);
|
}
|
||||||
|
|
||||||
|
public function deleteMany(array $keys, Tenant $tenant = null): void
|
||||||
|
{
|
||||||
|
$this->tenants->deleteMany($keys, $tenant ?? $this->currentTenant());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
|
||||||
*/
|
|
||||||
class DomainModel extends Model
|
|
||||||
{
|
|
||||||
use CentralConnection;
|
|
||||||
|
|
||||||
protected $guarded = [];
|
|
||||||
protected $primaryKey = 'domain';
|
|
||||||
public $incrementing = false;
|
|
||||||
public $timestamps = false;
|
|
||||||
|
|
||||||
public function getTable()
|
|
||||||
{
|
|
||||||
return config('tenancy.storage_drivers.db.table_names.DomainModel', 'domains');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
58
src/StorageDrivers/Database/DomainRepository.php
Normal file
58
src/StorageDrivers/Database/DomainRepository.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository as ConfigRepository;
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
|
class DomainRepository extends Repository
|
||||||
|
{
|
||||||
|
public function getTenantIdByDomain(string $domain): ?string
|
||||||
|
{
|
||||||
|
return $this->where('domain', $domain)->first()->tenant_id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function occupied(array $domains): bool
|
||||||
|
{
|
||||||
|
return $this->whereIn('domain', $domains)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTenantDomains($tenant)
|
||||||
|
{
|
||||||
|
$id = $tenant instanceof Tenant ? $tenant->id : $tenant;
|
||||||
|
|
||||||
|
return $this->where('tenant_id', $id)->get('domain')->pluck('domain')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insertTenantDomains(Tenant $tenant)
|
||||||
|
{
|
||||||
|
$this->insert(array_map(function ($domain) use ($tenant) {
|
||||||
|
return ['domain' => $domain, 'tenant_id' => $tenant->id];
|
||||||
|
}, $tenant->domains));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTenantDomains(Tenant $tenant)
|
||||||
|
{
|
||||||
|
$originalDomains = $this->getTenantDomains($tenant);
|
||||||
|
$deletedDomains = array_diff($originalDomains, $tenant->domains);
|
||||||
|
$newDomains = array_diff($tenant->domains, $originalDomains);
|
||||||
|
|
||||||
|
$this->whereIn('domain', $deletedDomains)->delete();
|
||||||
|
|
||||||
|
foreach ($newDomains as $domain) {
|
||||||
|
$this->insert([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'domain' => $domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/StorageDrivers/Database/Repository.php
Normal file
41
src/StorageDrivers/Database/Repository.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository as ConfigRepository;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
|
||||||
|
/** @mixin Builder */
|
||||||
|
abstract class Repository
|
||||||
|
{
|
||||||
|
/** @var Connection */
|
||||||
|
public $database;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $tableName;
|
||||||
|
|
||||||
|
/** @var Builder */
|
||||||
|
private $table;
|
||||||
|
|
||||||
|
public function __construct(ConfigRepository $config)
|
||||||
|
{
|
||||||
|
$this->database = DatabaseStorageDriver::getCentralConnection();
|
||||||
|
$this->tableName = $this->getTable($config);
|
||||||
|
$this->table = $this->database->table($this->tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table()
|
||||||
|
{
|
||||||
|
return $this->table->newQuery()->from($this->tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function getTable(ConfigRepository $config);
|
||||||
|
|
||||||
|
public function __call($method, $parameters)
|
||||||
|
{
|
||||||
|
return $this->table()->$method(...$parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
|
||||||
*/
|
|
||||||
class TenantModel extends Model
|
|
||||||
{
|
|
||||||
use CentralConnection;
|
|
||||||
|
|
||||||
protected $guarded = [];
|
|
||||||
protected $primaryKey = 'id';
|
|
||||||
public $incrementing = false;
|
|
||||||
public $timestamps = false;
|
|
||||||
|
|
||||||
public function getTable()
|
|
||||||
{
|
|
||||||
return config('tenancy.storage_drivers.db.table_names.TenantModel', 'tenants');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataColumn()
|
|
||||||
{
|
|
||||||
return config('tenancy.storage_drivers.db.data_column', 'data');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function customColumns()
|
|
||||||
{
|
|
||||||
return config('tenancy.storage_drivers.db.custom_columns', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function encodeData(array $data)
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
$jsonData = [];
|
|
||||||
|
|
||||||
foreach ($data as $key => $value) {
|
|
||||||
if (in_array($key, static::customColumns(), true)) {
|
|
||||||
$result[$key] = $value;
|
|
||||||
} else {
|
|
||||||
$jsonData[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$result['data'] = $jsonData ? json_encode($jsonData) : '{}';
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getAllTenants(array $ids)
|
|
||||||
{
|
|
||||||
$tenants = $ids ? static::findMany($ids) : static::all();
|
|
||||||
|
|
||||||
return $tenants->map([__CLASS__, 'decodeData'])->toBase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function decoded()
|
|
||||||
{
|
|
||||||
return static::decodeData($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a tenant array with data decoded into separate keys.
|
|
||||||
*
|
|
||||||
* @param self|array $tenant
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function decodeData($tenant)
|
|
||||||
{
|
|
||||||
$tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant;
|
|
||||||
$decoded = json_decode($tenant[$dataColumn = static::dataColumn()], true);
|
|
||||||
|
|
||||||
foreach ($decoded as $key => $value) {
|
|
||||||
$tenant[$key] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If $tenant[$dataColumn] has been overriden by a value, don't delete the key.
|
|
||||||
if (! array_key_exists($dataColumn, $decoded)) {
|
|
||||||
unset($tenant[$dataColumn]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFromData(string $key)
|
|
||||||
{
|
|
||||||
$this->dataArray = $this->dataArray ?? json_decode($this->{$this->dataColumn()}, true);
|
|
||||||
|
|
||||||
return $this->dataArray[$key] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get(string $key)
|
|
||||||
{
|
|
||||||
return $this->attributes[$key] ?? $this->getFromData($key) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMany(array $keys): array
|
|
||||||
{
|
|
||||||
return array_reduce($keys, function ($result, $key) {
|
|
||||||
$result[$key] = $this->get($key);
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function put(string $key, $value)
|
|
||||||
{
|
|
||||||
if (in_array($key, $this->customColumns())) {
|
|
||||||
$this->update([$key => $value]);
|
|
||||||
} else {
|
|
||||||
$obj = json_decode($this->{$this->dataColumn()});
|
|
||||||
$obj->$key = $value;
|
|
||||||
|
|
||||||
$this->update([$this->dataColumn() => json_encode($obj)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function putMany(array $kvPairs)
|
|
||||||
{
|
|
||||||
$customColumns = [];
|
|
||||||
$jsonObj = json_decode($this->{$this->dataColumn()});
|
|
||||||
|
|
||||||
foreach ($kvPairs as $key => $value) {
|
|
||||||
if (in_array($key, $this->customColumns())) {
|
|
||||||
$customColumns[$key] = $value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$jsonObj->$key = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->update(array_merge($customColumns, [
|
|
||||||
$this->dataColumn() => json_encode($jsonObj),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
186
src/StorageDrivers/Database/TenantRepository.php
Normal file
186
src/StorageDrivers/Database/TenantRepository.php
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository as ConfigRepository;
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class TenantRepository extends Repository
|
||||||
|
{
|
||||||
|
public function all($ids = [])
|
||||||
|
{
|
||||||
|
if ($ids) {
|
||||||
|
$data = $this->whereIn('id', $ids)->get();
|
||||||
|
} else {
|
||||||
|
$data = $this->table()->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data->map(function (stdClass $obj) {
|
||||||
|
return $this->decodeData((array) $obj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($tenant)
|
||||||
|
{
|
||||||
|
return (array) $this->table()->find(
|
||||||
|
$tenant instanceof Tenant ? $tenant->id : $tenant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findBy(string $key, $value)
|
||||||
|
{
|
||||||
|
if (in_array($key, static::customColumns())) {
|
||||||
|
return (array) $this->table()->where($key, $value)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (array) $this->table()->where(
|
||||||
|
static::dataColumn() . '->' . $key,
|
||||||
|
$value
|
||||||
|
)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTenant(Tenant $tenant)
|
||||||
|
{
|
||||||
|
$this->putMany($tenant->data, $tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exists(Tenant $tenant)
|
||||||
|
{
|
||||||
|
return $this->where('id', $tenant->id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, Tenant $tenant)
|
||||||
|
{
|
||||||
|
return $this->decodeFreshDataForTenant($tenant)[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMany(array $keys, Tenant $tenant)
|
||||||
|
{
|
||||||
|
$decodedData = $this->decodeFreshDataForTenant($tenant);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$result[$key] = $decodedData[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $key, $value, Tenant $tenant)
|
||||||
|
{
|
||||||
|
$record = $this->where('id', $tenant->id);
|
||||||
|
|
||||||
|
if (in_array($key, static::customColumns())) {
|
||||||
|
$record->update([$key => $value]);
|
||||||
|
} else {
|
||||||
|
$data = json_decode($record->first(static::dataColumn())->data, true);
|
||||||
|
$data[$key] = $value;
|
||||||
|
|
||||||
|
$record->update([static::dataColumn() => $data]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putMany(array $kvPairs, Tenant $tenant)
|
||||||
|
{
|
||||||
|
$record = $this->where('id', $tenant->id);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
$jsonData = json_decode($record->first(static::dataColumn())->data, true);
|
||||||
|
foreach ($kvPairs as $key => $value) {
|
||||||
|
if (in_array($key, static::customColumns())) {
|
||||||
|
$data[$key] = $value;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$jsonData[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[static::dataColumn()] = json_encode($jsonData);
|
||||||
|
|
||||||
|
$record->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteMany(array $keys, Tenant $tenant)
|
||||||
|
{
|
||||||
|
$record = $this->where('id', $tenant->id);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
$jsonData = json_decode($record->first(static::dataColumn())->data, true);
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (in_array($key, static::customColumns())) {
|
||||||
|
$data[$key] = null;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
unset($jsonData[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[static::dataColumn()] = json_encode($jsonData);
|
||||||
|
|
||||||
|
$record->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decodeFreshDataForTenant(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
return $this->decodeData(
|
||||||
|
(array) $this->table()->where('id', $tenant->id)->first()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decodeData(array $columns): array
|
||||||
|
{
|
||||||
|
$dataColumn = static::dataColumn();
|
||||||
|
$decoded = json_decode($columns[$dataColumn], true);
|
||||||
|
$columns = array_merge($columns, $decoded);
|
||||||
|
|
||||||
|
// If $columns[$dataColumn] has been overriden by a value, don't delete the key.
|
||||||
|
if (! array_key_exists($dataColumn, $decoded)) {
|
||||||
|
unset($columns[$dataColumn]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(Tenant $tenant)
|
||||||
|
{
|
||||||
|
$this->table()->insert(array_merge(
|
||||||
|
$this->encodeData($tenant->data),
|
||||||
|
['id' => $tenant->id]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function encodeData(array $data): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach (array_intersect(static::customColumns(), array_keys($data)) as $customColumn) {
|
||||||
|
$result[$customColumn] = $data[$customColumn];
|
||||||
|
unset($data[$customColumn]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array_merge($result, [static::dataColumn() => json_encode($data)]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function customColumns(): array
|
||||||
|
{
|
||||||
|
return config('tenancy.storage_drivers.db.custom_columns', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function dataColumn(): string
|
||||||
|
{
|
||||||
|
return config('tenancy.storage_drivers.db.data_column', 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTable(ConfigRepository $config)
|
||||||
|
{
|
||||||
|
return $config->get('tenancy.storage_drivers.db.table_names.TenantModel') // legacy
|
||||||
|
?? $config->get('tenancy.storage_drivers.db.table_names.tenants')
|
||||||
|
?? 'tenants';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\StorageDrivers;
|
||||||
|
|
||||||
use Illuminate\Contracts\Redis\Factory as Redis;
|
use Illuminate\Contracts\Redis\Factory as Redis;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
|
||||||
use Stancl\Tenancy\Contracts\StorageDriver;
|
use Stancl\Tenancy\Contracts\StorageDriver;
|
||||||
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
|
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||||
|
|
@ -13,7 +14,7 @@ use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
|
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
|
||||||
use Stancl\Tenancy\Tenant;
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
class RedisStorageDriver implements StorageDriver
|
class RedisStorageDriver implements StorageDriver, CanDeleteKeys
|
||||||
{
|
{
|
||||||
/** @var Application */
|
/** @var Application */
|
||||||
protected $app;
|
protected $app;
|
||||||
|
|
@ -230,4 +231,11 @@ class RedisStorageDriver implements StorageDriver
|
||||||
|
|
||||||
$this->redis->hmset("tenants:{$tenant->id}", $kvPairs);
|
$this->redis->hmset("tenants:{$tenant->id}", $kvPairs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteMany(array $keys, Tenant $tenant = null): void
|
||||||
|
{
|
||||||
|
$tenant = $tenant ?? $this->tenant();
|
||||||
|
|
||||||
|
$this->redis->hdel("tenants:{$tenant->id}", ...$keys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\TenancyBootstrappers;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\DatabaseManager;
|
use Stancl\Tenancy\DatabaseManager;
|
||||||
|
use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException;
|
||||||
use Stancl\Tenancy\Tenant;
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
@ -20,6 +21,11 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
|
||||||
public function start(Tenant $tenant)
|
public function start(Tenant $tenant)
|
||||||
{
|
{
|
||||||
|
$database = $tenant->getDatabaseName();
|
||||||
|
if (! $this->database->getTenantDatabaseManager($tenant)->databaseExists($database)) {
|
||||||
|
throw new TenantDatabaseDoesNotExistException($database);
|
||||||
|
}
|
||||||
|
|
||||||
$this->database->connect($tenant);
|
$this->database->connect($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ use Closure;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Support\Traits\ForwardsCalls;
|
use Illuminate\Support\Traits\ForwardsCalls;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
|
||||||
use Stancl\Tenancy\Contracts\StorageDriver;
|
use Stancl\Tenancy\Contracts\StorageDriver;
|
||||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
|
use Stancl\Tenancy\Exceptions\NotImplementedException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantStorageException;
|
use Stancl\Tenancy\Exceptions\TenantStorageException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -349,6 +351,39 @@ class Tenant implements ArrayAccess
|
||||||
return $this->put($key, $value);
|
return $this->put($key, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a key from the tenant's storage.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function deleteKey(string $key): self
|
||||||
|
{
|
||||||
|
return $this->deleteKeys([$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete keys from the tenant's storage.
|
||||||
|
*
|
||||||
|
* @param string[] $keys
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function deleteKeys(array $keys): self
|
||||||
|
{
|
||||||
|
if (! $this->storage instanceof CanDeleteKeys) {
|
||||||
|
throw new NotImplementedException(get_class($this->storage), 'deleteMany',
|
||||||
|
'This method was added to storage drivers provided by the package in 2.2.0 and will be part of the StorageDriver contract in 3.0.0.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->storage->deleteMany($keys);
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
unset($this->data[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a value.
|
* Set a value.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,15 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy;
|
namespace Stancl\Tenancy;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Traits\ForwardsCalls;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
|
||||||
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
|
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
|
||||||
|
use Stancl\Tenancy\Exceptions\NotImplementedException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||||
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
|
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
|
||||||
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
|
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
|
||||||
|
|
@ -17,6 +22,8 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
|
||||||
*/
|
*/
|
||||||
class TenantManager
|
class TenantManager
|
||||||
{
|
{
|
||||||
|
use ForwardsCalls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current tenant.
|
* The current tenant.
|
||||||
*
|
*
|
||||||
|
|
@ -47,7 +54,7 @@ class TenantManager
|
||||||
$this->app = $app;
|
$this->app = $app;
|
||||||
$this->storage = $storage;
|
$this->storage = $storage;
|
||||||
$this->artisan = $artisan;
|
$this->artisan = $artisan;
|
||||||
$this->database = $database;
|
$this->database = $database->withTenantManager($this);
|
||||||
|
|
||||||
$this->bootstrapFeatures();
|
$this->bootstrapFeatures();
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +67,8 @@ class TenantManager
|
||||||
*/
|
*/
|
||||||
public function createTenant(Tenant $tenant): self
|
public function createTenant(Tenant $tenant): self
|
||||||
{
|
{
|
||||||
|
$this->event('tenant.creating', $tenant);
|
||||||
|
|
||||||
$this->ensureTenantCanBeCreated($tenant);
|
$this->ensureTenantCanBeCreated($tenant);
|
||||||
|
|
||||||
$this->storage->createTenant($tenant);
|
$this->storage->createTenant($tenant);
|
||||||
|
|
@ -89,6 +98,8 @@ class TenantManager
|
||||||
|
|
||||||
$this->database->createDatabase($tenant, $afterCreating);
|
$this->database->createDatabase($tenant, $afterCreating);
|
||||||
|
|
||||||
|
$this->event('tenant.created', $tenant);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,12 +111,16 @@ class TenantManager
|
||||||
*/
|
*/
|
||||||
public function deleteTenant(Tenant $tenant): self
|
public function deleteTenant(Tenant $tenant): self
|
||||||
{
|
{
|
||||||
|
$this->event('tenant.deleting', $tenant);
|
||||||
|
|
||||||
$this->storage->deleteTenant($tenant);
|
$this->storage->deleteTenant($tenant);
|
||||||
|
|
||||||
if ($this->shouldDeleteDatabase()) {
|
if ($this->shouldDeleteDatabase()) {
|
||||||
$this->database->deleteDatabase($tenant);
|
$this->database->deleteDatabase($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->event('tenant.deleted', $tenant);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +213,34 @@ class TenantManager
|
||||||
return $this->storage->findByDomain($domain);
|
return $this->storage->findByDomain($domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a tenant using an arbitrary key.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @return Tenant
|
||||||
|
* @throws TenantCouldNotBeIdentifiedException
|
||||||
|
* @throws NotImplementedException
|
||||||
|
*/
|
||||||
|
public function findBy(string $key, $value): Tenant
|
||||||
|
{
|
||||||
|
if ($key === null) {
|
||||||
|
throw new Exception('No key supplied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
throw new Exception('No value supplied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->storage instanceof CanFindByAnyKey) {
|
||||||
|
throw new NotImplementedException(get_class($this->storage), 'findBy',
|
||||||
|
'This method was added to the DB storage driver provided by the package in 2.2.0 and might be part of the StorageDriver contract in 3.0.0.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->storage->findBy($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all tenants.
|
* Get all tenants.
|
||||||
*
|
*
|
||||||
|
|
@ -246,13 +289,13 @@ class TenantManager
|
||||||
*/
|
*/
|
||||||
public function bootstrapTenancy(Tenant $tenant): self
|
public function bootstrapTenancy(Tenant $tenant): self
|
||||||
{
|
{
|
||||||
$prevented = $this->event('bootstrapping');
|
$prevented = $this->event('bootstrapping', $tenant);
|
||||||
|
|
||||||
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
||||||
$this->app[$bootstrapper]->start($tenant);
|
$this->app[$bootstrapper]->start($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->event('bootstrapped');
|
$this->event('bootstrapped', $tenant);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
@ -263,7 +306,7 @@ class TenantManager
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
$prevented = $this->event('ending');
|
$prevented = $this->event('ending', $this->getTenant());
|
||||||
|
|
||||||
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
||||||
$this->app[$bootstrapper]->end();
|
$this->app[$bootstrapper]->end();
|
||||||
|
|
@ -383,17 +426,27 @@ class TenantManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute event listeners.
|
* Trigger an event and execute event listeners.
|
||||||
*
|
*
|
||||||
* @param string $name
|
* @param string $name
|
||||||
|
* @param mixed ...$args
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
protected function event(string $name): array
|
public function event(string $name, ...$args): array
|
||||||
{
|
{
|
||||||
return array_reduce($this->eventListeners[$name] ?? [], function ($prevented, $listener) {
|
return array_reduce($this->eventListeners[$name] ?? [], function ($results, $listener) use ($args) {
|
||||||
$prevented = array_merge($prevented, $listener($this) ?? []);
|
$results = array_merge($results, $listener($this, ...$args) ?? []);
|
||||||
|
|
||||||
return $prevented;
|
return $results;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __call($method, $parameters)
|
||||||
|
{
|
||||||
|
if (Str::startsWith($method, 'findBy')) {
|
||||||
|
return $this->findBy(Str::snake(substr($method, 6)), $parameters[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static::throwBadMethodCallException($method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,19 @@ use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
class CacheManagerTest extends TestCase
|
class CacheManagerTest extends TestCase
|
||||||
{
|
{
|
||||||
|
public $autoInitTenancy = false;
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function default_tag_is_automatically_applied()
|
public function default_tag_is_automatically_applied()
|
||||||
{
|
{
|
||||||
|
$this->initTenancy();
|
||||||
$this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
|
$this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function tags_are_merged_when_array_is_passed()
|
public function tags_are_merged_when_array_is_passed()
|
||||||
{
|
{
|
||||||
|
$this->initTenancy();
|
||||||
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
|
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
|
||||||
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
|
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +28,7 @@ class CacheManagerTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function tags_are_merged_when_string_is_passed()
|
public function tags_are_merged_when_string_is_passed()
|
||||||
{
|
{
|
||||||
|
$this->initTenancy();
|
||||||
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
|
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
|
||||||
$this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames());
|
$this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames());
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +36,7 @@ class CacheManagerTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method()
|
public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method()
|
||||||
{
|
{
|
||||||
|
$this->initTenancy();
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
cache()->tags();
|
cache()->tags();
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +44,7 @@ class CacheManagerTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method()
|
public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method()
|
||||||
{
|
{
|
||||||
|
$this->initTenancy();
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
cache()->tags(1, 2);
|
cache()->tags(1, 2);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
tests/FutureTest.php
Normal file
45
tests/FutureTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
|
class FutureTest extends TestCase
|
||||||
|
{
|
||||||
|
public $autoCreateTenant = false;
|
||||||
|
public $autoInitTenancy = false;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function keys_can_be_deleted_from_tenant_storage()
|
||||||
|
{
|
||||||
|
$tenant = Tenant::new()->withData(['email' => 'foo@example.com', 'role' => 'admin'])->save();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('email', $tenant->data);
|
||||||
|
$tenant->deleteKey('email');
|
||||||
|
$this->assertArrayNotHasKey('email', $tenant->data);
|
||||||
|
$this->assertArrayNotHasKey('email', tenancy()->all()->first()->data);
|
||||||
|
|
||||||
|
$tenant->put(['foo' => 'bar', 'abc' => 'xyz']);
|
||||||
|
$this->assertArrayHasKey('foo', $tenant->data);
|
||||||
|
$this->assertArrayHasKey('abc', $tenant->data);
|
||||||
|
|
||||||
|
$tenant->deleteKeys(['foo', 'abc']);
|
||||||
|
$this->assertArrayNotHasKey('foo', $tenant->data);
|
||||||
|
$this->assertArrayNotHasKey('abc', $tenant->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function tenant_can_be_identified_using_an_arbitrary_string()
|
||||||
|
{
|
||||||
|
if (! tenancy()->storage instanceof CanFindByAnyKey) {
|
||||||
|
$this->markTestSkipped(get_class(tenancy()->storage) . ' does not implement the CanFindByAnyKey interface.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::new()->withData(['email' => 'foo@example.com'])->save();
|
||||||
|
|
||||||
|
$this->assertSame($tenant->id, tenancy()->findByEmail('foo@example.com')->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -320,4 +320,14 @@ class TenantManagerTest extends TestCase
|
||||||
$this->expectException(TenantDoesNotExistException::class);
|
$this->expectException(TenantDoesNotExistException::class);
|
||||||
tenancy()->find('gjnfdgf');
|
tenancy()->find('gjnfdgf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function event_listeners_can_accept_arguments()
|
||||||
|
{
|
||||||
|
tenancy()->hook('tenant.creating', function ($tenantManager, $tenant) {
|
||||||
|
$this->assertSame('bar', $tenant->foo);
|
||||||
|
});
|
||||||
|
|
||||||
|
Tenant::new()->withData(['foo' => 'bar'])->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Tests;
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
use Stancl\Tenancy\StorageDrivers\Database\TenantModel;
|
use Stancl\Tenancy\StorageDrivers\Database\TenantRepository;
|
||||||
use Stancl\Tenancy\Tenant;
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
class TenantStorageTest extends TestCase
|
class TenantStorageTest extends TestCase
|
||||||
|
|
@ -112,10 +112,11 @@ class TenantStorageTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function tenant_model_uses_correct_connection()
|
public function tenant_repository_uses_correct_connection()
|
||||||
{
|
{
|
||||||
|
config(['database.connections.foo' => config('database.connections.sqlite')]);
|
||||||
config(['tenancy.storage_drivers.db.connection' => 'foo']);
|
config(['tenancy.storage_drivers.db.connection' => 'foo']);
|
||||||
$this->assertSame('foo', (new TenantModel)->getConnectionName());
|
$this->assertSame('foo', app(TenantRepository::class)->database->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -156,6 +157,9 @@ class TenantStorageTest extends TestCase
|
||||||
tenancy()->create(['foo.localhost']);
|
tenancy()->create(['foo.localhost']);
|
||||||
tenancy()->init('foo.localhost');
|
tenancy()->init('foo.localhost');
|
||||||
|
|
||||||
|
tenant()->put('foo', '111');
|
||||||
|
$this->assertSame('111', tenant()->get('foo'));
|
||||||
|
|
||||||
tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
|
tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
|
||||||
$this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
|
$this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue