diff --git a/assets/config.php b/assets/config.php index 6cd11eca..4b0aa810 100644 --- a/assets/config.php +++ b/assets/config.php @@ -13,8 +13,8 @@ return [ ], 'connection' => null, // Your central database connection. Set to null to use the default connection. 'table_names' => [ - 'TenantModel' => 'tenants', - 'DomainModel' => 'domains', + 'tenants' => 'tenants', + 'domains' => 'domains', ], ], 'redis' => [ diff --git a/src/StorageDrivers/Database/DatabaseStorageDriver.php b/src/StorageDrivers/Database/DatabaseStorageDriver.php index 6baf0f8b..fe3bd3ce 100644 --- a/src/StorageDrivers/Database/DatabaseStorageDriver.php +++ b/src/StorageDrivers/Database/DatabaseStorageDriver.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\StorageDrivers\Database; +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Database\Connection; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanDeleteKeys; @@ -13,8 +15,6 @@ use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantDoesNotExistException; use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException; -use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains; -use Stancl\Tenancy\StorageDrivers\Database\TenantModel as Tenants; use Stancl\Tenancy\Tenant; class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys @@ -22,24 +22,32 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys /** @var Application */ protected $app; - /** @var \Illuminate\Database\Connection */ + /** @var Connection */ protected $centralDatabase; + /** @var TenantRepository */ + protected $tenants; + + /** @var DomainRepository */ + protected $domains; + /** @var Tenant The default tenant. */ protected $tenant; - public function __construct(Application $app) + public function __construct(Application $app, ConfigRepository $config) { $this->app = $app; $this->centralDatabase = $this->getCentralConnection(); + $this->tenants = new TenantRepository($this->centralDatabase, $config); + $this->domains = new DomainRepository($this->centralDatabase, $config); } /** * Get the central database connection. * - * @return \Illuminate\Database\Connection + * @return Connection */ - public static function getCentralConnection(): \Illuminate\Database\Connection + public static function getCentralConnection(): Connection { return DB::connection(static::getCentralConnectionName()); } @@ -51,7 +59,7 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys public function findByDomain(string $domain): Tenant { - $id = $this->getTenantIdByDomain($domain); + $id = $this->domains->getTenantIdByDomain($domain); if (! $id) { throw new TenantCouldNotBeIdentifiedException($domain); } @@ -61,14 +69,14 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys public function findById(string $id): Tenant { - $tenant = Tenants::find($id); + $tenant = $this->tenants->find($id); if (! $tenant) { throw new TenantDoesNotExistException($id); } - return Tenant::fromStorage($tenant->decoded()) - ->withDomains($this->getTenantDomains($id)); + return Tenant::fromStorage($this->tenants->decodeData($tenant)) + ->withDomains($this->domains->getTenantDomains($id)); } /** @@ -81,31 +89,24 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys */ public function findBy(string $key, $value): Tenant { - // The key has to be a custom column. It's recommended to set up an index - $tenant = Tenants::where($key, $value)->first(); + // The key has to be a custom column. It's recommended to set up an index // TODO can we query JSON? + $tenant = $this->tenants->where($key, $value)->first()->toArray(); if (! $tenant) { throw new TenantDoesNotExistException($value, $key); } return Tenant::fromStorage($tenant->decoded()) - ->withDomains($this->getTenantDomains($tenant->id)); - } - - protected function getTenantDomains($id) - { - return Domains::where('tenant_id', $id)->get()->map(function ($model) { - return $model->domain; - })->toArray(); + ->withDomains($this->domains->getTenantDomains($tenant->id)); } public function ensureTenantCanBeCreated(Tenant $tenant): void { - if (Tenants::find($tenant->id)) { + if ($this->tenants->exists($tenant)) { throw new TenantWithThisIdAlreadyExistsException($tenant->id); } - if (Domains::whereIn('domain', $tenant->domains)->exists()) { + if ($this->domains->occupied($tenant->domains)) { throw new DomainsOccupiedByOtherTenantException; } } @@ -117,53 +118,32 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys return $this; } - public function getTenantIdByDomain(string $domain): ?string - { - return Domains::where('domain', $domain)->first()->tenant_id ?? null; - } - public function createTenant(Tenant $tenant): void { $this->centralDatabase->transaction(function () use ($tenant) { - Tenants::create(array_merge(Tenants::encodeData($tenant->data), [ - 'id' => $tenant->id, - ]))->toArray(); + $this->tenants->insert(array_merge( + $this->tenants->encodeData($tenant->data), + [] // ['id' => $tenant->id] // todo remove this line if things work + )); - $domainData = []; - foreach ($tenant->domains as $domain) { - $domainData[] = ['domain' => $domain, 'tenant_id' => $tenant->id]; - } - - Domains::insert($domainData); + $this->domains->insertTenantDomains($tenant); }); } public function updateTenant(Tenant $tenant): void { $this->centralDatabase->transaction(function () use ($tenant) { - Tenants::find($tenant->id)->putMany($tenant->data); + $this->tenants->updateTenant($tenant); - $original_domains = Domains::where('tenant_id', $tenant->id)->get()->map(function ($model) { - return $model->domain; - })->toArray(); - $deleted_domains = array_diff($original_domains, $tenant->domains); - - Domains::whereIn('domain', $deleted_domains)->delete(); - - foreach ($tenant->domains as $domain) { - Domains::firstOrCreate([ - 'tenant_id' => $tenant->id, - 'domain' => $domain, - ]); - } + $this->domains->updateTenantDomains($tenant); }); } public function deleteTenant(Tenant $tenant): void { $this->centralDatabase->transaction(function () use ($tenant) { - Tenants::find($tenant->id)->delete(); - Domains::where('tenant_id', $tenant->id)->delete(); + $this->tenants->find($tenant)->delete(); + $this->domains->where('tenant_id', $tenant->id)->delete(); }); } @@ -175,8 +155,8 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys */ public function all(array $ids = []): array { - return Tenants::getAllTenants($ids)->map(function ($data) { - return Tenant::fromStorage($data)->withDomains($this->getTenantDomains($data['id'])); + return $this->tenants->all($ids)->map(function ($data) { + return Tenant::fromStorage($data)->withDomains($this->domains->getTenantDomains($data['id'])); })->toArray(); } @@ -192,53 +172,26 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys public function get(string $key, Tenant $tenant = null) { - $tenant = $tenant ?? $this->currentTenant(); - - return Tenants::find($tenant->id)->get($key); + return $this->tenants->get($key, $tenant ?? $this->currentTenant()); } public function getMany(array $keys, Tenant $tenant = null): array { - $tenant = $tenant ?? $this->currentTenant(); - - return Tenants::find($tenant->id)->getMany($keys); + return $this->tenants->getMany($keys, $tenant ?? $this->currentTenant()); } public function put(string $key, $value, Tenant $tenant = null): void { - $tenant = $tenant ?? $this->currentTenant(); - Tenants::find($tenant->id)->put($key, $value); + $this->tenants->put($key, $value, $tenant ?? $this->currentTenant()); } public function putMany(array $kvPairs, Tenant $tenant = null): void { - $tenant = $tenant ?? $this->currentTenant(); - Tenants::find($tenant->id)->putMany($kvPairs); + $this->tenants->putMany($kvPairs, $tenant ?? $this->currentTenant()); } public function deleteMany(array $keys, Tenant $tenant = null): void { - $tenant = $tenant ?? $this->currentTenant(); - Tenants::find($tenant->id)->deleteMany($keys); + $this->tenants->deleteMany($keys, $tenant ?? $this->currentTenant()); } } - -class TenantModelTODO -{ - public static function makeTenant($data, $domains) - { - // TODO - } - - public static function findBy(string $key, $value) - { - // TODO - } - - // TODO Use this w/ DB builder calls instead of an Eloquent model -} - -class DomainModelTODO -{ - // TODO Use this w/ DB builder calls instead of an Eloquent model -} diff --git a/src/StorageDrivers/Database/DomainModel.php b/src/StorageDrivers/Database/DomainModel.php deleted file mode 100644 index abddff4b..00000000 --- a/src/StorageDrivers/Database/DomainModel.php +++ /dev/null @@ -1,25 +0,0 @@ -where('domain', $domain)->first()->tenant_id ?? null; + } + + public function occupied(array $domains): bool + { + return $this->whereIn('domain', $domains)->exists(); + } + + public function getTenantDomains($tenant) + { + $id = $tenant instanceof Tenant ? $tenant->id : $tenant; + return $this->where('tenant_id', $id)->get('domain')->toArray(); + } + + public function insertTenantDomains(Tenant $tenant) + { + $this->insert(array_map(function ($domain) use ($tenant) { + return ['domain' => $domain, 'tenant_id' => $tenant->id]; + }, $tenant->domains)); + } + + public function updateTenantDomains(Tenant $tenant) + { + $originalDomains = $this->domains->getTenantDomains($tenant); + $deletedDomains = array_diff($originalDomains, $tenant->domains); + $newDomains = array_intersect($originalDomains, $tenant->domains); + + $this->domains->whereIn('domain', $deletedDomains)->delete(); + + foreach ($newDomains as $domain) { + $this->insert([ + 'tenant_id' => $tenant->id, + 'domain' => $domain, + ]); + } + } + + public function getTable(ConfigRepository $config) + { + return $config->get('tenancy.storage_drivers.db.table_names.DomainModel') // legacy + ?? $config->get('tenancy.storage_drivers.db.table_names.domains') + ?? 'domains'; + } +} diff --git a/src/StorageDrivers/Database/Repository.php b/src/StorageDrivers/Database/Repository.php new file mode 100644 index 00000000..da11d4d3 --- /dev/null +++ b/src/StorageDrivers/Database/Repository.php @@ -0,0 +1,30 @@ +database = $database; + $this->table = $database->table($this->getTable($config)); + } + + abstract public function getTable(ConfigRepository $config); + + public function __call($method, $parameters) + { + return $this->table->$method(...$parameters); + } +} \ No newline at end of file diff --git a/src/StorageDrivers/Database/TenantModel.php b/src/StorageDrivers/Database/TenantModel.php deleted file mode 100644 index 78d7ab16..00000000 --- a/src/StorageDrivers/Database/TenantModel.php +++ /dev/null @@ -1,161 +0,0 @@ - $value) { - if (in_array($key, static::customColumns(), true)) { - $result[$key] = $value; - } else { - $jsonData[$key] = $value; - } - } - - $result['data'] = $jsonData ? json_encode($jsonData) : '{}'; - - return $result; - } - - public static function getAllTenants(array $ids) - { - $tenants = $ids ? static::findMany($ids) : static::all(); - - return $tenants->map([__CLASS__, 'decodeData'])->toBase(); - } - - public function decoded() - { - return static::decodeData($this); - } - - /** - * Return a tenant array with data decoded into separate keys. - * - * @param self|array $tenant - * @return array - */ - public static function decodeData($tenant) - { - $tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant; - $decoded = json_decode($tenant[$dataColumn = static::dataColumn()], true); - - foreach ($decoded as $key => $value) { - $tenant[$key] = $value; - } - - // If $tenant[$dataColumn] has been overriden by a value, don't delete the key. - if (! array_key_exists($dataColumn, $decoded)) { - unset($tenant[$dataColumn]); - } - - return $tenant; - } - - public function getFromData(string $key) - { - $this->dataArray = $this->dataArray ?? json_decode($this->{$this->dataColumn()}, true); - - return $this->dataArray[$key] ?? null; - } - - public function get(string $key) - { - return $this->attributes[$key] ?? $this->getFromData($key) ?? null; - } - - public function getMany(array $keys): array - { - return array_reduce($keys, function ($result, $key) { - $result[$key] = $this->get($key); - - return $result; - }, []); - } - - public function put(string $key, $value) - { - if (in_array($key, $this->customColumns())) { - $this->update([$key => $value]); - } else { - $obj = json_decode($this->{$this->dataColumn()}); - $obj->$key = $value; - - $this->update([$this->dataColumn() => json_encode($obj)]); - } - - return $value; - } - - public function putMany(array $kvPairs) - { - $customColumns = []; - $jsonObj = json_decode($this->{$this->dataColumn()}); - - foreach ($kvPairs as $key => $value) { - if (in_array($key, $this->customColumns())) { - $customColumns[$key] = $value; - continue; - } - - $jsonObj->$key = $value; - } - - $this->update(array_merge($customColumns, [ - $this->dataColumn() => json_encode($jsonObj), - ])); - } - - public function deleteKeys(array $keys) - { - $customColumns = []; - $jsonObj = json_decode($this->{$this->dataColumn()}); - - foreach ($keys as $key) { - if (in_array($key, $this->customColumns())) { - $customColumns[$key] = null; - continue; - } - - unset($jsonObj->$key); - } - - $this->update(array_merge($customColumns, [ - $this->dataColumn() => json_encode($jsonObj), - ])); - } -} diff --git a/src/StorageDrivers/Database/TenantRepository.php b/src/StorageDrivers/Database/TenantRepository.php new file mode 100644 index 00000000..4a627824 --- /dev/null +++ b/src/StorageDrivers/Database/TenantRepository.php @@ -0,0 +1,158 @@ +whereIn('id', $ids)->get(); + } + + return $this->table->get(); + } + + public function find($tenant) + { + return $this->table->find( + $tenant instanceof Tenant ? $tenant->id : $tenant + ); + } + + public function updateTenant(Tenant $tenant) + { + $this->putMany($tenant->data, $tenant); + } + + public function exists(Tenant $tenant) + { + return $this->where('id', $tenant->id)->exists(); + } + + public function get(string $key, Tenant $tenant) + { + return $this->decodedData($tenant)[$key]; + } + + public function getMany(array $keys, Tenant $tenant) + { + $decodedData = $this->decodedData($tenant); + + $result = []; + + foreach ($keys as $key) { + $result[$key] = $decodedData[$key]; + } + + return $result; + } + + public function put(string $key, $value, Tenant $tenant) + { + $record = $this->where('id', $tenant->id); + + if (in_array($key, static::customColumns())) { + $record->update([$key => $value]); + } else { + $data = json_decode($record->first(static::dataColumn()), true); + $data[$key] = $value; + + $record->update([static::dataColumn() => $data]); + } + } + + public function putMany(array $kvPairs, Tenant $tenant) + { + $record = $this->where('id', $tenant->id); + + $data = []; + $jsonData = json_decode($record->first(static::dataColumn()), true); + foreach ($kvPairs as $key => $value) { + if (in_array($key, static::customColumns())) { + $data[$key] = $value; + return; + } else { + $jsonData[$key] = $value; + } + } + + $data[static::dataColumn()] = json_encode($jsonData); + + $record->update($data); + } + + public function deleteMany(array $keys, Tenant $tenant) + { + $record = $this->where('id', $tenant->id); + + $data = []; + $jsonData = json_decode($record->first(static::dataColumn()), true); + foreach ($keys as $key => $key) { + if (in_array($key, static::customColumns())) { + $data[$key] = null; + return; + } else { + unset($jsonData[$key]); + } + } + + $data[static::dataColumn()] = json_encode($jsonData); + + $record->update($data); + } + + public function decodedData($tenant): array + { + return $this->decodeData( + $this->table->where('id', $tenant->id)->get()->toArray() + ); + } + + public static function decodeData(array $columns): array + { + $dataColumn = static::dataColumn(); + $decoded = json_decode($columns[$dataColumn], true); + $columns = array_merge($columns, $decoded); + + // If $columns[$dataColumn] has been overriden by a value, don't delete the key. + if (! array_key_exists($dataColumn, $decoded)) { + unset($columns[$dataColumn]); + } + + return $columns; + } + + public static function encodeData(array $data): array + { + $result = []; + foreach (array_intersect(static::customColumns(), array_keys($data)) as $customColumn) { + $result[$customColumn] = $data[$customColumn]; + unset($data[$customColumn]); + } + + $result = array_merge($result, [static::dataColumn() => json_encode($data)]); + + return $result; + } + + public static function customColumns(): array + { + return config('tenancy.storage_drivers.db.custom_columns', []); + } + + public static function dataColumn(): string + { + return config('tenancy.storage_drivers.db.data_column', 'data'); + } + + public function getTable(ConfigRepository $config) + { + return $config->get('tenancy.storage_drivers.db.table_names.TenantModel') // legacy + ?? $config->get('tenancy.storage_drivers.db.table_names.tenants') + ?? 'tenants'; + } +} \ No newline at end of file