mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 10:54:04 +00:00
vague first draft of v3. TenantModelTest is passing
This commit is contained in:
parent
c2c90ff755
commit
bd9aad229b
56 changed files with 803 additions and 1366 deletions
78
assets/TenancyServiceProvider.stub.php
Normal file
78
assets/TenancyServiceProvider.stub.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Events\DatabaseCreated;
|
||||
use Stancl\Tenancy\Events\DatabaseDeleted;
|
||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||
use Stancl\Tenancy\Events\DatabaseSeeded;
|
||||
use Stancl\Tenancy\Events\Listeners\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Jobs\DeleteDatabase;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
use Stancl\Tenancy\Jobs\SeedDatabase;
|
||||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function events()
|
||||
{
|
||||
return [
|
||||
TenantCreated::class => [
|
||||
JobPipeline::make([
|
||||
CreateDatabase::class,
|
||||
MigrateDatabase::class, // triggers DatabaseMigrated event
|
||||
SeedDatabase::class,
|
||||
])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->queue(true),
|
||||
],
|
||||
DatabaseCreated::class => [],
|
||||
DatabaseMigrated::class => [],
|
||||
DatabaseSeeded::class => [],
|
||||
TenantDeleted::class => [
|
||||
JobPipeline::make([
|
||||
DeleteDatabase::class,
|
||||
])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->queue(true),
|
||||
// DeleteStorage::class,
|
||||
],
|
||||
DatabaseDeleted::class => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$this->bootEvents();
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
protected function bootEvents()
|
||||
{
|
||||
foreach ($this->events() as $event => $listeners) {
|
||||
foreach (array_unique($listeners) as $listener) {
|
||||
// Technically, the string|Closure typehint is not enforced by
|
||||
// Laravel, but for type correctness, we wrap callables in
|
||||
// simple Closures, to match Laravel's docblock typehint.
|
||||
if (is_callable($listener) && !$listener instanceof Closure) {
|
||||
$listener = function ($event) use ($listener) {
|
||||
$listener($event);
|
||||
};
|
||||
}
|
||||
|
||||
Event::listen($event, $listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,68 +2,36 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
return [
|
||||
/**
|
||||
* Storage drivers are used to store information about your tenants.
|
||||
* They hold the Tenant Storage data and keeps track of domains.
|
||||
*/
|
||||
'storage_driver' => 'db',
|
||||
'storage_drivers' => [
|
||||
/**
|
||||
* The majority of applications will want to use this storage driver.
|
||||
* The information about tenants is persisted in a relational DB
|
||||
* like MySQL or PostgreSQL. The only downside is performance.
|
||||
*
|
||||
* A database connection to the central database has to be established on each
|
||||
* request, to identify the tenant based on the domain. This takes three DB
|
||||
* queries. Then, the connection to the tenant database is established.
|
||||
*
|
||||
* Note: From v2.3.0, the performance of the DB storage driver can be improved
|
||||
* by a lot by using Cached Tenant Lookup. Be sure to enable that if you're
|
||||
* using this storage driver. Enabling that feature can completely avoid
|
||||
* querying the central database to identify build the Tenant object.
|
||||
*/
|
||||
'db' => [
|
||||
'driver' => Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver::class,
|
||||
'data_column' => 'data',
|
||||
'custom_columns' => [
|
||||
// 'plan',
|
||||
],
|
||||
'tenant_model' => Tenant::class,
|
||||
'internal_prefix' => 'tenancy_',
|
||||
|
||||
/**
|
||||
* Your central database connection. Set to null to use the default one.
|
||||
*
|
||||
* Note: It's recommended to create a designated central connection,
|
||||
* to let you easily use it in your app, e.g. via the DB facade.
|
||||
*/
|
||||
'connection' => null,
|
||||
'central_connection' => 'central',
|
||||
'template_tenant_connection' => null,
|
||||
|
||||
'table_names' => [
|
||||
'tenants' => 'tenants',
|
||||
'domains' => 'domains',
|
||||
],
|
||||
'id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class,
|
||||
|
||||
/**
|
||||
* Here you can enable the Cached Tenant Lookup.
|
||||
*
|
||||
* You can specify what cache store should be used to cache the tenant resolution.
|
||||
* Set to string with a specific cache store name, or to null to disable cache.
|
||||
*/
|
||||
'cache_store' => null,
|
||||
'cache_ttl' => 3600, // seconds
|
||||
'custom_columns' => [
|
||||
//
|
||||
],
|
||||
|
||||
|
||||
'storage' => [
|
||||
'data_column' => 'data',
|
||||
'custom_columns' => [
|
||||
// 'plan',
|
||||
],
|
||||
|
||||
/**
|
||||
* The Redis storage driver is much more performant than the database driver.
|
||||
* However, by default, Redis is a not a durable data storage. It works well for ephemeral data
|
||||
* like cache, but to hold critical data, it needs to be configured in a way that guarantees
|
||||
* that data will be persisted permanently. Specifically, you want to enable both AOF and
|
||||
* RDB. Read this here: https://tenancy.samuelstancl.me/docs/v2/storage-drivers/#redis.
|
||||
* Here you can enable the Cached Tenant Lookup.
|
||||
*
|
||||
* You can specify what cache store should be used to cache the tenant resolution.
|
||||
* Set to string with a specific cache store name, or to null to disable cache.
|
||||
*/
|
||||
'redis' => [
|
||||
'driver' => Stancl\Tenancy\StorageDrivers\RedisStorageDriver::class,
|
||||
'connection' => 'tenancy',
|
||||
],
|
||||
'cache_store' => null, // env('CACHE_DRIVER')
|
||||
'cache_ttl' => 3600, // seconds
|
||||
],
|
||||
|
||||
/**
|
||||
|
|
@ -108,6 +76,7 @@ return [
|
|||
*/
|
||||
'prefix' => 'tenant',
|
||||
'suffix' => '',
|
||||
// todo get rid of this stuff, just set the closure instead
|
||||
],
|
||||
|
||||
/**
|
||||
|
|
@ -237,55 +206,30 @@ return [
|
|||
*/
|
||||
'home_url' => '/app',
|
||||
|
||||
/**
|
||||
* Automatically create a database when creating a tenant.
|
||||
*/
|
||||
'create_database' => true,
|
||||
|
||||
/**
|
||||
* Should tenant databases be created asynchronously in a queued job.
|
||||
*/
|
||||
'queue_database_creation' => false,
|
||||
'queue_database_creation' => false, // todo make this a static property
|
||||
|
||||
/**
|
||||
* Should tenant migrations be ran after the tenant's database is created.
|
||||
*/
|
||||
'migrate_after_creation' => false,
|
||||
'migration_parameters' => [
|
||||
'--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
|
||||
],
|
||||
|
||||
/**
|
||||
* Should tenant databases be automatically seeded after they're created & migrated.
|
||||
*/
|
||||
'seed_after_migration' => false, // should the seeder run after automatic migration
|
||||
'seeder_parameters' => [
|
||||
'--class' => 'DatabaseSeeder', // root seeder class, e.g.: 'DatabaseSeeder'
|
||||
// '--force' => true,
|
||||
],
|
||||
|
||||
/**
|
||||
* Automatically delete the tenant's database after the tenant is deleted.
|
||||
*
|
||||
* This will save space but permanently delete data which you might want to keep.
|
||||
*/
|
||||
'delete_database_after_tenant_deletion' => false,
|
||||
|
||||
/**
|
||||
* Should tenant databases be deleted asynchronously in a queued job.
|
||||
*/
|
||||
'queue_database_deletion' => false,
|
||||
|
||||
/**
|
||||
* If you don't supply an id when creating a tenant, this class will be used to generate a random ID.
|
||||
*/
|
||||
'unique_id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class,
|
||||
|
||||
/**
|
||||
* Middleware pushed to the global middleware stack.
|
||||
*/
|
||||
'global_middleware' => [
|
||||
'global_middleware' => [ // todo get rid of this
|
||||
Stancl\Tenancy\Middleware\InitializeTenancy::class,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ class CreateTenantsTable extends Migration
|
|||
Schema::create('tenants', function (Blueprint $table) {
|
||||
$table->string('id', 36)->primary(); // 36 characters is the default uuid length
|
||||
|
||||
// (optional) your custom, indexed columns may go here
|
||||
// your custom columns may go here
|
||||
|
||||
$table->json('data');
|
||||
$table->timestamps();
|
||||
$table->json('data')->default('{}');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stancl/tenancy",
|
||||
"description": "A Laravel multi-database tenancy package that respects your code.",
|
||||
"description": "Automatic multi-tenancy for your Laravel application.",
|
||||
"keywords": ["laravel", "multi-tenancy", "multi-database", "tenancy"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"laravel/framework": "^6.0|^7.0",
|
||||
"orchestra/testbench-browser-kit": "^4.0|^5.0",
|
||||
"league/flysystem-aws-s3-v3": "~1.0",
|
||||
"phpunit/phpcov": "^6.0|^7.0"
|
||||
"doctrine/dbal": "^2.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ services:
|
|||
DB_PASSWORD: password
|
||||
DB_USERNAME: root
|
||||
DB_DATABASE: main
|
||||
TENANCY_TEST_REDIS_HOST: redis
|
||||
TENANCY_TEST_MYSQL_HOST: mysql
|
||||
TENANCY_TEST_PGSQL_HOST: postgres
|
||||
stdin_open: true
|
||||
tty: true
|
||||
mysql:
|
||||
|
|
|
|||
1
fulltest
1
fulltest
|
|
@ -4,4 +4,3 @@ set -e
|
|||
# for development
|
||||
docker-compose up -d
|
||||
./test "$@"
|
||||
docker-compose exec -T test vendor/bin/phpcov merge --clover clover.xml coverage/
|
||||
|
|
|
|||
|
|
@ -30,6 +30,5 @@
|
|||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="central"/>
|
||||
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
||||
<env name="STANCL_TENANCY_TEST_VARIANT" value="1"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
@ -8,6 +8,7 @@ use Illuminate\Console\Command;
|
|||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||
use Stancl\Tenancy\Traits\DealsWithMigrations;
|
||||
use Stancl\Tenancy\Traits\HasATenantsOption;
|
||||
|
||||
|
|
@ -62,6 +63,8 @@ class Migrate extends MigrateCommand
|
|||
// Migrate
|
||||
parent::handle();
|
||||
});
|
||||
|
||||
event(new DatabaseMigrated($tenant));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands;
|
|||
use Illuminate\Database\ConnectionResolverInterface;
|
||||
use Illuminate\Database\Console\Seeds\SeedCommand;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\DatabaseSeeded;
|
||||
use Stancl\Tenancy\Traits\HasATenantsOption;
|
||||
|
||||
class Seed extends SeedCommand
|
||||
|
|
@ -60,6 +61,8 @@ class Seed extends SeedCommand
|
|||
// Seed
|
||||
parent::handle();
|
||||
});
|
||||
|
||||
event(new DatabaseSeeded($tenant));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Contracts;
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
interface UniqueIdentifierGenerator
|
||||
{
|
||||
/**
|
||||
* Generate a unique identifier.
|
||||
*
|
||||
* @param string[] $domains
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function generate(array $domains, array $data = []): string;
|
||||
public static function generate(Tenant $tenant): string;
|
||||
}
|
||||
|
|
|
|||
11
src/Database/Models/Concerns/CentralConnection.php
Normal file
11
src/Database/Models/Concerns/CentralConnection.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models\Concerns;
|
||||
|
||||
trait CentralConnection
|
||||
{
|
||||
public function getConnectionName()
|
||||
{
|
||||
return config('tenancy.central_connection');
|
||||
}
|
||||
}
|
||||
15
src/Database/Models/Concerns/GeneratesIds.php
Normal file
15
src/Database/Models/Concerns/GeneratesIds.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models\Concerns;
|
||||
|
||||
trait GeneratesIds
|
||||
{
|
||||
public static function bootGeneratesIds()
|
||||
{
|
||||
static::creating(function (self $model) {
|
||||
if (! $model->id && config('tenancy.id_generator')) {
|
||||
$model->id = app(config('tenancy.id_generator'))->generate($model);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
55
src/Database/Models/Concerns/HasADataColumn.php
Normal file
55
src/Database/Models/Concerns/HasADataColumn.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models\Concerns;
|
||||
|
||||
trait HasADataColumn
|
||||
{
|
||||
public static function bootHasADataColumn()
|
||||
{
|
||||
$encode = function (self $model) {
|
||||
foreach ($model->getAttributes() as $key => $value) {
|
||||
if (! in_array($key, static::getCustomColums())) {
|
||||
$current = $model->getAttribute(static::getDataColumn()) ?? [];
|
||||
|
||||
$model->setAttribute(static::getDataColumn(), array_merge($current, [
|
||||
$key => $value,
|
||||
]));
|
||||
|
||||
unset($model->attributes[$key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$decode = function (self $model) {
|
||||
foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) {
|
||||
$model->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
$model->setAttribute(static::getDataColumn(), null);
|
||||
};
|
||||
|
||||
static::saving($encode);
|
||||
static::saved($decode);
|
||||
static::retrieved($decode);
|
||||
}
|
||||
|
||||
public function getCasts()
|
||||
{
|
||||
return array_merge(parent::getCasts(), [
|
||||
static::getDataColumn() => 'array',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the column that stores additional data.
|
||||
*/
|
||||
public static function getDataColumn(): string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public static function getCustomColums(): array
|
||||
{
|
||||
return array_merge(['id'], config('tenancy.custom_columns'));
|
||||
}
|
||||
}
|
||||
24
src/Database/Models/Domain.php
Normal file
24
src/Database/Models/Domain.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Events\DomainCreated;
|
||||
use Stancl\Tenancy\Events\DomainDeleted;
|
||||
use Stancl\Tenancy\Events\DomainSaved;
|
||||
use Stancl\Tenancy\Events\DomainUpdated;
|
||||
|
||||
class Domain extends Model
|
||||
{
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
protected $dispatchEvents = [
|
||||
'saved' => DomainSaved::class,
|
||||
'created' => DomainCreated::class,
|
||||
'updated' => DomainUpdated::class,
|
||||
'deleted' => DomainDeleted::class,
|
||||
];
|
||||
}
|
||||
93
src/Database/Models/Tenant.php
Normal file
93
src/Database/Models/Tenant.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\DatabaseConfig;
|
||||
use Stancl\Tenancy\Events;
|
||||
|
||||
// todo use a contract
|
||||
class Tenant extends Model
|
||||
{
|
||||
use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn {
|
||||
Concerns\HasADataColumn::getCasts as dataColumnCasts;
|
||||
}
|
||||
|
||||
public $primaryKey = 'id';
|
||||
|
||||
public function getCasts()
|
||||
{
|
||||
return array_merge($this->dataColumnCasts(), [
|
||||
'id' => $this->getIncrementing() ? 'integer' : 'string',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getIncrementing()
|
||||
{
|
||||
return config('tenancy.id_generator') === null;
|
||||
}
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public function domains() // todo not required
|
||||
{
|
||||
return $this->hasMany(Domain::class);
|
||||
}
|
||||
|
||||
public static function internalPrefix(): string
|
||||
{
|
||||
return config('tenancy.database_prefix');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an internal key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getInternal(string $key)
|
||||
{
|
||||
return $this->getAttribute(static::internalPrefix() . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set internal key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setInternal(string $key, $value)
|
||||
{
|
||||
$this->setAttribute($key, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function database(): DatabaseConfig
|
||||
{
|
||||
return new DatabaseConfig($this);
|
||||
}
|
||||
|
||||
public function run(callable $callback)
|
||||
{
|
||||
$originalTenant = $this->manager->getTenant();
|
||||
|
||||
$this->manager->initializeTenancy($this);
|
||||
$result = $callback($this);
|
||||
$this->manager->endTenancy($this);
|
||||
|
||||
if ($originalTenant) {
|
||||
$this->manager->initializeTenancy($originalTenant);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected $dispatchesEvents = [
|
||||
'saved' => Events\TenantSaved::class,
|
||||
'created' => Events\TenantCreated::class,
|
||||
'updated' => Events\TenantUpdated::class,
|
||||
'deleted' => Events\TenantDeleted::class,
|
||||
];
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts\Future\CanSetConnection;
|
|||
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
|
||||
use Stancl\Tenancy\Contracts\ModifiesDatabaseNameForConnection;
|
||||
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
|
||||
|
||||
class DatabaseConfig
|
||||
|
|
@ -65,33 +66,33 @@ class DatabaseConfig
|
|||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->tenant->data['_tenancy_db_name'] ?? (static::$databaseNameGenerator)($this->tenant);
|
||||
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->tenant->data['_tenancy_db_username'] ?? null;
|
||||
return $this->tenant->getInternal('db_username') ?? null;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->tenant->data['_tenancy_db_password'] ?? null;
|
||||
return $this->tenant->getInternal('db_password') ?? null;
|
||||
}
|
||||
|
||||
public function makeCredentials(): void
|
||||
{
|
||||
$this->tenant->data['_tenancy_db_name'] = $this->getName() ?? (static::$databaseNameGenerator)($this->tenant);
|
||||
$this->tenant->setInternal('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);
|
||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTemplateConnectionName(): string
|
||||
{
|
||||
return $this->tenant->data['_tenancy_db_connection']
|
||||
?? config('tenancy.database.template_connection')
|
||||
return $this->tenant->getInternal('db_connection')
|
||||
?? config('tenancy.template_tenant_connection')
|
||||
?? DatabaseManager::$originalDefaultConnectionName;
|
||||
}
|
||||
|
||||
|
|
@ -120,23 +121,23 @@ class DatabaseConfig
|
|||
*/
|
||||
public function tenantConfig(): array
|
||||
{
|
||||
$dbConfig = array_filter(array_keys($this->tenant->data), function ($key) {
|
||||
return Str::startsWith($key, '_tenancy_db_');
|
||||
$dbConfig = array_filter(array_keys($this->tenant->getAttributes()), function ($key) {
|
||||
return Str::startsWith($key, $this->tenant->internalPrefix() . 'db_');
|
||||
});
|
||||
|
||||
// Remove DB name because we set that separately
|
||||
if (($pos = array_search('_tenancy_db_name', $dbConfig)) !== false) {
|
||||
if (($pos = array_search($this->tenant->internalPrefix() . '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) {
|
||||
if (($pos = array_search($this->tenant->internalPrefix() . '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],
|
||||
Str::substr($key, strlen($this->tenant->internalPrefix() . 'db_')) => $this->tenant->getAttribute($key),
|
||||
]);
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
19
src/Events/Contracts/DomainEvent.php
Normal file
19
src/Events/Contracts/DomainEvent.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events\Contracts;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Database\Models\Domain;
|
||||
|
||||
abstract class DomainEvent
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/** @var Domain */
|
||||
public $domain;
|
||||
|
||||
public function __construct(Domain $domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
}
|
||||
}
|
||||
19
src/Events/Contracts/TenantEvent.php
Normal file
19
src/Events/Contracts/TenantEvent.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events\Contracts;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
abstract class TenantEvent
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/** @var Tenant */
|
||||
public $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
}
|
||||
7
src/Events/DatabaseCreated.php
Normal file
7
src/Events/DatabaseCreated.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DatabaseCreated extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/DatabaseDeleted.php
Normal file
7
src/Events/DatabaseDeleted.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DatabaseDeleted extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
6
src/Events/DatabaseMigrated.php
Normal file
6
src/Events/DatabaseMigrated.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DatabaseMigrated extends Contracts\TenantEvent
|
||||
{}
|
||||
7
src/Events/DatabaseSeeded.php
Normal file
7
src/Events/DatabaseSeeded.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DatabaseSeeded extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/DomainCreated.php
Normal file
7
src/Events/DomainCreated.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DomainCreated extends Contracts\DomainEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/DomainDeleted.php
Normal file
7
src/Events/DomainDeleted.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DomainDeleted extends Contracts\DomainEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/DomainSaved.php
Normal file
7
src/Events/DomainSaved.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DomainSaved extends Contracts\DomainEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/DomainUpdated.php
Normal file
7
src/Events/DomainUpdated.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class DomainUpdated extends Contracts\DomainEvent
|
||||
{
|
||||
}
|
||||
0
src/Events/Listeners/BootstrapTenancy.php
Normal file
0
src/Events/Listeners/BootstrapTenancy.php
Normal file
74
src/Events/Listeners/JobPipeline.php
Normal file
74
src/Events/Listeners/JobPipeline.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events\Listeners;
|
||||
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
|
||||
class JobPipeline implements ShouldQueue
|
||||
{
|
||||
/** @var bool */
|
||||
public static $shouldQueueByDefault = true;
|
||||
|
||||
/** @var callable[]|string[] */
|
||||
public $jobs = [];
|
||||
|
||||
/** @var callable */
|
||||
public $send;
|
||||
|
||||
/** @var bool */
|
||||
public $shouldQueue = true;
|
||||
|
||||
public function __construct($jobs, callable $send = null, bool $shouldQueue = null)
|
||||
{
|
||||
$this->jobs = $jobs;
|
||||
$this->send = $send ?? function ($event) {
|
||||
// If no $send callback is set, we'll just pass the event through the jobs.
|
||||
return $event;
|
||||
};
|
||||
$this->shouldQueue = $shouldQueue ?? static::$shouldQueueByDefault;
|
||||
}
|
||||
|
||||
/** @param callable[]|string[] $jobs */
|
||||
public static function make(array $jobs): self
|
||||
{
|
||||
return new static($jobs);
|
||||
}
|
||||
|
||||
public function queue(bool $shouldQueue): self
|
||||
{
|
||||
$this->shouldQueue = $shouldQueue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function send(callable $send): self
|
||||
{
|
||||
$this->send = $send;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return bool|$this */
|
||||
public function shouldQueue(bool $shouldQueue = null)
|
||||
{
|
||||
if ($shouldQueue !== null) {
|
||||
$this->shouldQueue = $shouldQueue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->shouldQueue;
|
||||
}
|
||||
|
||||
public function handle($event): void
|
||||
{
|
||||
/** @var Pipeline $pipeline */
|
||||
$pipeline = app(Pipeline::class);
|
||||
|
||||
$pipeline
|
||||
->send(($this->send)($event))
|
||||
->through($this->jobs)
|
||||
->thenReturn();
|
||||
}
|
||||
}
|
||||
15
src/Events/Listeners/QueueableListener.php
Normal file
15
src/Events/Listeners/QueueableListener.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events\Listeners;
|
||||
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
abstract class QueueableListener implements ShouldQueue
|
||||
{
|
||||
public static $shouldQueue = false;
|
||||
|
||||
public function shouldQueue()
|
||||
{
|
||||
return static::$shouldQueue;
|
||||
}
|
||||
}
|
||||
15
src/Events/Listeners/RevertToCentral.php
Normal file
15
src/Events/Listeners/RevertToCentral.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
|
||||
class RevertToCentral
|
||||
{
|
||||
public function handle(TenancyEnded $event)
|
||||
{
|
||||
foreach (tenancy()->getBootstrappers() as $bootstrapper) {
|
||||
$bootstrapper->end();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Events/TenancyEnded.php
Normal file
16
src/Events/TenancyEnded.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class TenancyEnded
|
||||
{
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
}
|
||||
16
src/Events/TenancyInitialized.php
Normal file
16
src/Events/TenancyInitialized.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class TenancyInitialized
|
||||
{
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
}
|
||||
7
src/Events/TenantCreated.php
Normal file
7
src/Events/TenantCreated.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantCreated extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/TenantDeleted.php
Normal file
7
src/Events/TenantDeleted.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantDeleted extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/TenantSaved.php
Normal file
7
src/Events/TenantSaved.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantSaved extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
7
src/Events/TenantUpdated.php
Normal file
7
src/Events/TenantUpdated.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantUpdated extends Contracts\TenantEvent
|
||||
{
|
||||
}
|
||||
32
src/Jobs/CreateDatabase.php
Normal file
32
src/Jobs/CreateDatabase.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class CreateDatabase implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if ($this->tenant->getAttribute('_tenancy_create_database') !== false) {
|
||||
$this->tenant->database()->manager()->createDatabase($this->tenant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,31 +10,22 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class QueuedTenantDatabaseDeleter implements ShouldQueue
|
||||
class DeleteDatabase implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var TenantDatabaseManager */
|
||||
protected $databaseManager;
|
||||
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant)
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->databaseManager = $databaseManager;
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->databaseManager->deleteDatabase($this->tenant);
|
||||
$this->tenant->database()->manager()->deleteDatabase($this->tenant);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,22 +9,18 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class QueuedTenantDatabaseCreator implements ShouldQueue
|
||||
class MigrateDatabase implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var TenantDatabaseManager */
|
||||
protected $databaseManager;
|
||||
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant)
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->databaseManager = $databaseManager;
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +31,12 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->databaseManager->createDatabase($this->tenant);
|
||||
$migrationParameters = [
|
||||
// todo ...
|
||||
];
|
||||
|
||||
Artisan::call('tenants:migrate', [
|
||||
'--tenants' => [$this->tenant->id],
|
||||
] + $migrationParameters);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class QueuedTenantDatabaseMigrator implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var string */
|
||||
protected $tenantId;
|
||||
|
||||
/** @var array */
|
||||
protected $migrationParameters = [];
|
||||
|
||||
public function __construct(Tenant $tenant, $migrationParameters = [])
|
||||
{
|
||||
$this->tenantId = $tenant->id;
|
||||
$this->migrationParameters = $migrationParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Artisan::call('tenants:migrate', [
|
||||
'--tenants' => [$this->tenantId],
|
||||
] + $this->migrationParameters);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,18 +10,18 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class QueuedTenantDatabaseSeeder implements ShouldQueue
|
||||
class SeedDatabase implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var string */
|
||||
protected $tenantId;
|
||||
/** @var Tenant */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenantId = $tenant->id;
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -32,7 +32,7 @@ class QueuedTenantDatabaseSeeder implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
Artisan::call('tenants:seed', [
|
||||
'--tenants' => [$this->tenantId],
|
||||
'--tenants' => [$this->tenant->id],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers;
|
||||
|
||||
use Illuminate\Contracts\Redis\Factory as Redis;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
|
||||
use Stancl\Tenancy\Contracts\StorageDriver;
|
||||
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
||||
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class RedisStorageDriver implements StorageDriver, CanDeleteKeys
|
||||
{
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
/** @var Redis */
|
||||
protected $redis;
|
||||
|
||||
/** @var Tenant The default tenant. */
|
||||
protected $tenant;
|
||||
|
||||
public function __construct(Application $app, Redis $redis)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->redis = $redis->connection($app['config']['tenancy.storage_drivers.redis.connection'] ?? 'tenancy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant.
|
||||
*
|
||||
* @return Tenant
|
||||
*/
|
||||
protected function tenant()
|
||||
{
|
||||
return $this->tenant ?? $this->app[Tenant::class];
|
||||
}
|
||||
|
||||
public function withDefaultTenant(Tenant $tenant): self
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
||||
{
|
||||
// Tenant ID
|
||||
if ($this->redis->exists("tenants:{$tenant->id}")) {
|
||||
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
|
||||
}
|
||||
|
||||
// Domains
|
||||
if ($this->redis->exists(...array_map(function ($domain) {
|
||||
return "domains:$domain";
|
||||
}, $tenant->domains))) {
|
||||
throw new DomainsOccupiedByOtherTenantException;
|
||||
}
|
||||
}
|
||||
|
||||
public function findByDomain(string $domain): Tenant
|
||||
{
|
||||
$id = $this->getTenantIdByDomain($domain);
|
||||
if (! $id) {
|
||||
throw new TenantCouldNotBeIdentifiedException($domain);
|
||||
}
|
||||
|
||||
return $this->findById($id);
|
||||
}
|
||||
|
||||
public function findById(string $id): Tenant
|
||||
{
|
||||
$data = $this->redis->hgetall("tenants:$id");
|
||||
|
||||
if (! $data) {
|
||||
throw new TenantDoesNotExistException($id);
|
||||
}
|
||||
|
||||
return $this->makeTenant($data);
|
||||
}
|
||||
|
||||
public function getTenantIdByDomain(string $domain): ?string
|
||||
{
|
||||
return $this->redis->hget("domains:$domain", 'tenant_id') ?: null;
|
||||
}
|
||||
|
||||
public function createTenant(Tenant $tenant): void
|
||||
{
|
||||
$this->redis->transaction(function ($pipe) use ($tenant) {
|
||||
foreach ($tenant->domains as $domain) {
|
||||
$pipe->hmset("domains:$domain", ['tenant_id' => $tenant->id]);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($tenant->data as $key => $value) {
|
||||
$data[$key] = json_encode($value);
|
||||
}
|
||||
|
||||
$pipe->hmset("tenants:{$tenant->id}", array_merge($data, ['_tenancy_domains' => json_encode($tenant->domains)]));
|
||||
});
|
||||
}
|
||||
|
||||
public function updateTenant(Tenant $tenant): void
|
||||
{
|
||||
$id = $tenant->id;
|
||||
|
||||
$old_domains = json_decode($this->redis->hget("tenants:$id", '_tenancy_domains'), true);
|
||||
$deleted_domains = array_diff($old_domains, $tenant->domains);
|
||||
$domains = $tenant->domains;
|
||||
|
||||
$data = [];
|
||||
foreach ($tenant->data as $key => $value) {
|
||||
$data[$key] = json_encode($value);
|
||||
}
|
||||
|
||||
$this->redis->transaction(function ($pipe) use ($id, $data, $deleted_domains, $domains) {
|
||||
foreach ($deleted_domains as $deleted_domain) {
|
||||
$pipe->del("domains:$deleted_domain");
|
||||
}
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$pipe->hset("domains:$domain", 'tenant_id', $id);
|
||||
}
|
||||
|
||||
$pipe->hmset("tenants:$id", array_merge($data, ['_tenancy_domains' => json_encode($domains)]));
|
||||
});
|
||||
}
|
||||
|
||||
public function deleteTenant(Tenant $tenant): void
|
||||
{
|
||||
$this->redis->transaction(function ($pipe) use ($tenant) {
|
||||
foreach ($tenant->domains as $domain) {
|
||||
$pipe->del("domains:$domain");
|
||||
}
|
||||
|
||||
$pipe->del("tenants:{$tenant->id}");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all tenants.
|
||||
*
|
||||
* @param string[] $ids
|
||||
* @return Tenant[]
|
||||
*/
|
||||
public function all(array $ids = []): array
|
||||
{
|
||||
$hashes = array_map(function ($hash) {
|
||||
return "tenants:{$hash}";
|
||||
}, $ids);
|
||||
|
||||
if (! $hashes) {
|
||||
// Prefix is applied to all functions except scan().
|
||||
// This code applies the correct prefix manually.
|
||||
$redis_prefix = $this->redis->getOption($this->redis->client()::OPT_PREFIX);
|
||||
|
||||
$all_keys = $this->redis->keys('tenants:*');
|
||||
|
||||
$hashes = array_map(function ($key) use ($redis_prefix) {
|
||||
// Left strip $redis_prefix from $key
|
||||
return substr($key, strlen($redis_prefix));
|
||||
}, $all_keys);
|
||||
}
|
||||
|
||||
return array_map(function ($tenant) {
|
||||
return $this->makeTenant($this->redis->hgetall($tenant));
|
||||
}, $hashes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a Tenant instance from low-level array data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return Tenant
|
||||
*/
|
||||
protected function makeTenant(array $data): Tenant
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = json_decode($value, true);
|
||||
}
|
||||
|
||||
$domains = $data['_tenancy_domains'];
|
||||
unset($data['_tenancy_domains']);
|
||||
|
||||
return Tenant::fromStorage($data)->withDomains($domains);
|
||||
}
|
||||
|
||||
public function get(string $key, Tenant $tenant = null)
|
||||
{
|
||||
$tenant = $tenant ?? $this->tenant();
|
||||
|
||||
$json_data = $this->redis->hget("tenants:{$tenant->id}", $key);
|
||||
if ($json_data === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return json_decode($json_data, true);
|
||||
}
|
||||
|
||||
public function getMany(array $keys, Tenant $tenant = null): array
|
||||
{
|
||||
$tenant = $tenant ?? $this->tenant();
|
||||
|
||||
$result = [];
|
||||
$values = $this->redis->hmget("tenants:{$tenant->id}", $keys);
|
||||
foreach ($keys as $i => $key) {
|
||||
$result[$key] = json_decode($values[$i], true);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function put(string $key, $value, Tenant $tenant = null): void
|
||||
{
|
||||
$tenant = $tenant ?? $this->tenant();
|
||||
$this->redis->hset("tenants:{$tenant->id}", $key, json_encode($value));
|
||||
}
|
||||
|
||||
public function putMany(array $kvPairs, Tenant $tenant = null): void
|
||||
{
|
||||
$tenant = $tenant ?? $this->tenant();
|
||||
|
||||
foreach ($kvPairs as $key => $value) {
|
||||
$kvPairs[$key] = json_encode($value);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
39
src/Tenancy.php
Normal file
39
src/Tenancy.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class Tenancy
|
||||
{
|
||||
/** @var Tenant|null */
|
||||
public $tenant;
|
||||
|
||||
/** @var callable|null */
|
||||
public static $getBootstrappers = null;
|
||||
|
||||
public function initialize(Tenant $tenant): void
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
|
||||
event(new Events\TenancyInitialized($tenant));
|
||||
}
|
||||
|
||||
public function end(): void
|
||||
{
|
||||
event(new Events\TenancyEnded($this->tenant));
|
||||
|
||||
$this->tenant = null;
|
||||
}
|
||||
|
||||
/** @return TenancyBootstrapper[] */
|
||||
public function getBootstrappers(): array
|
||||
{
|
||||
$resolve = static::$getBootstrappers ?? function (Tenant $tenant) {
|
||||
return config('tenancy.bootstrappers');
|
||||
};
|
||||
|
||||
return $resolve($this->tenant);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ use Illuminate\Cache\CacheManager;
|
|||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Database\TenantObserver;
|
||||
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
|
||||
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
|
||||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
|
|
@ -22,13 +25,13 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
|
||||
|
||||
$this->app->bind(Contracts\StorageDriver::class, function ($app) {
|
||||
return $app->make($app['config']['tenancy.storage_drivers'][$app['config']['tenancy.storage_driver']]['driver']);
|
||||
return $app->make(DatabaseStorageDriver::class);
|
||||
});
|
||||
$this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.unique_id_generator']);
|
||||
$this->app->singleton(DatabaseManager::class);
|
||||
$this->app->singleton(TenantManager::class);
|
||||
$this->app->singleton(Tenancy::class);
|
||||
$this->app->bind(Tenant::class, function ($app) {
|
||||
return $app[TenantManager::class]->getTenant();
|
||||
return $app[Tenancy::class]->tenant;
|
||||
});
|
||||
|
||||
foreach ($this->app['config']['tenancy.bootstrappers'] as $bootstrapper) {
|
||||
|
|
|
|||
453
src/Tenant.php
453
src/Tenant.php
|
|
@ -1,453 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use ArrayAccess;
|
||||
use Closure;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Traits\ForwardsCalls;
|
||||
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
|
||||
use Stancl\Tenancy\Contracts\StorageDriver;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
use Stancl\Tenancy\Exceptions\NotImplementedException;
|
||||
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,
|
||||
ForwardsCalls;
|
||||
|
||||
/**
|
||||
* Tenant data. A "cache" of tenant storage.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* List of domains that belong to the tenant.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $domains = [];
|
||||
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
|
||||
/** @var StorageDriver|CanDeleteKeys */
|
||||
protected $storage;
|
||||
|
||||
/** @var TenantManager */
|
||||
protected $manager;
|
||||
|
||||
/** @var UniqueIdentifierGenerator */
|
||||
protected $idGenerator;
|
||||
|
||||
/**
|
||||
* Does this tenant exist in the storage.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $persisted = false;
|
||||
|
||||
/**
|
||||
* Use new() if you don't want to swap dependencies.
|
||||
*
|
||||
* @param StorageDriver $storage
|
||||
* @param TenantManager $tenantManager
|
||||
* @param UniqueIdentifierGenerator $idGenerator
|
||||
*/
|
||||
public function __construct(Repository $config, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->storage = $storage->withDefaultTenant($this);
|
||||
$this->manager = $tenantManager;
|
||||
$this->idGenerator = $idGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param Application $app
|
||||
* @return self
|
||||
*/
|
||||
public static function new(Application $app = null): self
|
||||
{
|
||||
$app = $app ?? app();
|
||||
|
||||
return new static(
|
||||
$app[Repository::class],
|
||||
$app[StorageDriver::class],
|
||||
$app[TenantManager::class],
|
||||
$app[UniqueIdentifierGenerator::class]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by storage drivers to create persisted instances of Tenant.
|
||||
*
|
||||
* @param array $data
|
||||
* @return self
|
||||
*/
|
||||
public static function fromStorage(array $data): self
|
||||
{
|
||||
return static::new()->withData($data)->persisted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tenant in a single call.
|
||||
*
|
||||
* @param string|string[] $domains
|
||||
* @param array $data
|
||||
* @return self
|
||||
*/
|
||||
public static function create($domains, array $data = []): self
|
||||
{
|
||||
return static::new()->withDomains((array) $domains)->withData($data)->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT CALL THIS METHOD FROM USERLAND UNLESS YOU KNOW WHAT YOU ARE DOING.
|
||||
* Set $persisted.
|
||||
*
|
||||
* @param bool $persisted
|
||||
* @return self
|
||||
*/
|
||||
public function persisted(bool $persisted): self
|
||||
{
|
||||
$this->persisted = $persisted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this model exist in the tenant storage.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPersisted(): bool
|
||||
{
|
||||
return $this->persisted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign domains to the tenant.
|
||||
*
|
||||
* @param string|string[] $domains
|
||||
* @return self
|
||||
*/
|
||||
public function addDomains($domains): self
|
||||
{
|
||||
$domains = (array) $domains;
|
||||
$this->domains = array_merge($this->domains, $domains);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign domains from the tenant.
|
||||
*
|
||||
* @param string|string[] $domains
|
||||
* @return self
|
||||
*/
|
||||
public function removeDomains($domains): self
|
||||
{
|
||||
$domains = (array) $domains;
|
||||
$this->domains = array_diff($this->domains, $domains);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign all domains from the tenant.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function clearDomains(): self
|
||||
{
|
||||
$this->domains = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set (overwrite) the tenant's domains.
|
||||
*
|
||||
* @param string|string[] $domains
|
||||
* @return self
|
||||
*/
|
||||
public function withDomains($domains): self
|
||||
{
|
||||
$domains = (array) $domains;
|
||||
|
||||
$this->domains = $domains;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set (overwrite) tenant data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return self
|
||||
*/
|
||||
public function withData(array $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function generateId()
|
||||
{
|
||||
$this->id = $this->idGenerator->generate($this->domains, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the tenant's state to storage.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function save(): self
|
||||
{
|
||||
if (! isset($this->data['id'])) {
|
||||
$this->generateId();
|
||||
}
|
||||
|
||||
if ($this->persisted) {
|
||||
$this->manager->updateTenant($this);
|
||||
} else {
|
||||
$this->database()->makeCredentials();
|
||||
$this->manager->createTenant($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tenant from storage.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function delete(): self
|
||||
{
|
||||
if ($this->persisted) {
|
||||
$this->manager->deleteTenant($this);
|
||||
$this->persisted = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign all domains from the tenant and write to storage.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function softDelete(): self
|
||||
{
|
||||
$this->manager->event('tenant.softDeleting', $this);
|
||||
|
||||
$this->put([
|
||||
'_tenancy_original_domains' => $this->domains,
|
||||
]);
|
||||
$this->clearDomains();
|
||||
$this->save();
|
||||
|
||||
$this->manager->event('tenant.softDeleted', $this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database config.
|
||||
*
|
||||
* @return DatabaseConfig
|
||||
*/
|
||||
public function database(): DatabaseConfig
|
||||
{
|
||||
return new DatabaseConfig($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from tenant storage.
|
||||
*
|
||||
* @param string|string[] $keys
|
||||
* @return void
|
||||
*/
|
||||
public function get($keys)
|
||||
{
|
||||
if (is_array($keys)) {
|
||||
if ((array_intersect(array_keys($this->data), $keys) === $keys) ||
|
||||
! $this->persisted) { // if all keys are present in cache
|
||||
|
||||
return array_reduce($keys, function ($pairs, $key) {
|
||||
$pairs[$key] = $this->data[$key] ?? null;
|
||||
|
||||
return $pairs;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return $this->storage->getMany($keys);
|
||||
}
|
||||
|
||||
// single key
|
||||
$key = $keys;
|
||||
|
||||
if (! isset($this->data[$key]) && $this->persisted) {
|
||||
$this->data[$key] = $this->storage->get($key);
|
||||
}
|
||||
|
||||
return $this->data[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value and write to storage.
|
||||
*
|
||||
* @param string|array<string, mixed> $key
|
||||
* @param mixed $value
|
||||
* @return self
|
||||
*/
|
||||
public function put($key, $value = null): self
|
||||
{
|
||||
$this->manager->event('tenant.updating', $this);
|
||||
|
||||
if ($key === 'id') {
|
||||
throw new TenantStorageException("Tenant ids can't be changed.");
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
if ($this->persisted) {
|
||||
$this->storage->putMany($key);
|
||||
}
|
||||
|
||||
foreach ($key as $k => $v) { // Add to cache
|
||||
$this->data[$k] = $v;
|
||||
}
|
||||
} else {
|
||||
if ($this->persisted) {
|
||||
$this->storage->put($key, $value);
|
||||
}
|
||||
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
$this->manager->event('tenant.updated', $this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @alias put */
|
||||
public function set($key, $value = null): self
|
||||
{
|
||||
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
|
||||
{
|
||||
$this->manager->event('tenant.updating', $this);
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->manager->event('tenant.updated', $this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the data array without saving into storage.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return self
|
||||
*/
|
||||
public function with(string $key, $value): self
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a closure inside the tenant's environment.
|
||||
*
|
||||
* @param Closure $closure
|
||||
* @return mixed
|
||||
*/
|
||||
public function run(Closure $closure)
|
||||
{
|
||||
$originalTenant = $this->manager->getTenant();
|
||||
|
||||
$this->manager->initializeTenancy($this);
|
||||
$result = $closure($this);
|
||||
$this->manager->endTenancy($this);
|
||||
|
||||
if ($originalTenant) {
|
||||
$this->manager->initializeTenancy($originalTenant);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->get($key);
|
||||
}
|
||||
|
||||
public function __set($key, $value)
|
||||
{
|
||||
if ($key === 'id' && isset($this->data['id'])) {
|
||||
throw new TenantStorageException("Tenant ids can't be changed.");
|
||||
}
|
||||
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (Str::startsWith($method, 'with')) {
|
||||
return $this->with(Str::snake(substr($method, 4)), $parameters[0]);
|
||||
}
|
||||
|
||||
static::throwBadMethodCallException($method);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,471 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Foundation\Application;
|
||||
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\Exceptions\NotImplementedException;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
|
||||
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
|
||||
|
||||
/**
|
||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
||||
*/
|
||||
class TenantManager
|
||||
{
|
||||
use ForwardsCalls;
|
||||
|
||||
/** @var Tenant The current tenant. */
|
||||
protected $tenant;
|
||||
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
/** @var ConsoleKernel */
|
||||
protected $artisan;
|
||||
|
||||
/** @var Contracts\StorageDriver */
|
||||
public $storage;
|
||||
|
||||
/** @var DatabaseManager */
|
||||
public $database;
|
||||
|
||||
/** @var callable[][] */
|
||||
protected $eventListeners = [];
|
||||
|
||||
/** @var bool Has tenancy been initialized. */
|
||||
public $initialized = false;
|
||||
|
||||
public function __construct(Application $app, ConsoleKernel $artisan, Contracts\StorageDriver $storage, DatabaseManager $database)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->storage = $storage;
|
||||
$this->artisan = $artisan;
|
||||
$this->database = $database->withTenantManager($this);
|
||||
|
||||
$this->bootstrapFeatures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a new tenant to storage.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return self
|
||||
*/
|
||||
public function createTenant(Tenant $tenant): self
|
||||
{
|
||||
$this->event('tenant.creating', $tenant);
|
||||
|
||||
$this->ensureTenantCanBeCreated($tenant);
|
||||
|
||||
$this->storage->createTenant($tenant);
|
||||
|
||||
$tenant->persisted = true;
|
||||
|
||||
/** @var \Illuminate\Contracts\Queue\ShouldQueue[]|callable[] $afterCreating */
|
||||
$afterCreating = [];
|
||||
|
||||
if ($this->shouldMigrateAfterCreation()) {
|
||||
$afterCreating[] = $this->databaseCreationQueued()
|
||||
? new QueuedTenantDatabaseMigrator($tenant, $this->getMigrationParameters())
|
||||
: function () use ($tenant) {
|
||||
$this->artisan->call('tenants:migrate', [
|
||||
'--tenants' => [$tenant['id']],
|
||||
] + $this->getMigrationParameters());
|
||||
};
|
||||
}
|
||||
|
||||
if ($this->shouldSeedAfterMigration()) {
|
||||
$afterCreating[] = $this->databaseCreationQueued()
|
||||
? new QueuedTenantDatabaseSeeder($tenant)
|
||||
: function () use ($tenant) {
|
||||
$this->artisan->call('tenants:seed', [
|
||||
'--tenants' => [$tenant['id']],
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
if ($this->shouldCreateDatabase($tenant)) {
|
||||
$this->database->createDatabase($tenant, $afterCreating);
|
||||
}
|
||||
|
||||
$this->event('tenant.created', $tenant);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tenant from storage.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return self
|
||||
*/
|
||||
public function deleteTenant(Tenant $tenant): self
|
||||
{
|
||||
$this->event('tenant.deleting', $tenant);
|
||||
|
||||
$this->storage->deleteTenant($tenant);
|
||||
|
||||
if ($this->shouldDeleteDatabase()) {
|
||||
$this->database->deleteDatabase($tenant);
|
||||
}
|
||||
|
||||
$this->event('tenant.deleted', $tenant);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for Stancl\Tenancy\Tenant::create.
|
||||
*
|
||||
* @param string|string[] $domains
|
||||
* @param array $data
|
||||
* @return Tenant
|
||||
*/
|
||||
public static function create($domains, array $data = []): Tenant
|
||||
{
|
||||
return Tenant::create($domains, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a tenant can be created.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return void
|
||||
* @throws TenantCannotBeCreatedException
|
||||
*/
|
||||
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
||||
{
|
||||
if ($this->shouldCreateDatabase($tenant)) {
|
||||
$this->database->ensureTenantCanBeCreated($tenant);
|
||||
}
|
||||
|
||||
$this->storage->ensureTenantCanBeCreated($tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tenant in storage.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return self
|
||||
*/
|
||||
public function updateTenant(Tenant $tenant): self
|
||||
{
|
||||
$this->event('tenant.updating', $tenant);
|
||||
|
||||
$this->storage->updateTenant($tenant);
|
||||
|
||||
$this->event('tenant.updated', $tenant);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find tenant by domain & initialize tenancy.
|
||||
*
|
||||
* @param string|null $domain
|
||||
* @return self
|
||||
*/
|
||||
public function init(string $domain = null): self
|
||||
{
|
||||
$domain = $domain ?? request()->getHost();
|
||||
$this->initializeTenancy($this->findByDomain($domain));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find tenant by ID & initialize tenancy.
|
||||
*
|
||||
* @param string $id
|
||||
* @return self
|
||||
*/
|
||||
public function initById(string $id): self
|
||||
{
|
||||
$this->initializeTenancy($this->find($id));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tenant using an id.
|
||||
*
|
||||
* @param string $id
|
||||
* @return Tenant
|
||||
* @throws TenantCouldNotBeIdentifiedException
|
||||
*/
|
||||
public function find(string $id): Tenant
|
||||
{
|
||||
return $this->storage->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tenant using a domain name.
|
||||
*
|
||||
* @param string $id
|
||||
* @return Tenant
|
||||
* @throws TenantCouldNotBeIdentifiedException
|
||||
*/
|
||||
public function findByDomain(string $domain): Tenant
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param Tenant[]|string[] $only
|
||||
* @return Collection<Tenant>
|
||||
*/
|
||||
public function all($only = []): Collection
|
||||
{
|
||||
$only = array_map(function ($item) {
|
||||
return $item instanceof Tenant ? $item->id : $item;
|
||||
}, (array) $only);
|
||||
|
||||
return collect($this->storage->all($only));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tenancy.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return self
|
||||
*/
|
||||
public function initializeTenancy(Tenant $tenant): self
|
||||
{
|
||||
if ($this->initialized) {
|
||||
$this->endTenancy();
|
||||
}
|
||||
|
||||
$this->setTenant($tenant);
|
||||
$this->bootstrapTenancy($tenant);
|
||||
$this->initialized = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @alias initializeTenancy */
|
||||
public function initialize(Tenant $tenant): self
|
||||
{
|
||||
return $this->initializeTenancy($tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute TenancyBootstrappers.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @return self
|
||||
*/
|
||||
public function bootstrapTenancy(Tenant $tenant): self
|
||||
{
|
||||
$prevented = $this->event('bootstrapping', $tenant);
|
||||
|
||||
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
||||
$this->app[$bootstrapper]->start($tenant);
|
||||
}
|
||||
|
||||
$this->event('bootstrapped', $tenant);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function endTenancy(): self
|
||||
{
|
||||
if (! $this->initialized) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$prevented = $this->event('ending', $this->tenant);
|
||||
|
||||
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
|
||||
$this->app[$bootstrapper]->end();
|
||||
}
|
||||
|
||||
$this->initialized = false;
|
||||
$this->tenant = null;
|
||||
|
||||
$this->event('ended');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @alias endTenancy */
|
||||
public function end(): self
|
||||
{
|
||||
return $this->endTenancy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant.
|
||||
*
|
||||
* @param string $key
|
||||
* @return Tenant|null|mixed
|
||||
*/
|
||||
public function getTenant(string $key = null)
|
||||
{
|
||||
if (! $this->tenant) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_null($key)) {
|
||||
return $this->tenant[$key];
|
||||
}
|
||||
|
||||
return $this->tenant;
|
||||
}
|
||||
|
||||
protected function setTenant(Tenant $tenant): self
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bootstrapFeatures(): self
|
||||
{
|
||||
foreach ($this->app['config']['tenancy.features'] as $feature) {
|
||||
$this->app[$feature]->bootstrap($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of TenancyBootstrappers.
|
||||
*
|
||||
* @param string[] $except
|
||||
* @return Contracts\TenancyBootstrapper[]
|
||||
*/
|
||||
public function tenancyBootstrappers($except = []): array
|
||||
{
|
||||
return array_diff_key($this->app['config']['tenancy.bootstrappers'], array_flip($except));
|
||||
}
|
||||
|
||||
public function shouldCreateDatabase(Tenant $tenant): bool
|
||||
{
|
||||
if (array_key_exists('_tenancy_create_database', $tenant->data)) {
|
||||
return $tenant->data['_tenancy_create_database'];
|
||||
}
|
||||
|
||||
return $this->app['config']['tenancy.create_database'] ?? true;
|
||||
}
|
||||
|
||||
public function shouldMigrateAfterCreation(): bool
|
||||
{
|
||||
return $this->app['config']['tenancy.migrate_after_creation'] ?? false;
|
||||
}
|
||||
|
||||
public function shouldSeedAfterMigration(): bool
|
||||
{
|
||||
return $this->shouldMigrateAfterCreation() && $this->app['config']['tenancy.seed_after_migration'] ?? false;
|
||||
}
|
||||
|
||||
public function databaseCreationQueued(): bool
|
||||
{
|
||||
return $this->app['config']['tenancy.queue_database_creation'] ?? false;
|
||||
}
|
||||
|
||||
public function shouldDeleteDatabase(): bool
|
||||
{
|
||||
return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false;
|
||||
}
|
||||
|
||||
public function getSeederParameters()
|
||||
{
|
||||
return $this->app['config']['tenancy.seeder_parameters'] ?? [];
|
||||
}
|
||||
|
||||
public function getMigrationParameters()
|
||||
{
|
||||
return $this->app['config']['tenancy.migration_parameters'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener.
|
||||
*
|
||||
* @param string $name
|
||||
* @param callable $listener
|
||||
* @return self
|
||||
*/
|
||||
public function eventListener(string $name, callable $listener): self
|
||||
{
|
||||
$this->eventListeners[$name] = $this->eventListeners[$name] ?? [];
|
||||
$this->eventListeners[$name][] = $listener;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event hook.
|
||||
* @alias eventListener
|
||||
*
|
||||
* @param string $name
|
||||
* @param callable $listener
|
||||
* @return self
|
||||
*/
|
||||
public function hook(string $name, callable $listener): self
|
||||
{
|
||||
return $this->eventListener($name, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an event and execute its listeners.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed ...$args
|
||||
* @return string[]
|
||||
*/
|
||||
public function event(string $name, ...$args): array
|
||||
{
|
||||
return array_reduce($this->eventListeners[$name] ?? [], function ($results, $listener) use ($args) {
|
||||
return array_merge($results, $listener($this, ...$args) ?? []);
|
||||
}, []);
|
||||
}
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (Str::startsWith($method, 'findBy')) {
|
||||
return $this->findBy(Str::snake(substr($method, 6)), $parameters[0]);
|
||||
}
|
||||
|
||||
static::throwBadMethodCallException($method);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,11 @@ namespace Stancl\Tenancy\UniqueIDGenerators;
|
|||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
|
||||
class UUIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static function generate(array $domains, array $data = []): string
|
||||
public static function generate(Tenant $tenant): string
|
||||
{
|
||||
return Uuid::uuid4()->toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,14 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\TenantManager;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
if (! function_exists('tenancy')) {
|
||||
/** @return TenantManager|mixed */
|
||||
function tenancy($key = null)
|
||||
/** @return Tenancy */
|
||||
function tenancy()
|
||||
{
|
||||
if ($key) {
|
||||
return app(TenantManager::class)->getTenant($key) ?? null;
|
||||
}
|
||||
|
||||
return app(TenantManager::class);
|
||||
return app(Tenancy::class);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
6
test
6
test
|
|
@ -1,7 +1,3 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
printf "Variant 1 (DB)\n\n"
|
||||
docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/1.cov "$@"
|
||||
printf "Variant 2 (Redis)\n\n"
|
||||
docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/2.cov "$@"
|
||||
docker-compose exec -T test vendor/bin/phpunit "$@"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
{
|
||||
parent::setUp();
|
||||
|
||||
Redis::connection('tenancy')->flushdb();
|
||||
Redis::connection('cache')->flushdb();
|
||||
// Redis::connection('cache')->flushdb();
|
||||
|
||||
file_put_contents(database_path('central.sqlite'), '');
|
||||
$this->artisan('migrate:fresh', [
|
||||
|
|
@ -102,7 +101,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'--realpath' => true,
|
||||
'--force' => true,
|
||||
],
|
||||
'tenancy.storage_drivers.db.connection' => 'central',
|
||||
'tenancy.storage.connection' => 'central',
|
||||
'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class,
|
||||
'queue.connections.central' => [
|
||||
'driver' => 'sync',
|
||||
|
|
@ -112,8 +111,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
]);
|
||||
|
||||
$app->singleton(\Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class);
|
||||
|
||||
$app['config']->set(['tenancy.storage_driver' => env('TENANCY_TEST_STORAGE_DRIVER', 'redis')]);
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
|
|
|
|||
0
tests/v3/DatabasePreparationTest.php
Normal file
0
tests/v3/DatabasePreparationTest.php
Normal file
0
tests/v3/DomainTest.php
Normal file
0
tests/v3/DomainTest.php
Normal file
0
tests/v3/EventListenerTest.php
Normal file
0
tests/v3/EventListenerTest.php
Normal file
0
tests/v3/HostnameIdentificationTest.php
Normal file
0
tests/v3/HostnameIdentificationTest.php
Normal file
0
tests/v3/JobPipelineTest.php
Normal file
0
tests/v3/JobPipelineTest.php
Normal file
109
tests/v3/TenantModelTest.php
Normal file
109
tests/v3/TenantModelTest.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\v3;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
use Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator;
|
||||
|
||||
class TenantModelTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function created_event_is_dispatched()
|
||||
{
|
||||
Event::fake([TenantCreated::class]);
|
||||
|
||||
Event::assertNotDispatched(TenantCreated::class);
|
||||
|
||||
Tenant::create();
|
||||
|
||||
Event::assertDispatched(TenantCreated::class);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function current_tenant_can_be_resolved_from_service_container_using_typehint()
|
||||
{
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$this->assertSame($tenant->id, app(Tenant::class)->id);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
$this->assertSame(null, app(Tenant::class));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function keys_which_dont_have_their_own_column_go_into_data_json_column()
|
||||
{
|
||||
$tenant = Tenant::create([
|
||||
'foo' => 'bar',
|
||||
]);
|
||||
|
||||
// Test that model works correctly
|
||||
$this->assertSame('bar', $tenant->foo);
|
||||
$this->assertSame(null, $tenant->data);
|
||||
|
||||
// Low level test to test database structure
|
||||
$this->assertSame(json_encode(['foo' => 'bar']), DB::table('tenants')->where('id', $tenant->id)->first()->data);
|
||||
$this->assertSame(null, DB::table('tenants')->where('id', $tenant->id)->first()->foo ?? null);
|
||||
|
||||
// Model has the correct structure when retrieved
|
||||
$tenant = Tenant::first();
|
||||
$this->assertSame('bar', $tenant->foo);
|
||||
$this->assertSame(null, $tenant->data);
|
||||
|
||||
// Model can be updated
|
||||
$tenant->update([
|
||||
'foo' => 'baz',
|
||||
'abc' => 'xyz',
|
||||
]);
|
||||
|
||||
$this->assertSame('baz', $tenant->foo);
|
||||
$this->assertSame('xyz', $tenant->abc);
|
||||
$this->assertSame(null, $tenant->data);
|
||||
|
||||
// Model can be retrieved after update & is structure correctly
|
||||
$tenant = Tenant::first();
|
||||
|
||||
$this->assertSame('baz', $tenant->foo);
|
||||
$this->assertSame('xyz', $tenant->abc);
|
||||
$this->assertSame(null, $tenant->data);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function id_is_generated_when_no_id_is_supplied()
|
||||
{
|
||||
config(['tenancy.id_generator' => UUIDGenerator::class]);
|
||||
|
||||
$this->mock(UUIDGenerator::class, function ($mock) {
|
||||
return $mock->shouldReceive('generate')->once();
|
||||
});
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
$this->assertNotNull($tenant->id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function autoincrement_ids_are_supported()
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->bigIncrements('id')->change();
|
||||
});
|
||||
|
||||
config(['tenancy.id_generator' => null]);
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
$this->assertSame(1, $tenant1->id);
|
||||
$this->assertSame(2, $tenant2->id);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue