⌘K

Panels

Tenancy

Tenant v URL — nalezený podle slugu, ověřený proti přihlášenému člověku, nastavený jako aktuální a přenesený do každého odkazu, který framework staví.

Na této stránce

Tenant je firma, kterou aplikace obsluhuje, a tenantová zóna je skupina stránek, která pod ní žije: /app/acme/orders, nebo acme.example.com/orders. Tahle stránka je polovina o routování — jak se tenant dostane z URL do requestu. Které řádky tenant vidí, je polovina o datech, a ta je v Autorizaci § Multi-tenancy.

Jak to funguje

Tenant je parametr routy jménem tenant, v prefixu nebo před doménou, a middleware wire.tenant z něj udělá aktuálního tenanta. Na každém requestu uvnitř zóny udělá čtyři věci, v tomto pořadí:

  1. Najde tenanta podle route key modelu — getRouteKeyName(), slug, pokud to model tak říká — bez globálních scopů modelu.
  2. Ověří přihlášeného člověka: HasTenants::canAccessTenant($tenant). Cizí člověk, neznámý slug i host dostanou stejné 404 — 403 by tomu, kdo napsal /app/globex/, prozradilo, že globex existuje.
  3. Vstoupí do něj (CurrentTenant), takže každý model s BelongsToTenant je na něj scopovaný bez čehokoli dalšího.
  4. Přenese ho: URL::defaults(['tenant' => …]), takže každé URL postavené od té chvíle — urlFor(), OrderResource::url(), menu, výsledky hledání, drobečky, přesměrování po uložení, vaše vlastní route() — míří do téhož tenanta. Parametr se pak z routy odebere, takže žádný mount() stránky nedostane parametr, o který si neřekl.

Livewire round trip ho udrží. Update request jde na livewire/update, kde žádný {tenant} není; middleware je zaregistrovaný jako Livewire persistent middleware, takže Livewire sestaví původní request stránky a spustí ho znovu. Druhé vykreslení je scopované a prolinkované stejně jako první.

Pasti. Zóna round trip přežije stejně, na každé stránce zaregistrované přes Route::wireResources(); komponenta na vaší vlastní routě si zónu přečtenou v mountu nechá a předá ji (Routování). A uživatelský model, který neimplementuje HasTenants, se při prvním requestu odmítne výjimkou — nikdy se nečte jako „smí do každého tenanta“.

Tenantová zóna

Jedna volba u skupiny rout panelu udělá všechno — parametr, middleware a vlastní adresu zóny vedle. V route souboru:

Route::middleware(['web', 'auth'])->prefix('app') // nebo ->domain('example.com')
->group(fn () => Route::wire('panel', zone: 'app', tenant: 'path'));

Nebo jako záznam wire-core.routes.groups (Routování):

// config/wire-core.php
'routes' => ['groups' => [
'app' => ['uses' => 'panel', 'prefix' => 'app', 'tenant' => 'path'], // app/{tenant}/…
'portal' => ['uses' => 'panel', 'domain' => 'example.com', 'tenant' => 'domain'], // {tenant}.example.com
]],

Vypsaná ručně je to tatáž skupina, s parametrem a middlewarem v ní od vás:

Route::name('app.')->prefix('app/{tenant}')->middleware(['web', 'auth', 'wire.tenant'])
->group(fn () => Route::wireResources());

Nic tady s tenancy nepočítá. Zóna bez tenant je obyčejná zóna a stránka, která existuje jen uvnitř firmy — profil a členové z modulu firem —, se v ní přeskočí, takže aplikace, která tenancy nepoužívá, nikdy nenese adresu, která by mohla odpovědět jen 404.

Cesta nepotřebuje nic od DNS a dovolí mít dva tenanty otevřené ve dvou záložkách; doména potřebuje wildcard DNS záznam a certifikát a doménu session cookie, která pokryje subdomény (SESSION_DOMAIN=.example.com). Hodnota jiná než path nebo domain, nebo domain u zóny bez domain, se odmítne při registraci rout.

Vlastní adresa zóny

Každá stránka tenantové zóny žije pod firmou, takže samotné /app by bylo 404 — a přitom je to adresa, kterou člověk napíše, a ta, kam vede přihlášení. Místo toho odpoví: pošle na getDefaultTenant() přihlášeného a odtud na první stránku té firmy, kterou smí otevřít.

tenant: ji zaregistruje sám, na prefixu bez tenanta nebo v kořeni domény, pod jménem {zone}.wire.tenants — a právě to nabídne výběr zón jako adresu zóny. Ručně vypsaná tenantová zóna ji umístí jako vlastní skupinu a řekne, kde je adresa firmy:

Route::name('app.')->middleware(['web', 'auth'])
->group(fn () => Route::wire('tenant-entry', uri: 'app', to: 'app/{tenant}'));

to má {tenant} tam, kam patří klíč firmy — //{tenant}.example.com pro doménovou zónu.

Kdo nepatří do žádné firmy, dostane wire-panels.routes.tenant_entry.view, pokud ho aplikace pojmenuje — místo, kde nabídnout registraci firmy — a jinak 403, které řekne proč. Modul firem pojmenuje vlastní, když to aplikace neudělala.

Přepínač firem

Uvnitř firmy dostane horní lišta každého shellu, který kreslí PageChrome::TOPBAR, přepínač z wire-panels. Nabídne firmy, do kterých člověk patří, každou s odkazem na tutéž stránku v té firmě — kromě stránky záznamu, kde odkaz vede na seznam téhož resource: záznam 7 jedné firmy není záznamem jiné a přepnutí by skončilo na 404. Mimo firmu, nebo pro někoho s jedinou firmou, se nevykreslí nic.

Firma se jmenuje podle wire-core.tenancy.label (name), a když je prázdné, podle svého route key.

Model tenanta

// config/wire-core.php
'tenancy' => [
'enabled' => true,
'model' => App\Models\Company::class,
'members_table' => 'tenant_user',
],
class Company extends Model
{
public function getRouteKeyName(): string
{
return 'slug'; // /app/acme/… místo /app/12/…
}
}

Kdo kam patří

Odpovídá uživatelský model, přes HasTenants:

use NyonCode\WirePanels\Concerns\InteractsWithTenants;
use NyonCode\WirePanels\Contracts\HasTenants;
 
class User extends Authenticatable implements HasTenants
{
use InteractsWithTenants;
}

InteractsWithTenants čte členství z vazby many-to-many na model tenanta přes members_table, se jmény sloupců pivotu podle konvence Laravelu — user_id a pro Company company_id. Členství jiného tvaru přepíše tenants(), nebo implementuje tři metody samo:

Metoda Vrací Účel
getTenants(): iterable iterable<int, Model> Tenanti, do kterých člověk patří — co nabídne přepínač
canAccessTenant(Model $tenant): bool bool Jestli v tomhle smí pracovat; ptá se na každém requestu uvnitř
getDefaultTenant(): ?Model Model|null Kam ho pošle vlastní adresa zóny

Související