1
0
Fork 0
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:
Samuel Štancl 2020-05-08 04:37:43 +02:00
parent c2c90ff755
commit bd9aad229b
56 changed files with 803 additions and 1366 deletions

View 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);
}
}
}
}

View file

@ -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,
],
];

View file

@ -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('{}');
});
}

View file

@ -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": {

View file

@ -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:

View file

@ -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/

View file

@ -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>

View file

@ -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));
});
}
}

View file

@ -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));
});
}
}

View file

@ -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;
}

View file

@ -0,0 +1,11 @@
<?php
namespace Stancl\Tenancy\Database\Models\Concerns;
trait CentralConnection
{
public function getConnectionName()
{
return config('tenancy.central_connection');
}
}

View 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);
}
});
}
}

View 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'));
}
}

View 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,
];
}

View 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,
];
}

View file

@ -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),
]);
}, []);
}

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DatabaseCreated extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DatabaseDeleted extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,6 @@
<?php
namespace Stancl\Tenancy\Events;
class DatabaseMigrated extends Contracts\TenantEvent
{}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DatabaseSeeded extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DomainCreated extends Contracts\DomainEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DomainDeleted extends Contracts\DomainEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DomainSaved extends Contracts\DomainEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class DomainUpdated extends Contracts\DomainEvent
{
}

View 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();
}
}

View 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;
}
}

View 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();
}
}
}

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class TenantCreated extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class TenantDeleted extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class TenantSaved extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace Stancl\Tenancy\Events;
class TenantUpdated extends Contracts\TenantEvent
{
}

View 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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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],
]);
}
}

View file

@ -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
View 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);
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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
View file

@ -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 "$@"

View file

@ -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)

View file

0
tests/v3/DomainTest.php Normal file
View file

View file

View file

View file

View 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);
}
}