diff --git a/assets/config.php b/assets/config.php index 8a266b83..702af5b0 100644 --- a/assets/config.php +++ b/assets/config.php @@ -2,10 +2,12 @@ declare(strict_types=1); +use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Database\Models\Tenant; return [ 'tenant_model' => Tenant::class, + 'domain_model' => Domain::class, 'internal_prefix' => 'tenancy_', 'central_connection' => 'central', diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php index 1ee0d5f3..aad343ee 100644 --- a/assets/migrations/2019_09_15_000020_create_domains_table.php +++ b/assets/migrations/2019_09_15_000020_create_domains_table.php @@ -16,9 +16,11 @@ class CreateDomainsTable extends Migration public function up(): void { Schema::create('domains', function (Blueprint $table) { - $table->string('domain', 255)->primary(); + $table->increments('id'); + $table->string('domain', 255)->unique(); $table->string('tenant_id', 36); + $table->timestamps(); $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); }); } diff --git a/src/Contracts/Tenant.php b/src/Contracts/Tenant.php new file mode 100644 index 00000000..dcb3a289 --- /dev/null +++ b/src/Contracts/Tenant.php @@ -0,0 +1,6 @@ +belongsTo(Tenant::class); + $ensureDomainIsNotOccupied = function (Domain $self) { + if ($domain = Domain::where('domain', $self->domain)->first()) { + if ($domain->getKey() !== $self->getKey()) { + throw new DomainsOccupiedByOtherTenantException; + } + } + }; + + static::saving($ensureDomainIsNotOccupied); } - protected $dispatchEvents = [ + public function tenant() + { + return $this->belongsTo(config('tenancy.tenant_model')); + } + + public $dispatchEvents = [ 'saved' => DomainSaved::class, 'created' => DomainCreated::class, 'updated' => DomainUpdated::class, diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 2f6eb7cb..c95e5382 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -5,9 +5,11 @@ namespace Stancl\Tenancy\Database\Models; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\DatabaseConfig; use Stancl\Tenancy\Events; +use Stancl\Tenancy\Contracts; // todo use a contract -class Tenant extends Model +// todo @property +class Tenant extends Model implements Contracts\Tenant { use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn { Concerns\HasADataColumn::getCasts as dataColumnCasts; @@ -80,7 +82,7 @@ class Tenant extends Model return $result; } - protected $dispatchesEvents = [ + public $dispatchesEvents = [ 'saved' => Events\TenantSaved::class, 'created' => Events\TenantCreated::class, 'updated' => Events\TenantUpdated::class, diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedException.php b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php similarity index 66% rename from src/Exceptions/TenantCouldNotBeIdentifiedException.php rename to src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php index fbebcd10..127407d9 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php @@ -7,8 +7,9 @@ namespace Stancl\Tenancy\Exceptions; use Facade\IgnitionContracts\BaseSolution; use Facade\IgnitionContracts\ProvidesSolution; use Facade\IgnitionContracts\Solution; +use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -class TenantCouldNotBeIdentifiedException extends \Exception implements ProvidesSolution +class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution { public function __construct($domain) { @@ -20,7 +21,7 @@ class TenantCouldNotBeIdentifiedException extends \Exception implements Provides return BaseSolution::create('Tenant could not be identified on this domain') ->setSolutionDescription('Did you forget to create a tenant for this domain?') ->setDocumentationLinks([ - 'Creating Tenants' => 'https://tenancy.samuelstancl.me/docs/v2/creating-tenants/', + 'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v2/creating-tenants/', // todo update link for v3 ]); } } diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php new file mode 100644 index 00000000..2f0c647e --- /dev/null +++ b/src/Resolvers/DomainTenantResolver.php @@ -0,0 +1,22 @@ +where('domain', $args[0])->first(); + + if ($domain) { + return $domain->tenant; + } + + throw new TenantCouldNotBeIdentifiedOnDomainException($domain); + } +} \ No newline at end of file diff --git a/tests/v3/HostnameIdentificationTest.php b/tests/v3/CachedResolutionTest.php similarity index 100% rename from tests/v3/HostnameIdentificationTest.php rename to tests/v3/CachedResolutionTest.php diff --git a/tests/v3/DomainTest.php b/tests/v3/DomainTest.php index e69de29b..4c987fcf 100644 --- a/tests/v3/DomainTest.php +++ b/tests/v3/DomainTest.php @@ -0,0 +1,70 @@ + Tenant::class]); + } + + /** @test */ + public function tenant_can_be_identified_using_hostname() + { + $tenant = Tenant::create(); + + $id = $tenant->id; + + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + + $resolvedTenant = app(DomainTenantResolver::class)->resolve('foo.localhost'); + + $this->assertSame($id, $resolvedTenant->id); + $this->assertSame(['foo.localhost'], $resolvedTenant->domains->pluck('domain')->toArray()); + } + + /** @test */ + public function a_domain_can_belong_to_only_one_tenant() + { + $tenant = Tenant::create(); + + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + + $tenant2 = Tenant::create(); + + $this->expectException(DomainsOccupiedByOtherTenantException::class); + $tenant2->domains()->create([ + 'domain' => 'foo.localhost', + ]); + } + + /** @test */ + public function an_exception_is_thrown_if_tenant_cannot_be_identified() + { + $this->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); + + app(DomainTenantResolver::class)->resolve('foo.localhost'); + } +} + +class Tenant extends Models\Tenant +{ + public function domains() + { + return $this->hasMany(Domain::class); + } +}